C ++에서 동일한 목적으로 인터페이스를 사용할 수있는 반면 PImpl 패턴의 요점은 무엇입니까?


49

C ++에서 PImpl 관용구를 사용하는 많은 소스 코드가 보입니다. 개인 데이터 / 유형 / 구현을 숨기고 의존성을 제거한 다음 컴파일 시간과 헤더 포함 문제를 줄이는 것이 목적이라고 가정합니다.

그러나 C ++의 인터페이스 / 순수한 클래스에도이 기능이 있으며 데이터 / 유형 / 구현을 숨길 수 있습니다. 그리고 객체를 생성 할 때 호출자가 인터페이스를 볼 수있게하기 위해 인터페이스 헤더에 팩토리 메소드를 선언 할 수 있습니다.

비교는 다음과 같습니다.

  1. 비용 :

    공용 래퍼 함수 구현을 반복 할 필요가 없기 때문에 인터페이스 방식 비용이 저렴 void Bar::doWork() { return m_impl->doWork(); }합니다. 인터페이스에서 서명 만 정의하면됩니다.

  2. 잘 이해 :

    인터페이스 기술은 모든 C ++ 개발자가 더 잘 이해합니다.

  3. 성능 :

    인터페이스 방식 성능은 추가 메모리 액세스 인 PImpl 관용구보다 나쁘지 않습니다. 성능이 같다고 가정합니다.

다음은 내 질문을 설명하는 의사 코드입니다.

// Forward declaration can help you avoid include BarImpl header, and those included in BarImpl header.
class BarImpl;
class Bar
{
public:
    // public functions
    void doWork();
private:
    // You don't need to compile Bar.cpp after changing the implementation in BarImpl.cpp
    BarImpl* m_impl;
};

인터페이스를 사용하여 동일한 목적을 구현할 수 있습니다.

// Bar.h
class IBar
{
public:
    virtual ~IBar(){}
    // public functions
    virtual void doWork() = 0;
};

// to only expose the interface instead of class name to caller
IBar* createObject();

PImpl의 요점은 무엇입니까?

답변:


50

첫째, PImpl은 일반적으로 비다 형성 클래스에 사용됩니다. 다형성 클래스에 PImpl이있는 경우 일반적으로 다형성으로 유지됩니다. 이는 여전히 인터페이스를 구현하고 기본 클래스 등의 가상 메소드를 대체합니다. 따라서 PImpl의 간단한 구현은 인터페이스 가 아니며 멤버를 직접 포함하는 간단한 클래스입니다!

PImpl을 사용해야하는 세 가지 이유가 있습니다.

  1. 제작 바이너리 개인 회원의 인터페이스 (ABI) 독립을. 종속 코드를 다시 컴파일하지 않고 공유 라이브러리를 업데이트 할 수 있지만 바이너리 인터페이스가 동일하게 유지되는 한 에만 가능 합니다. 비 멤버 함수 추가 및 비가 상 멤버 함수 추가를 제외한 거의 모든 헤더 변경으로 ABI가 변경됩니다. PImpl 관용구는 개인 구성원의 정의를 소스로 이동하여 ABI와 해당 정의를 분리합니다. 깨지기 쉬운 이진 인터페이스 문제 참조

  2. 헤더가 변경되면 헤더를 포함한 모든 소스를 다시 컴파일해야합니다. 그리고 C ++ 컴파일은 다소 느립니다. 따라서 개인 구성원의 정의를 소스로 이동하면 PImpl 관용구가 헤더에서 더 적은 종속성을 가져와야하므로 컴파일 시간을 줄이고 종속 항목을 다시 컴파일 할 필요가 없으므로 수정 후 컴파일 시간을 줄입니다. (이것은 숨겨진 콘크리트 클래스가있는 인터페이스 + 팩토리 기능에도 적용됩니다).

  3. C ++의 많은 클래스에서 예외 안전은 중요한 속성입니다. 여러 클래스를 구성해야 할 경우가 종종 있는데, 둘 이상의 멤버에서 작업하는 동안 멤버가 수정되지 않거나 멤버가 throw 될 때 멤버가 일치하지 않는 상태를 유지하도록하는 작업이있는 경우 포함 오브젝트를 유지해야합니다. 일관된. 이 경우 PImpl의 새 인스턴스를 생성하여 작업을 구현하고 작업이 성공하면이를 교체합니다.

실제로 인터페이스는 구현 숨기기에만 사용할 수 있지만 다음과 같은 단점이 있습니다.

  1. 비가 상 방법을 추가해도 ABI가 중단되지 않지만 가상 방법을 추가해도 문제가 해결됩니다. 따라서 인터페이스는 메소드 추가를 전혀 허용하지 않습니다.

  2. Inteface는 포인터 / 참조를 통해서만 사용할 수 있으므로 사용자는 적절한 자원 관리를 관리해야합니다. 반면 PImpl을 사용하는 클래스는 여전히 값 유형이며 리소스를 내부적으로 처리합니다.

  3. 숨겨진 구현은 상속 할 수 없으며 PImpl이있는 클래스는 상속 할 수 없습니다.

물론 인터페이스는 예외 안전에 도움이되지 않습니다. 이를 위해서는 클래스 내부에 간접적 인 지시가 필요합니다.


좋은 지적. Impl클래스에 public 함수를 추가 해도는 깨지지 ABI않지만 인터페이스에 public 함수를 추가하면 깨 ABI집니다.
ZijingWu

1
퍼블릭 함수 / 메서드를 추가한다고 해서 ABI를 중단 할 필요 는 없습니다 . 엄격하게 추가 변경 사항이 변경 사항을 위반하지 않습니다. 그러나 항상 조심해야합니다. 프론트 엔드와 백엔드 사이의 코드 조정은 버전 관리와 관련된 모든 종류의 재미를 처리해야합니다. 모든 것이 까다로워집니다. (이런 일은 많은 소프트웨어 프로젝트가 C보다 C ++을 선호하는 이유입니다. ABI가 무엇인지 정확하게 파악할 수있게 해줍니다.)
Donal Fellows

3
@DonalFellows : 공용 함수 / 방법을 추가 해도 ABI 중단 되지 않습니다 . 가상 방법을 추가 하면 사용자가 PImpl이 달성 한 객체를 상속받지 못하는 경우가 아니면 항상 그렇게 합니다.
Jan Hudec

4
가상 방법을 추가하면 ABI가 중단되는 이유 를 분명히 알 수 있습니다. 그것은 연계와 관련이 있습니다. 라이브러리가 정적인지 동적인지에 관계없이 비가 상 메소드에 링크하는 것은 이름으로 이루어집니다. 가상이 아닌 다른 방법을 추가하는 것은 연결할 다른 이름을 추가하는 것입니다. 그러나 가상 메서드는 vtable의 인덱스이며 외부 코드는 인덱스별로 효과적으로 연결됩니다. 가상 메소드를 추가하면 외부 코드가 알 수있는 방법없이 vtable에서 다른 메소드를 이동할 수 있습니다.
SnakE

1
PImpl은 또한 초기 컴파일 시간을 줄입니다. 예를 들어 구현에 unordered_setPImpl이없는 일부 템플릿 유형 (예 :) 필드가있는 경우 #include <unordered_set>in을 입력 해야합니다 MyClass.h. PImpl을 사용하면 .cpp파일이 아닌 파일 에 포함시키기 만하면 .h됩니다.
Martin C. Martin

7

난 그냥 당신의 성능 포인트를 해결하고 싶습니다. 인터페이스를 사용하는 경우 컴파일러의 옵티마이 저가 인라인하지 않는 가상 함수를 작성해야합니다. PIMPL 기능은 아마도 너무 짧아서 인라인 될 수 있습니다 (IMPL 기능이 작은 경우 아마도 두 번 이상). 시간이 오래 걸리는 전체 프로그램 분석 최적화를 사용하지 않으면 가상 함수 호출을 최적화 할 수 없습니다.

PIMPL 클래스가 성능 결정적인 방식으로 사용되지 않는 경우이 점은 중요하지 않지만 성능이 동일하다는 가정은 일부 상황에서만 가능합니다.


1
링크 타임 최적화를 사용하면 pimpl 관용구를 사용하는 대부분의 오버 헤드가 제거됩니다. 외부 래퍼 함수와 구현 함수를 모두 인라인 할 수 있으므로 헤더 내부에 구현 된 일반 클래스처럼 함수를 효과적으로 호출 할 수 있습니다.
goji

링크 타임 최적화를 사용하는 경우에만 해당됩니다. PImpl의 요점은 컴파일러에 전달 기능이 아니라 구현이 없다는 것입니다. 링커는 그래요.
Martin C. Martin

5

이 페이지는 귀하의 질문에 답변합니다. 많은 사람들이 당신의 주장에 동의합니다. Pimpl을 사용하면 개인 필드 / 멤버에 비해 다음과 같은 장점이 있습니다.

  1. 클래스의 전용 멤버 변수를 변경하면 클래스에 의존하는 클래스를 다시 컴파일 할 필요가 없으므로 시간이 빨라지고 FragileBinaryInterfaceProblem 이 줄어 듭니다.
  2. 헤더 파일은 전용 멤버 변수에서 '값별'로 사용되는 클래스를 #include 할 필요가 없으므로 컴파일 시간이 더 빠릅니다.

Pimpl 관용구는 컴파일 타임 최적화, 종속성 해제 기술입니다. 큰 헤더 파일을 줄입니다. 헤더를 공용 인터페이스로 줄입니다. 헤더 길이는 작동 방식에 대해 생각할 때 C ++에서 중요합니다. #include는 헤더 파일을 소스 파일에 효과적으로 연결하므로 사전 처리 된 C ++ 단위가 매우 클 수 있다는 점을 명심하십시오. Pimpl을 사용하면 컴파일 시간을 향상시킬 수 있습니다.

디자인을 개선하는 다른 더 나은 종속성 깨기 기술이 있습니다. 개인 메소드는 무엇을 나타 냅니까? 단일 책임은 무엇입니까? 그들은 다른 수업이어야합니까?


PImpl은 런타임 측면에서 전혀 최적화되지 않습니다. 할당은 사소한 것이 아니며 pimpl은 추가 할당을 의미합니다. 의존성을 깨는 기술이며 컴파일 시간 만 최적화 합니다.
Jan Hudec

@JanHudec가 명확 해졌습니다.
Dave Hillier

@DaveHillier, +1 언급 FragileBinaryInterfaceProblem
ZijingWu
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.