TECH 으로 돌아가기
TECH HACKER NEWS 오늘 7분 읽기 24 READS

C++26 std::polymorphic: 복제 코드 없이 다형성 멤버를 값처럼 다루기

C++에서 "어떤 클래스가 다른 객체를 자신의 일부로 소유한다"는 합성(composition) 관계는 흔하지만, 그 소유 대상이 실행 시점에 여러 하위 타입으로 달라질 수 있을 때 코드가 금세 지저분해진다. 기본 타입을 가리키는 포인터를 멤버로 들고 다형성을 얻는 대신, 소유하는 쪽 클래스를 복사 가능하고 일반적인 값(value) 타입으로 유지하려면 그동안 상당한 보일러플레이트가 필요했다. C++26에 추가된 std::polymorphic은 바로 이 지점을 겨냥한 표준 유틸리티다. PImpl 관용구를 돕는 std::indirect와 함께 표준에 편입됐는데, std::indirect가 타입이 고정된 경우를 다룬다면 std::polymorphic은 동적 타입이 바뀌는 멤버를 담당한다.

왜 기존 방식이 번거로웠나

원문은 스마트홈 센서 이벤트를 예로 든다. 온도 등 여러 센서가 만들어내는 이벤트를 공통 기반 클래스 Event에서 파생시키고, 이들을 이벤트 로그(EventLog)에 모아 저장하려는 상황이다. 이때 로그가 기반 클래스 포인터로 다양한 파생 이벤트를 담되, 로그 자체는 복사해도 내부 이벤트까지 깊게 복사되는 정상적인 값처럼 동작하게 만들고 싶다면, 전통적으로는 가상 clone() 같은 복제 메커니즘을 직접 구현해야 했다. 그 결과 소유 클래스는 복사 생성자, 복사 대입, 이동 연산, 소멸자를 모두 손봐야 하는 이른바 Rule of Five의 부담을 떠안게 된다. 다형성을 얻는 대가로 특수 멤버 함수 전부를 사람이 관리해야 하는 셈이다.

std::polymorphic이 해결하는 것

std::polymorphic을 쓰면 복제 메커니즘 자체가 필요 없어진다. Event 클래스에서 수동 clone() 계열 장치를 걷어낼 수 있고, 파생 클래스도 그만큼 단순해진다. 가장 큰 변화는 EventLog다. 더 이상 Rule of Five를 지키며 특수 멤버 함수를 전부 작성할 필요가 없고, 오히려 아무것도 작성하지 않는 Rule of Zero를 따를 수 있다. 중요한 점은 이 과정에서 클라이언트 코드는 바뀌지 않는다는 것이다. 즉, 사용하는 쪽의 인터페이스는 그대로 두고 소유 클래스 내부의 복잡도만 걷어낸다. 결과적으로 std::polymorphic 멤버는 다시 평범한 멤버처럼 행동한다. 복사하면 함께 복사되고, const가 내부로 전파되며, 항상 존재하고(널이 아님), 그러면서도 자신의 동적 타입을 유지한다.

virtual clone() 없이 어떻게 복사가 되나

자연히 드는 의문이 있다. TemperatureReadingEvent를 담은 std::polymorphic를 복사할 때, 가상 clone()도 없는데 어떻게 올바른 구체 타입을 복제하는 걸까. 답은 생성 시점에 포착되는 복사 연산의 타입 소거(type erasure)다. std::in_place_type로 객체를 구성하는 순간 생성자는 구체 타입이 무엇인지 정적으로 알고 있으며, 이때 그 타입에 맞는 복사·소멸 연산을 내부 제어 구조에 저장해 둔다. 이후 std::polymorphic의 복사 생성자는 저장된 그 연산을 호출할 뿐이다. 이는 std::function이나 std::any가 쓰는 것과 동일한 기법을, 복사 생성자에 적용한 것이다. 같은 이유로 소거된 소멸 연산도 구체 타입을 이미 알고 있어, std::polymorphic이 객체를 파괴하는 데는 기반 클래스의 가상 소멸자가 굳이 필요하지 않다. 다만 원문도 분명히 짚듯이, 그렇다고 가상 소멸자를 제거하라는 뜻은 아니다. 다른 코드가 여전히 기반 포인터를 통해 delete할 수 있기 때문이다. 나머지 인터페이스는 std::indirect와 대체로 같은 모양을 띤다.

언제 무엇을 고를 것인가

선택 기준은 비교적 명확하게 정리된다. 어떤 클래스가 소유하는 구성 요소의 동적 타입이 런타임에 달라지고, 그 구성 요소가 소유자의 값의 일부일 때 std::polymorphic을 쓴다. 소유자를 복사하면 그 요소도 복사돼야 하고, const가 그 요소까지 전파돼야 하며, 요소가 비어 있는 상태가 없어야 하는 경우다. 전략(strategy) 멤버, 이벤트 로그 같은 이종(heterogeneous) 컬렉션, 그리고 표현식 트리처럼 재귀적인 구조가 전형적인 사례다. 반대로 타입이 고정돼 있다면 std::indirect가 맞고, 가능한 대안의 집합이 닫혀 있다면 std::variant가 더 낫다. 또한 그 객체가 값이라기보다 여러 주체가 공유하는 정체성(shared identity)에 가깝다면 스마트 포인터를 쓰는 편이 맞다.

한국의 C++ 실무자 입장에서 이 기능의 의미는 "다형성과 값 의미론(value semantics)의 양립"에 있다. 그동안 다형적 멤버를 쓰려면 포인터와 수동 생명주기 관리로 값으로서의 성질을 포기하거나, clone()과 Rule of Five를 직접 떠안아야 했다. std::polymorphic은 그 타협을 표준 라이브러리 수준에서 없애 준다. 다만 전제가 있다. C++26 기능이므로 당장 프로덕션에 투입하려면 컴파일러와 표준 라이브러리의 지원 성숙도를 확인해야 하고, 공유가 본질인 객체나 타입 집합이 닫힌 경우에는 각각 스마트 포인터와 std::variant가 여전히 더 적절한 도구라는 점이다. 새 타입을 "포인터의 상위 호환"으로 오해하지 말고, 소유와 값 의미론이 필요한 합성 관계에 한정해 적용하는 것이 핵심이다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://mariusbancila.ro/blog/2026/10/18/composite-objects-a...
SHARE
NEXT · CHOOSE

변화를 읽었다면,
내가 만들 수익 구조를 고릅니다.

정보를 더 모으는 데서 멈추지 않고, 광고·외주·판매·중개·구독 중 내 상황에 맞는 출발점을 정해보세요.

21가지 수익 구조 살펴보기 →
처리 중...