서론: C#의 object와 dynamic, 그리고 C++의 철학적 차이
C#에서는 object가 모든 참조형의 공통 기반 타입으로 작동하며, dynamic은 런타임 시점에 타입 확인을 지연시킵니다. 반면 C++은 성능 우선 설계를 지향하기 때문에 컴파일 타임에 가능한 많은 결정을 내리는 것을 선호합니다. 이로 인해 C++은 동적 타입 같은 개념을 직접 제공하지 않으며, 대신 다양한 메커니즘을 통해 유사한 요구를 해결합니다.
1. 정적 다형성 – 템플릿 (Templates)
템플릿은 C++의 제네릭 프로그래밍 핵심 도구로, C#의 제네릭스와 유사하지만 그 유연성과 강력함 면에서 더 깊은 수준까지 확장됩니다.
- 기본 원리: 템플릿은 사용되는 각 타입마다 별도의 함수 또는 클래스 인스턴스를 컴파일 시 생성합니다. 즉, 코드가 실제로 필요한 순간에 맞춰 특수화된 버전이 만들어집니다.
- 덕 타이핑 지원: 특정 인터페이스를 구현할 필요 없이, 오직 필요한 멤버 함수나 연산자가 존재하면 됩니다. 예를 들어:
template <typename Animal>
void makeSound(Animal& a) {
a.makeNoise(); // a가 makeNoise()를 호출할 수 있으면 컴파일 성공
}
이 방식은 인터페이스 계약보다는 행동 기반 접근을 허용하므로, 보다 유연한 재사용이 가능합니다.
2. 제한된 다중 타입 저장 – std::variant (C++17)
변수가 미리 정의된 소수의 타입 중 하나만 가질 수 있을 때, std::variant는 이상적인 선택입니다. 이는 안전한 union의 진화 형태로 간주될 수 있습니다.
- 메모리 관리: 스택 상에 할당되며, 내부 최대 타입 크기에 맞춰 고정된 크기를 가집니다. GC나 힙 할당이 필요 없습니다.
- 타입 안정성: 잘못된 타입 접근 시 예외(
std::bad_variant_access) 발생. 안전하게 값을 추출하려면std::get<T>또는std::visit사용이 권장됩니다.
#include <variant>
#include <string>
std::variant<int, double, std::string> value = 3.14;
value = "variant example"; // 상태 전환
// 패턴 매칭 스타일 처리
std::visit([](const auto& v) {
std::cout << v << '\n';
}, value);
3. 완전한 타입 자유도 – std::any
실제로 어떤 타입이 들어올지 전혀 예측할 수 없는 경우, std::any는 C#의 object처럼 모든 타입의 값을 담을 수 있습니다.
- 내부 구조: 내부 값은 일반적으로 힙에 복사되며, 타입 정보와 함께 저장됩니다.
- 값 추출: 반드시 원래 타입을 알고 있어야 하며,
std::any_cast를 통해 안전하게 변환해야 합니다. 실패 시std::bad_any_cast예외가 발생합니다.
#include <any>
#include <iostream>
std::any data = 2024;
data = std::vector<double>{1.1, 2.2};
if (data.type() == typeid(std::vector<double>)) {
auto vec = std::any_cast<std::vector<double>>(data);
std::cout << "Vector size: " << vec.size() << "\n";
}
4. 객체 지향적 다형성 – 가상 함수 (Virtual Functions)
C#의 virtual/override 메커니즘과 가장 유사한 C++의 접근법은 포인터 또는 참조를 통한 동적 바인딩입니다.
- 명시적 설정 필요: 기본적으로 C++ 멤버 함수는 정적 바인딩됩니다. 동적 디스패치를 원하면 반드시
virtual키워드를 명시해야 하며, 이를 통해 vtable 기반의 실행 시간 다형성이 활성화됩니다. - 예시:
class Animal {
public:
virtual void speak() const { std::cout << "..." << "\n"; }
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void speak() const override { std::cout << "Woof!\n"; }
};
Animal* pet = new Dog();
pet->speak(); // 출력: Woof!
비교 및 요약
| 요구 사항 | C# 솔루션 | C++ 대안 | 차이점 |
|---|---|---|---|
| 모든 타입을 담을 수 있는 컨테이너 | object, dynamic |
std::any |
C++는 명시적 캐스트 필요, 힙 할당 발생 |
| 재사용 가능한 제네릭 로직 | Generics |
Templates |
템플릿은 컴파일 타임 코드 생성, 런타임 오버헤드 없음 |
| 제한된 타입 집합 내 전환 | Union 또는 switch on type |
std::variant |
스택 기반, 타입 안전, visit 패턴 지원 |
| 런타임 다형성 | virtual 메서드 |
virtual 함수 + 상속 |
C++에서 비가상 함수는 기본이며, 성능 최적화 의도 |
디자인 조언 및 주의사항
- std::any 남용 금지: 너무 많은
std::any사용은 설계의 모호성을 나타냅니다. 가능한 한 타입을 컴파일 타임에 결정하세요. - 템플릿 우선 고려: 제네릭 알고리즘 작성 시 템플릿은 가장 효율적인 방법입니다. 초기 문법 난해함을 극복하면 강력한 도구가 됩니다.
- variant로 타입 상태 머신 표현: 상태 기반 데이터 처리나 파서 구현 등에서
std::variant는 매우 효과적입니다.