2026 개정판 · 1차 8월 5일 공개전체 커리큘럼 →
TECH 으로 돌아가기
TECH HACKER NEWS 1주 전 7분 읽기 98 READS

C++26 std::indirect가 PImpl 관용구를 어떻게 단순화하는가

클래스의 구현 세부사항을 헤더에서 걷어내 별도의 클래스로 옮기고, 이를 불투명한 포인터(opaque pointer)를 통해서만 접근하게 만드는 기법을 PImpl(Pointer to Implementation)이라고 부른다. 이 관용구의 목적은 인터페이스와 구현을 분리해 컴파일 시점의 의존성을 최소화하는 데 있다. 헤더에 멤버 변수와 내부 자료구조가 그대로 노출되면, 그 세부사항이 조금만 바뀌어도 헤더를 포함한 모든 번역 단위가 다시 컴파일되어야 한다. PImpl은 이 연쇄 재컴파일을 끊어내고, 라이브러리의 ABI 안정성을 확보하는 실무적 도구로 오래 쓰여 왔다. C++26에 새로 들어오는 std::indirect 타입은 이 오래된 패턴을 조금 더 안전하게 다듬는다.

원시 포인터와 unique_ptr로 구현할 때의 부담

원문은 라벨을 갖고 클릭할 때마다 카운터가 증가하는 UI 위젯을 예로 든다. Widget 클래스는 내부 상태인 카운터와 라벨을 Widget::Impl이라는 별도 클래스에 숨기고, 헤더에는 그 클래스를 전방 선언(forward declaration)만 해 둔 뒤 실제 정의는 .cpp 파일에 둔다. 헤더에서 Impl은 불완전 타입(incomplete type)이며, Widget은 이 타입을 가리키는 포인터만 보유한다.

원시 포인터로 이를 구현하면 이른바 '5의 규칙(Rule of Five)'을 따라야 한다. 자원 관리를 위해 특수 멤버 함수를 하나라도 정의하면 소멸자, 복사 생성자, 복사 대입 연산자, 이동 생성자, 이동 대입 연산자 다섯 가지를 모두 정의해야 한다는 지침이다. 특히 이동 연산자에는 noexcept를 붙이는 것이 중요한데, 단순히 예외가 나지 않는다는 표시를 넘어 성능 문제이기도 하다. std::vector 같은 컨테이너는 재할당 시 이동 생성자가 noexcept일 때만 요소를 이동하고, 그렇지 않으면 강한 예외 보증을 지키기 위해 복사로 되돌아가기 때문이다.

C++11의 std::unique_ptr을 쓰면 자원 관리를 자동화해 부담을 줄일 수 있다. 다만 복사 생성자와 복사 대입은 여전히 직접 정의해야 하고, 소멸자·이동 생성자·이동 대입은 컴파일러에 위임(default)하되 이 위임을 반드시 .cpp 파일에서 해야 한다. 헤더에서는 Impl이 불완전 타입이라 unique_ptr의 삭제자가 Impl의 크기를 알 수 없기 때문이다.

두 가지 함정: const 전파와 이동 후 널

포인터 기반 구현에는 미묘하지만 실무에서 자주 발목을 잡는 두 가지 함정이 있다. 첫째는 const가 Widget에서 Impl로 전파되지 않는다는 점이다. clickCount() 같은 const 메서드 안에서도 포인터 자체가 const일 뿐 그 포인터가 가리키는 객체는 const가 아니어서, const 메서드가 내부 상태를 바꿔도 컴파일이 통과하고 의도치 않은 결과를 낳을 수 있다. 둘째는 이동 의미론의 결과로, 이동된(moved-from) Widget 객체는 pimpl_ 포인터가 널이 되며, 이 상태에서 포인터를 쓰는 함수를 호출하면 정의되지 않은 동작(UB)이 된다. unique_ptr로 바꿔도 이 두 문제는 그대로 남는다.

std::indirect가 채우는 빈틈

std::indirect는 헤더에 정의된 C++26의 새 어휘 타입(vocabulary type)으로, 동적으로 할당되지만 값(value)처럼 동작해야 하는 클래스 멤버를 위해 설계됐다. unique_ptr의 의미론, 즉 복사 불가·const 비전파·널 가능이라는 성질이 적절하지 않은 자리에 대신 쓰도록 만들어졌고, PImpl이 바로 그 대표적인 사례다. std::indirect는 힙에 할당된 단일 T 객체를 소유하면서도 T 멤버처럼 행동한다.

다만 오해하면 안 되는 지점이 있다. std::indirect를 쓴다고 해서 다섯 개의 특수 멤버 함수를 직접 선언하고 위임하는 작업 자체가 사라지지는 않는다. 여전히 헤더에서 선언하고, Impl이 완전한 타입으로 알려진 .cpp 파일에서 default로 위임해야 한다. 이는 unique_ptr과 동일한 요구다. std::indirect가 실제로 해결하는 것은 const 전파다. const를 올바르게 전파해 주기 때문에 const 메서드가 실수로 내부 상태를 바꾸는 사고에 대해 코드가 더 견고해진다.

이동 후 널 검사에 해당하는 기능은 valueless_after_move()가 제공한다. pimpl_ 객체를 쓰는 모든 함수에 assert를 걸어 위젯이 이동된 뒤에는 호출되지 않도록 방어할 수 있고, 벡터에서 이동해 낸 객체들을 erase-remove로 걸러내는 데에도 활용할 수 있다. 원문도 지적하듯 이는 unique_ptr에서 w == nullptr로 하던 검사를 다소 장황하게 바꾼 것에 가까워서, 값을 아직 보유하고 있는지를 명시적으로 확인하는 표현일 뿐 근본적으로 새로운 능력은 아니다.

실무 관점에서 std::indirect의 가치는 '마법 같은 단순화'라기보다 오작동하기 쉬운 지점 하나를 언어 차원에서 막아 주는 데 있다. 특수 멤버 함수를 손수 챙겨야 하는 번거로움은 남지만, const 안전성이라는 미묘한 버그의 온상이 제거된다는 점은 대규모 코드베이스에서 의미가 크다. 현실적인 제약도 분명하다. 집필 시점 기준으로 std::indirect를 지원하는 컴파일러는 GCC 16뿐이므로, 당장 프로덕션에 도입하기보다는 도구 지원이 넓어지는 시점을 대비해 패턴을 익혀 두는 편이 적절하다. 참고로 std::indirect에는 다형적 타입을 위한 짝인 std::polymorphic이 함께 도입된다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://mariusbancila.ro/blog/2026/07/23/the-pimpl-idiom-and...
SHARE
NEXT · CHOOSE

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

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

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