특수 함수 및 Pimpl Idiom

발행: (2026년 3월 7일 AM 08:30 GMT+9)
5 분 소요
원문: Dev.to

Source: Dev.to

Benefits

이점은 컴파일 시간 감소와 헤더 관리 용이성 형태로 나타납니다.
컴파일 시간이 줄어드는 이유는, 모든 멤버를 헤더 파일에 선언하는 대신(그 경우 해당 헤더들을 포함해야 함) 멤버들을 포인터 뒤에 숨기기 때문입니다. 예를 들어 std::vectorstd::string 타입의 멤버가 있다면 보통은 그 헤더들을 포함해야 합니다. 이러한 멤버 타입이나 자주 수정되는 사용자 정의 타입이 바뀔 경우, 클래스 전체 헤더가 다시 컴파일되는 상황을 방지할 수 있습니다.

Use Pointers

이름 그대로 구현에 대한 포인터를 보관합니다. 포인터는 원시 포인터, std::unique_ptr, 혹은 std::shared_ptr가 될 수 있습니다.
원시 포인터와 유니크 포인터가 가장 성능에 친화적이므로, 먼저 그것들을 살펴보겠습니다.

Pointer

// Header file
class Widget
{
public:
    Widget();
    ~Widget();

private:
    struct Impl;               // forward declaration (incomplete type)
    Impl* pImpl;                // pointer to implementation
};
// Source file
struct Widget::Impl
{
    std::vector    m_age;
    std::string    m_name;
    PlayerController    m_controller;   // custom type
};

Widget::Widget()
{
    // Constructor
    pImpl = new Impl();
}

Widget::~Widget()
{
    delete pImpl;
}

Unique Pointer

코드에 들어가기 전에 Pimpl idiom에 대한 몇 가지 기본 사항을 짚고 넘어갑니다:

  • private 데이터를 숨겨서 public API를 안정적으로 유지합니다.
  • 헤더에는 작은 포인터(보통 4바이트 또는 8바이트)만 포함되므로 구현이 바뀌어도 헤더를 다시 컴파일할 필요가 없습니다.
  • 간접 참조가 약간의 런타임 오버헤드를 도입합니다.
  • 복사 연산자를 정의할 때 특별히 주의가 필요합니다.

이 접근법은 많은 수의 private 멤버를 가진 클래스에 적합합니다.

// Header file
class Person
{
public:
    Person(std::string name, std::string address);
    ~Person();

    // Copy operations
    Person(const Person& rhs);
    Person& operator=(const Person& rhs) = delete;

    // Move operations
    Person(Person&& rhs);
    Person& operator=(Person&& rhs) = delete;

    std::string GetAttributes() const;

private:
    struct Impl;
    std::unique_ptr pImpl;
};
// Source file
// Define the structure that holds the member data
struct Person::Impl
{
    std::string name;
    std::string address;
};

Person::Person(std::name, std::address) : pImpl{std::make_unique()}
{
    pImpl->name    = name;
    pImpl->address = address;
}

Person::~Person() = default;

Person::Person(const Person& rhs)
{
    pImpl = std::make_unique();
    pImpl->name    = rhs.pImpl->name;
    pImpl->address = rhs.pImpl->address;
}

Person::Person(Person&& rhs) noexcept
{
    pImpl = std::move(rhs.pImpl);
}

std::string Person::GetAttributes() const
{
    return pImpl->name + '\t' + pImpl->address;
}

A few things to note

  • 구현 구조체는 이를 사용하는 모든 생성자보다 먼저 정의되어야 합니다. 그렇지 않으면 std::unique_ptr가 불완전 타입을 관리할 수 없다는 오류가 발생합니다.
  • 소멸자를 선언하면 컴파일러가 자동으로 생성하는 이동 연산자가 억제되므로, 필요하다면 직접 정의해야 합니다.
  • 유니크 포인터를 사용할 경우 사용자 정의 복사 생성자를 제공해야 합니다; 기본 복사는 얕은 복사가 되기 때문입니다.

Shared Pointer

std::shared_ptr를 Pimpl idiom에 사용하는 경우는 드물지만, 공유 포인터는 본질적으로 복사와 이동이 가능하므로 별도의 복사·이동 연산자를 작성할 필요가 없습니다. 또한 불완전 타입을 지원하므로 구현 구조체를 헤더에서 전방 선언만으로 두어도 됩니다.

0 조회
Back to Blog

관련 글

더 보기 »

C#와 SOLID 원칙

소프트웨어 개발에서 SOLID 원칙은 로버트 C. 마틴이 정의한 다섯 가지 기본 지침으로, 유연하고 가독성이 높으며 유지보수가 쉬운 코드를 만들도록 돕습니다.

C#와 F#의 주요 차이점은 무엇인가요?

소개 종종 우리 .NET 클라이언트가 이 질문을 합니다: C를 사용해야 할까요, 아니면 F를 사용해야 할까요? 두 언어 모두 동일한 .NET 런타임에서 실행되며 동일한 라이브러리에 접근합니다.