2008-09-13 4 views
104

배경 설명 :"PIMPL"관용구를 사용해야하는 이유는 무엇입니까?

PIMPL Idiom (구현에 대한 포인터)가 공용 클래스 공용 클래스의 일부 라이브러리 밖에 볼 수없는 구조 또는 클래스를 감싸고있는 이행 은닉하는 기술이다.

이렇게하면 라이브러리 사용자의 내부 구현 세부 사항과 데이터가 숨겨집니다.

공용 클래스 메소드 구현이 라이브러리로 컴파일되고 사용자가 헤더 파일 만 갖기 때문에이 관용구를 구현할 때 왜 공용 메소드를 pimpl 클래스에 배치하겠습니까?

설명하기 위해이 코드는 Purr() 구현을 impl 클래스에 넣고 랩핑합니다.

Purr을 공용 클래스에서 직접 구현하지 않는 이유는 무엇입니까? impl-> 푸르르 내부의 CPP 파일에 대한 호출을 배치

// header file: 
class Cat { 
    private: 
     class CatImpl; // Not defined here 
     CatImpl *cat_; // Handle 

    public: 
     Cat();   // Constructor 
     ~Cat();   // Destructor 
     // Other operations... 
     Purr(); 
}; 


// CPP file: 
#include "cat.h" 

class Cat::CatImpl { 
    Purr(); 
...  // The actual implementation can be anything 
}; 

Cat::Cat() { 
    cat_ = new CatImpl; 
} 

Cat::~Cat() { 
    delete cat_; 
} 

Cat::Purr(){ cat_->Purr(); } 
CatImpl::Purr(){ 
    printf("purrrrrr"); 
} 
+26

왜냐하면 PIMP 관용구는 피해야하기 때문입니다 ... – mlvljr

+1

우수 답변,이 링크에는 광범위한 정보가 포함되어 있습니다. http://marcmutz.wordpress.com/translated-articles/pimp-my-pimpl/ – zhanxw

+11

If 당신은 호의 이후에 오는 유지 보수 코더를 원한다. 이것이 ** 인터페이스 ** 패턴이라는 것을 기억하라. 거기에있는 모든 내부 클래스에 사용하지 마십시오. 블레이드 러너의 말을 인용하면 사람들이 믿지 않을 것이라고 생각했습니다. – DevSolar

답변

34
  • 때문에 (특히 오랜 시간을 구축 피에) 큰 도움이 될 수 있습니다 Purr()CatImpl의 개인 회원을 사용할 수있게하려고합니다. Cat::Purr()friend 선언이없는 액세스를 허용하지 않습니다.
  • 다음은 책임을 혼합하지 않기 때문에 하나의 클래스가 구현되고 하나의 클래스가 전달합니다.
+4

그래도 유지하는 것이 고통입니다. 그러나 다시 라이브러리 클래스 인 경우 메서드가 많이 변경되지 않아야합니다. 내가보고있는 코드는 안전한 길을 택하고 도처에 핌플을 사용하는 것 같습니다. – JeffV

+0

모든 멤버가 비공개이므로이 줄을 올바르지 않습니다 : cat _-> Purr(); Purr()는 외부에서 액세스 할 수 없으므로 개인 소아의 때문에. 내가 여기서 무엇을 놓치고 있니? – binaryguy

+0

두 지점 모두 의미가 없습니다. 만약 당신이 하나의 클래스 인'Cat'을 가지고 있다면 멤버들에게 접근 할 수있을 것이고, 그것은 "구현"하는 클래스이기 때문에 "리포지토리를 혼합하지"않을 것입니다. PIMPL을 사용하는 이유는 다릅니다. – doc

2

미래에 당신이 헤더 파일을 변경하지 않고 완전히 다른 뭔가를 할 수 있다는 것을 의미한다. 어쩌면 내년에는 그들이 대신 호출 할 수있는 도우미 메서드를 발견하고 직접 호출하고 impl-> Purr를 사용하지 않는 코드를 변경할 수 있습니다. (예, 실제 impl :: Purr 메서드를 업데이트하여 동일한 결과를 얻을 수 있지만이 경우에는 다음 함수를 차례로 호출하는 추가 함수 호출이 필요합니다)

헤더는 단지 정의를 가지고 있으며, 이디엄의 전체적인 점인 더 깔끔한 분리를 구현하는 구현이 없습니다.

50

대부분의 사람들은 이것을 핸들 바디 관용어라고합니다. James Coplien의 Advanced C++ Programming Styles and Idioms (Amazon link)를 참조하십시오. Cheshire Cat로도 알려져 있기 때문에 루이스 캐롤 (Lewis Caroll)의 성격이 희미해질 때까지 희미해진다.

예제 코드는 두 세트의 소스 파일에 걸쳐 배포되어야합니다. 그런 다음 Cat.h만이 제품과 함께 제공되는 파일입니다.

CatImpl.h는 Cat.cpp에 포함되어 있으며 CatImpl.cpp에는 CatImpl :: Purr()에 대한 구현이 포함되어 있습니다. 귀하의 제품을 사용하여 대중에게 공개되지 않습니다.

기본적으로 아이디어는 눈을 엿보는 구현을 최대한 숨기는 것입니다. 이것은 고객의 코드가 컴파일되고 링크되는 API를 통해 액세스되는 일련의 라이브러리로 제공되는 상용 제품을 가지고있는 경우에 가장 유용합니다. 그의 기술은 완전히 객체의 인터페이스에서 구현을 분리시켜 사용하여 다른 사람에 의해 언급 한 바와 같이

우리는 2000

에서 IONAs Orbix 3.3 제품의 재 작성으로 이런 짓을. Purr()의 구현을 변경하려는 경우 Cat을 사용하는 모든 것을 다시 컴파일 할 필요가 없습니다.

이 기술은 design by contract이라는 방법론에서 사용됩니다.

+4

계약시 디자인에서 사용되는 pimpl 관용구 (또는 핸들 바디 숙어라고 부름)는 어떻게됩니까? –

+0

안녕하세요, Andreas, API 사용자 인터페이스는 노출 된 계약 (핸들)에만 있으며 광고 기능을 제공하기 위해 본문을 구현하는 방식이 아닙니다. 광고주가 광고 한 API의 의미를 변경하지 않는 한 자유롭게 구현을 변경할 수 있습니다. –

+0

@Rob, 나는 거기에 거의 오버 헤드가 없다고 생각합니다. 여분의 수업이지만, 그들에게는 거의 없습니다. 기존 클래스를 감싸는 얇은 래퍼. 누군가 내가 틀렸다면 나를 고쳐 주겠지 만, 메모리 사용량은 RAM에있는 여분의 함수 테이블, pimpl에 대한 포인터 및 코드 공간의 각 메서드에 대한 리디렉션 함수 일뿐입니다. 유지 관리 및 디버깅을위한 고통. – JeffV

6

일반적으로 소유자 클래스 (이 경우 Cat)의 헤더에있는 Pimpl 클래스에 대한 유일한 참조는 종속성을 크게 줄일 수 있으므로 여기에서 수행 한 것처럼 전달 선언입니다.

예를 들어, Pimpl 클래스에 ComplicatedClass가 멤버 (포인터 또는 참조)가 아니라면 사용하기 전에 ComplicatedClass를 완전히 정의해야합니다. 실제로 이것은 "ComplicatedClass.h"를 포함한다는 것을 의미합니다 (이는 또한 ComplicatedClass가 의존하는 것을 간접적으로 포함합니다). 이것은 많은 헤더를 채우는 하나의 헤더 채움으로 이어질 수 있습니다. 이것은 당신의 의존성 (그리고 컴파일 시간)을 관리하는데 좋지 않습니다.

pimpl idion을 사용할 때 # 소유자 유형의 공개 인터페이스에 사용 된 항목 만 # 포함해야합니다 (Cat입니다). 여러분의 라이브러리를 사용하는 사람들에게 더 좋은 점이 있습니다. 즉, 실수로 또는 허용하지 않는 작업을 수행하기 위해 라이브러리의 내부 부분에 따라 사람들에 대해 걱정할 필요가 없습니다. #define 파일을 포함하기 전에 비공개로

는 간단한 클래스의 경우 Pimpl를 사용하는 이유는 일반적으로 없다,하지만 종류가 매우 큰 경우 회, 그것은

14

클래스에서 pimpl 관용구를 사용하는 경우 공용 클래스에서 헤더 파일을 변경하지 않아도됩니다.

이렇게하면 외부 클래스의 헤더 파일을 수정하지 않고도 pimpl 클래스에 메서드를 추가/제거 할 수 있습니다. #includes도 여드름에 추가/제거 할 수 있습니다. 당신은 외부 클래스의 헤더 파일을 변경하면

, 당신은 #include를 모두 다시 컴파일해야 함 (그 중 하나가 헤더 파일 인 경우에, 당신은 등을 #include를 다하고 다시 컴파일해야)

0

:이 언급하지만 차이가 가치가 있는지 나는

자신의 이름 공간에 구현이 사용자에게 표시되는 코드에 대한 공공 래퍼/라이브러리 네임 스페이스를 가질 수 있을까 모르겠어 ...
catlib::Cat::Purr(){ cat_->Purr(); } 
cat::Cat::Purr(){ 
    printf("purrrrrr"); 
} 

이렇게하면 모든 lib rary 코드는 cat 네임 스페이스를 사용할 수 있으며 클래스를 사용자에게 노출해야하므로 catlib 네임 스페이스에 래퍼가 만들어 질 수 있습니다.

0

나는 pimpl 관용구가 얼마나 잘 알려져 있음에도 불구하고 실생활 (예 : 오픈 소스 프로젝트)에서 자주 볼 수 없다는 것을 알게되었습니다.

"이점"이 과도하게 강조되는지 궁금합니다. 예, 구현 세부 정보 중 일부를 더 숨길 수 있습니다. 예, 헤더를 변경하지 않고 구현을 변경할 수 있지만 실제로는 큰 장점이라고 할 수 없습니다.

즉, 이 구현되어야 할 필요가 있다는 것은 확실하지 않습니다. 즉,은 잘 숨겨져 있으며 사람들이 실제로 구현 만 변경하는 경우는 거의 없습니다. 어쨌든 헤더를 변경해야하는 등 새로운 메소드를 추가해야하는 경우

+1

예, 좋은 공용 인터페이스를 고수 할 수만 있다면 차이가 있습니다. Rob Wells가 언급했듯이, 컴파일 된 버전의 라이브러리에 링크 된 사람들에게 업데이트 된 라이브러리를 배포하여 재 컴파일하지 않고 배포해야하는 경우 중요합니다. 새 DLL과 voila 만 제공하면됩니다. 인터페이스 (= C++에서 데이터 멤버가없는 추상 클래스)를 사용하면 관리 방법을 줄이고 (각 공용 메서드를 수동으로 전달하지 않아도) 똑같은 작업을 수행 할 수 있습니다. 하지만 OTOH에서는 인스턴스 생성 (즉, 팩토리 메소드 호출)에 특수 구문을 사용해야합니다. –

+4

Qt는 PIMPL 관용어를 광범위하게 사용합니다. –

1

지난 며칠 동안 처음으로 pimpl 클래스를 구현했습니다. 나는 그것을 Borland Builder에서 winsock2.h를 포함하는 문제를 제거하기 위해 사용했습니다. 구조체 정렬을 망쳐 놓은 것처럼 보였고 클래스 개인 데이터에 소켓 항목이 있었기 때문에 헤더를 포함하는 모든 cpp 파일에 해당 문제가 확산되었습니다.

pimpl을 사용하면 winsock2.h가 단 하나의 cpp 파일에 포함되어 문제를 해결할 수 있고 다시 물지 않을까 걱정하지 않아도됩니다.

원래의 질문에 대답하기 위해 pimpl 클래스에 호출을 전달할 때 발견 한 이점은 pimpl 클래스가 pimpl'd가되기 전의 원래 클래스와 같았고 구현에 ' 이상한 방식으로 2 개 학급 이상으로 퍼졌습니다. 단순히 pimpl 클래스로 전달하기 위해 퍼블릭을 구현하는 것이 훨씬 명확합니다.

Mr Nodet이 말한 것처럼, 하나의 클래스, 책임 하나.

3

글쎄, 나는 그것을 사용하지 않을 것이다. 나는 더 나은 대안이 :

foo.h :

class Foo { 
public: 
    virtual ~Foo() { } 
    virtual void someMethod() = 0; 

    // This "replaces" the constructor 
    static Foo *create(); 
} 

foo.cpp의 :

namespace { 
    class FooImpl: virtual public Foo { 

    public: 
     void someMethod() { 
      //.... 
     }  
    }; 
} 

Foo *Foo::create() { 
    return new FooImpl; 
} 

이 패턴은 이름이 있습니까?

파이썬과 자바 프로그래머로서, 필자는 pImpl 관용구보다 더 많은 것을 좋아합니다.

+2

이미 개체 생성에 대한 공장 접근 방식으로 자신을 제한하고 있다면 괜찮습니다. 그러나 기존의 pImpl은 두 가지 방법 모두에서 작동하지만 값 의미를 완전히 제거합니다. –

+2

잘 pImpl은 기본적으로 포인터를 감싸기 만합니다. 위의 create()를 PointerWrapperWithCopySemantics :-)로 반환하면됩니다. 나는 대개 반대하고 std :: auto_ptr 을 반환합니다. –

+10

왜 상속 구성인가? vtable의 오버 헤드를 추가하는 이유는 무엇입니까? 왜 가상 상속인가? – derpface

17

무엇이 가치있는 지, 인터페이스와 구현을 구분합니다. 이것은 일반적으로 작은 규모의 프로젝트에서는 그리 중요하지 않습니다. 그러나 대규모 프로젝트 및 라이브러리에서는 빌드 시간을 크게 줄이는 데 사용할 수 있습니다.

Cat의 구현에는 많은 헤더가 포함될 수 있으며 자체적으로 컴파일하는 데 시간이 걸리는 템플릿 메타 프로그래밍이 필요할 수 있습니다. Cat을 사용하려는 사용자는 왜 모든 것을 포함해야합니까? 따라서 필요한 모든 파일은 pimpl 관용구 (forward 선언은 CatImpl)를 사용하여 숨겨지며 인터페이스를 사용하더라도 사용자가이를 포함시키지 않습니다.

저는 비선형 최적화를위한 라이브러리를 개발하고 있습니다 ("lots of nasty math"를 읽습니다). 이것은 템플릿에 구현되어있어서 대부분의 코드가 헤더에 있습니다. 알맞은 멀티 코어 CPU에서 컴파일하는 데 약 5 분이 걸리고, 그렇지 않은 경우 .cpp의 헤더를 파싱하는 데 약 1 분이 걸립니다. 따라서 라이브러리를 사용하는 사람은 코드를 컴파일 할 때마다 2 분 정도 기다려야합니다. 따라서 개발은 꽤 tedious입니다. 그러나 구현과 헤더를 숨김으로써 즉시 컴파일되는 간단한 인터페이스 파일을 포함합니다.

구현의 보호를 다른 회사에서 보호하는 것과 반드시 ​​관련이있는 것은 아닙니다. 알고리즘의 내부 동작을 멤버 변수 정의에서 추측 할 수있는 경우가 아니면 그래서, 그것은 아마도 매우 복잡하지 않고 처음부터 보호 할만한 가치가 없습니다).

1

우리는 멤버 함수 실행 전후에 사전, 사후 및 오류 측면이 호출되는 aspect 지향 프로그래밍을 에뮬레이션하기 위해 PIMPL 관용구를 사용합니다.

struct Omg{ 
    void purr(){ cout<< "purr\n"; } 
}; 

struct Lol{ 
    Omg* omg; 
    /*...*/ 
    void purr(){ try{ pre(); omg-> purr(); post(); }catch(...){ error(); } } 
}; 

또한 기본 클래스에 대한 포인터를 사용하여 여러 클래스 간의 서로 다른 측면을 공유합니다.

이 접근법의 단점은 라이브러리 사용자가 실행될 모든 측면을 고려해야하지만 자신의 클래스 만보아야한다는 것입니다. 모든 부작용에 대한 문서를 찾아야합니다.

관련 문제