pImpl 관용구가 실제로 실제로 사용됩니까?


165

Herb Sutter의 "Exceptional C ++"책을 읽고 있는데,이 책에서 pImpl 관용구에 대해 배웠습니다. 기본적으로 아이디어는 private객체의 구조를 만들고 class동적으로 할당 하여 컴파일 시간단축하고 더 나은 방식으로 개인 구현을 숨기는 것입니다.

예를 들면 다음과 같습니다.

class X
{
private:
  C c;
  D d;  
} ;

다음과 같이 변경할 수 있습니다.

class X
{
private:
  struct XImpl;
  XImpl* pImpl;       
};

그리고 CPP에서 정의는 다음과 같습니다.

struct X::XImpl
{
  C c;
  D d;
};

이것은 꽤 흥미로운 것처럼 보이지만, 내가 일한 회사 나 소스 코드를 본 오픈 소스 프로젝트에서는 이전에 이런 종류의 접근 방식을 본 적이 없습니다. 그래서이 기술이 실제로 실제로 사용되는 것이 궁금합니다.

어디서나주의해서 사용해야합니까? 그리고이 기술을 임베디드 시스템 (성능이 매우 중요한 곳)에서 사용하는 것이 좋습니다?


이것은 X가 (추상적 인) 인터페이스이고 Ximpl이 구현이라는 결정과 본질적으로 동일합니까? struct XImpl : public X. 그것은 더 자연스러운 느낌입니다. 내가 놓친 다른 문제가 있습니까?
Aaron McDaid

@AaronMcDaid : 비슷하지만 (a) 멤버 함수가 가상 일 필요는 없으며 (b) 인스턴스화하기 위해 팩토리 또는 구현 클래스 정의가 필요하지 않다는 장점이 있습니다.
Mike Seymour

2
@AaronMcDaid pimpl 관용구는 가상 함수 호출을 피합니다. 또한 약간 더 C ++-ish입니다 (C ++-ish 개념). 팩토리 함수가 아닌 생성자를 호출합니다. 필자는 기존 코드베이스의 내용에 따라 두 가지를 모두 사용했습니다. 원래는 Pimpl 관용구 (원래 Cheshire cat 관용구라고하며 Herb의 설명을 5 년 이상 미달)는 더 긴 역사를 가진 것으로 보입니다. C ++에서 널리 사용되지만 그렇지 않으면 두 가지 모두 작동합니다.
James Kanze

30
C ++에서는 pimpl이 const unique_ptr<XImpl>아닌 으로 구현되어야합니다 XImpl*.
Neil G

1
"이전에 이런 종류의 접근법을 본 적이 없었습니다. Qt는 거의 사용하지 않습니다.
ManuelSchneid3r

답변:


132

그래서이 기술이 실제로 실제로 사용되는 것이 궁금합니다. 어디서나주의해서 사용해야합니까?

물론 사용됩니다. 나는 거의 모든 수업에서 내 프로젝트에서 사용합니다.


PIMPL 관용구를 사용하는 이유 :

이진 호환성

라이브러리를 개발할 때 XImpl클라이언트와 바이너리 호환성 을 유지 하지 않고도 필드를 추가 / 수정할 수 있습니다 (충돌을 의미합니다). X클래스에 새 필드를 추가 할 때 클래스 의 이진 레이아웃이 변경되지 않으므로 Ximpl부 버전 업데이트에서 라이브러리에 새 기능을 추가하는 것이 안전합니다.

물론, 당신은 또한 새로운 공개 / 개인 가상이 아닌 방법을 추가 할 수 있습니다 X/ XImpl이진 호환성을 깨지 않고 있지만, 표준 헤더 / 구현 기술로 파에 그의.

데이터 숨기기

라이브러리, 특히 독점 라이브러리를 개발하는 경우 라이브러리의 공용 인터페이스를 구현하는 데 사용 된 다른 라이브러리 / 구현 기술을 공개하지 않는 것이 좋습니다. 지적 재산권 문제로 인해 또는 사용자가 구현에 대해 위험한 가정을 취하려고 유혹하거나 끔찍한 캐스팅 트릭을 사용하여 캡슐화를 깨뜨릴 수 있다고 생각하기 때문입니다. PIMPL은이를 해결 / 완화합니다.

컴파일 시간

X필드 및 / 또는 메소드를 XImpl클래스에 추가 / 제거 할 때 소스 파일 (구현) 파일 만 다시 빌드하면 되므로 컴파일 시간이 단축됩니다 (표준 기술에서 개인 필드 / 메소드 추가에 맵핑 됨). 실제로는 일반적인 작업입니다.

표준 헤더 / 구현 기술 (PIMPL 제외)을 사용하여에 새 필드를 추가하면 X할당 X크기를 조정해야하므로 스택 (또는 스택 또는 힙)에 할당 한 모든 클라이언트를 다시 컴파일해야합니다. X 할당하지 않은 모든 클라이언트 다시 컴파일해야하지만 오버 헤드 일뿐입니다 (클라이언트 측의 결과 코드는 동일합니다).

또한 캡슐화 이유로 인해이 메소드를 호출 할 수는 없지만 XClient1.cpp개인용 메소드 X::foo()를 추가 X하고 X.h변경 한 경우에도 표준 헤더 / 구현 분리 를 다시 컴파일해야합니다 XClient1.cpp! 위와 같이 순수한 오버 헤드이며 실제 C ++ 빌드 시스템 작동 방식과 관련이 있습니다.

물론, 메소드의 구현을 수정하는 경우 (헤더를 건드리지 않기 때문에) 재 컴파일은 필요하지 않지만 표준 헤더 / 구현 기술과 동등합니다.


이 기술을 임베디드 시스템 (성능이 매우 중요한 곳)에서 사용하도록 권장합니까?

그것은 당신의 목표가 얼마나 강력한 지에 달려 있습니다. 그러나이 질문에 대한 유일한 답은 얻는 것과 잃는 것을 측정하고 평가하는 것입니다. 또한 클라이언트가 임베디드 시스템에서 사용할 라이브러리를 게시하지 않으면 컴파일 시간 이점 만 적용된다는 점을 고려하십시오!


16
+1도 같은 이유로 회사에서 널리 사용되기 때문에 +1입니다.
Benoit

9
또한 이진 호환성
Ambroz Bizjak

9
Qt 라이브러리에서이 방법은 스마트 포인터 상황에서도 사용됩니다. 따라서 QString은 내용을 내부적으로 변경할 수없는 클래스로 유지합니다. 공개 클래스가 "복사"되면 전체 개인 클래스 대신 개인 멤버의 포인터가 복사됩니다. 이러한 개인 클래스는 또한 스마트 포인터를 사용하므로 기본적으로 전체 클래스 복사 대신 포인터 복사로 인해 성능이 크게 향상 될뿐만 아니라 대부분의 클래스에서 가비지 수집을 수행합니다.
Timothy Baldridge

8
또한, pimpl 관용구를 사용하여 Qt는 단일 주요 버전 (대부분의 경우) 내에서 순방향 및 역방향 이진 호환성을 유지할 수 있습니다. IMO는 이것이 가장 중요한 이유입니다.
whitequark

1
동일한 API를 유지할 수 있으므로 플랫폼 별 코드를 구현하는 데에도 유용합니다.
doc

49

많은 라이브러리가 API에서 안정적으로 유지하기 위해 적어도 일부 버전에서는 사용하는 것으로 보입니다.

그러나 모든 것에 관해서는주의를 기울이지 않고 어디서나 아무것도 사용해서는 안됩니다. 항상 사용하기 전에 생각하십시오. 그것이 당신에게주는 이점과 그것이 지불 할만한 가치가 있는지 평가하십시오.

이 장점 당신에게은 다음과 같습니다 :

  • 공유 라이브러리의 이진 호환성 유지에 도움
  • 특정 내부 세부 사항 숨기기
  • 재 컴파일주기 감소

그것들은 당신에게 진정한 이점 일 수도 있고 아닐 수도 있습니다. 나처럼, 나는 몇 분의 재 컴파일 시간을 신경 쓰지 않는다. 최종 사용자는 항상 처음부터 한 번만 컴파일하므로 일반적으로 그렇지 않습니다.

가능한 단점은 다음과 같습니다 (또한 구현 및 실제 단점인지에 따라 여기에 있음).

  • 순진한 변형보다 더 많은 할당으로 인해 메모리 사용량이 증가합니다.
  • 유지 보수 노력 증가 (적어도 전달 기능을 작성해야 함)
  • 성능 손실 (컴파일러는 클래스의 순진한 구현과 같이 내용을 인라인하지 못할 수 있음)

따라서 모든 것을 신중하게 가치를 부여하고 스스로 평가하십시오. 나에게 거의 항상 pimpl 관용구를 사용하는 것이 노력할 가치가 없다는 것이 밝혀졌습니다. 개인적으로 사용하는 경우는 하나뿐입니다 (또는 적어도 비슷한 것).

리눅스 stat호출을 위한 내 C ++ 래퍼 . 여기서 #defines설정 한 내용에 따라 C 헤더의 구조체가 다를 수 있습니다 . 그리고 내 래퍼 헤더 이후에만 모두를 제어 할 수 없습니다 #include <sys/stat.h>내에서 .cxx파일과 이러한 문제를 피할 수 있습니다.


2
인터페이스 코드 시스템을 독립적으로 만들려면 시스템 인터페이스에 거의 항상 사용되어야합니다. 예를 들어, Unix에서 반환되는 File많은 정보를 공개하는 내 클래스 stat는 Windows와 Unix에서 동일한 인터페이스를 사용합니다.
James Kanze

5
@JamesKanze : 저도 개인적으로 잠시 앉아 처음으로 #ifdef래퍼를 가능한 한 얇게 만드는 것이 충분하지 않은지 생각 합니다. 그러나 모든 사람은 다른 목표를 가지고 있습니다. 중요한 것은 맹목적으로 무언가를 따르는 대신 시간을내어 생각하는 것입니다.
PlasmaHH

31

상품에 대해 다른 모든 사람들과 동의하지만 한계를 두자 : 템플릿과 잘 작동하지 않습니다 .

그 이유는 템플릿 인스턴스화에는 인스턴스화가 발생한 곳에서 사용 가능한 전체 선언이 필요하기 때문입니다. (그리고 이것이 CPP 파일에 템플릿 메소드가 정의되어 있지 않은 주된 이유입니다)

여전히 템플 화 된 서브 클래스를 참조 할 수 있지만, 서브 클래스를 모두 포함해야하기 때문에 컴파일시 "구현 디커플링"의 모든 장점 (모든 플랫폼에 특정한 플랫폼 코드를 포함하지 않고 컴파일 시간 단축)을 잃게됩니다.

일반 OOP (상속 기반)에는 좋은 패러다임이지만 일반 프로그래밍 (전문화 기반)에는 적합하지 않습니다.


4
PIMPL 클래스를 템플릿 유형 인수로 사용할 때는 문제가 없습니다 . 구현 클래스 자체가 외부 클래스의 템플리트 인수에서 매개 변수화되어야하는 경우에만 여전히 개인 클래스 인 경우에도 인터페이스 헤더에서 숨길 수 없습니다. 템플릿 인수를 삭제할 수 있으면 여전히 "적절한"PIMPL을 수행 할 수 있습니다. 형식을 삭제하면 기본 비 템플릿 클래스에서 PIMPL을 수행 한 다음 템플릿 클래스에서 파생시킬 수 있습니다.
Reinstate Monica

22

다른 사람들은 이미 기술적 인 단점을 제공했지만 다음과 같은 점은 주목할 가치가 있다고 생각합니다.

무엇보다도 독단적이지 마십시오. pImpl이 상황에 맞는 경우 사용하십시오. " 실제로 구현을 숨기 므로 OO가 더 좋습니다 "등으로 사용하지 마십시오 . C ++ FAQ 인용 :

캡슐화는 사람이 아닌 코드를위한 것입니다. ( source )

OpenSceneGraph가 사용하는 스레딩 라이브러리 인 OpenThreads가 사용되는 오픈 소스 소프트웨어의 예와 그 이유를 설명합니다 . <Thread.h>내부 상태 변수 (예 : 스레드 핸들)가 플랫폼마다 다르기 때문에 모든 플랫폼 별 코드를 헤더에서 제거하는 것이 좋습니다 . 이렇게하면 모든 것이 숨겨져 있기 때문에 다른 플랫폼의 특성에 대한 지식없이 라이브러리에 대해 코드를 컴파일 할 수 있습니다.


12

주로 다른 모듈에서 API로 사용되도록 노출 된 클래스에 대한 PIMPL을 고려합니다. PIMPL 구현에서 변경 한 내용을 다시 컴파일해도 나머지 프로젝트에는 영향을 미치지 않으므로 많은 이점이 있습니다. 또한 API 클래스의 경우 이진 호환성을 향상시킵니다 (모듈 구현의 변경 사항은 해당 모듈의 클라이언트에 영향을 미치지 않으므로 새 구현에 동일한 이진 인터페이스 (PIMPL에 의해 노출 된 인터페이스)이 있으므로 다시 컴파일 할 필요가 없습니다).

모든 클래스에 PIMPL을 사용하는 경우 모든 이점이 비용 때문에 발생하므로주의를 고려할 것입니다. 구현 방법에 액세스하려면 추가 수준의 간접 성이 필요합니다.


"구현 방법에 접근하기 위해서는 추가적인 수준의 간접적 인 지시가 필요하다." 그것은?
xaxxon

@xaxxon 그렇습니다. 방법이 낮은 수준이면 pimpl이 느려집니다. 예를 들어 꽉 조이는 물건에는 절대 사용하지 마십시오.
Erik Aronesty

@xaxxon 나는 일반적으로 여분의 레벨이 필요하다고 말합니다. 인라인이 no보다 수행 된 경우. 그러나 inlinning은 다른 dll로 컴파일 된 코드의 옵션이 아닙니다.
Ghita

5

이것이 디커플링을위한 가장 기본적인 도구 중 하나라고 생각합니다.

임베디드 프로젝트 (SetTopBox)에서 pimpl (및 탁월한 C ++의 다른 많은 관용구)을 사용하고있었습니다.

프로젝트에서이 idoim의 특별한 목적은 XImpl 클래스가 사용하는 유형을 숨기는 것이 었습니다. 구체적으로 우리는이를 사용하여 서로 다른 헤더를 가져올 다른 하드웨어에 대한 구현 세부 정보를 숨겼습니다. 플랫폼마다 다른 XImpl 클래스 구현과 다른 플랫폼마다 다른 구현이있었습니다. 클래스 X의 레이아웃은 플랫폼과 상관없이 동일하게 유지되었습니다.


4

예전에는이 기술을 많이 사용했지만 그로부터 멀어졌습니다.

물론 클래스 사용자에게 구현 세부 사항을 숨기는 것이 좋습니다. 그러나 클래스 사용자가 추상 인터페이스를 사용하고 구현 세부 사항을 구체적인 클래스로 만들면 가능합니다.

pImpl의 장점은 다음과 같습니다.

  1. 이 인터페이스의 구현이 하나만 있다고 가정하면 추상 클래스 / 구체적 구현을 ​​사용하지 않는 것이 더 명확합니다.

  2. 여러 클래스가 동일한 "impl"에 액세스하지만 모듈 사용자는 "노출 된"클래스 만 사용하도록 클래스 스위트 (모듈)가있는 경우.

  3. 이것이 나쁜 것으로 간주되면 v- 테이블이 없습니다.

pImpl의 단점 (추상 인터페이스가 더 나은 곳)

  1. "인터페이스"구현이 하나만있을 수 있지만 추상 인터페이스를 사용하여 단위 테스트에서 작동하는 "모의"구현을 만들 수도 있습니다.

  2. (가장 큰 문제). unique_ptr 및 이동 이전에는 pImpl을 저장하는 방법에 대한 선택이 제한되었습니다. 생생한 포인터와 수업에 복사 할 수없는 문제가있었습니다. 오래된 auto_ptr은 앞으로 선언 된 클래스에서 작동하지 않습니다 (어쨌든 모든 컴파일러에서). 그래서 사람들은 shared_ptr을 사용하기 시작했습니다.이 클래스는 복사 가능하지만 클래스는 모두 기본적으로 shared_ptr이 같았습니다 (수정 및 수정). 따라서 솔루션은 종종 내부 포인터에 원시 포인터를 사용하고 클래스를 복사 할 수 없도록 만들고 대신 shared_ptr을 반환합니다. 새로운 것에 대한 두 번의 호출. (실제로 shared_ptr이 주어지면 실제로 3은 두 번째 것을 주었다).

  3. constness가 멤버 포인터로 전파되지 않으므로 기술적으로 실제로 const-correct가 아닙니다.

일반적으로 필자는 몇 년 동안 pImpl에서 벗어나 추상 인터페이스 사용 (및 인스턴스를 작성하는 팩토리 메소드)으로 이동했습니다.


3

다른 많은 사람들이 말했듯이, Pimpl 관용구는 불행히도 성능 손실 (추가 포인터 간접) 및 추가 메모리 요구 (구성원 포인터 자체) 비용으로 완전한 정보 숨기기 및 컴파일 독립성에 도달 할 수 있습니다. 임베디드 소프트웨어 개발, 특히 메모리를 최대한 절약해야하는 시나리오에서 추가 비용이 중요 할 수 있습니다. C ++ 추상 클래스를 인터페이스로 사용하면 동일한 비용으로 동일한 이점을 얻을 수 있습니다. 이것은 실제로 C와 같은 인터페이스 (불투명 한 포인터를 매개 변수로 사용하는 전역 메서드)에 반복하지 않고 추가 리소스 단점없이 진정한 정보 숨기기 및 컴파일 독립성을 가질 수없는 C ++의 큰 결함을 보여줍니다. 사용자가 반드시 포함해야하는 클래스 선언


3

이 관용구가 큰 도움이 된 실제 시나리오는 다음과 같습니다. 최근에 게임 엔진에서 DirectX 11과 기존 DirectX 9 지원을 지원하기로 결정했습니다. 엔진은 이미 대부분의 DX 기능을 래핑 했으므로 DX 인터페이스는 직접 사용되지 않았습니다. 그들은 헤더에 개인 멤버로 정의되었습니다. 엔진은 DLL을 확장으로 사용하여 키보드, 마우스, 조이스틱 및 스크립팅 지원을 다른 확장만큼 주 단위로 사용합니다. 대부분의 DLL은 DX를 직접 사용하지 않았지만 DX를 노출시키는 헤더를 가져 오기 때문에 DX에 대한 지식과 연결이 필요했습니다. DX 11을 추가 할 때이 복잡성은 불필요하게 급격히 증가했습니다. DX 멤버를 소스에서만 정의 된 Pimpl로 이동하면 이러한 부과가 사라졌습니다. 이러한 라이브러리 종속성 감소 외에도


2

실제로 많은 프로젝트에서 사용됩니다. 유용성은 프로젝트의 종류에 따라 크게 다릅니다. 이것을 사용하는 더 눈에 띄는 프로젝트 중 하나는 Qt 이며, 기본 아이디어는 구현 또는 플랫폼 특정 코드를 사용자 (Qt를 사용하는 다른 개발자)로부터 숨기는 것입니다.

이것은 고귀한 아이디어이지만 이것에 대한 실질적인 단점이 있습니다. 그가 구현 소스 코드를 가지고 있더라도 숨겨진 구현에 대한 바보 같은 포인터이기 때문입니다.

거의 모든 디자인 결정에서와 같이 장단점이 있습니다.


9
그것은 바보이지만 입력되었습니다 ... 왜 디버거의 코드를 따라갈 수 없습니까?
UncleZeiv

2
일반적으로 Qt 코드로 디버그하려면 Qt를 직접 빌드해야합니다. 일단 수행하면 PIMPL 방법으로 들어가서 PIMPL 데이터의 내용을 검사하는 데 아무런 문제가 없습니다.
Reinstate Monica

0

내가 볼 수있는 한 가지 이점은 프로그래머가 특정 작업을 상당히 빠른 방식으로 구현할 수 있다는 것입니다.

X( X && move_semantics_are_cool ) : pImpl(NULL) {
    this->swap(move_semantics_are_cool);
}
X& swap( X& rhs ) {
    std::swap( pImpl, rhs.pImpl );
    return *this;
}
X& operator=( X && move_semantics_are_cool ) {
    return this->swap(move_semantics_are_cool);
}
X& operator=( const X& rhs ) {
    X temporary_copy(rhs);
    return this->swap(temporary_copy);
}

추신 : 나는 움직임 의미를 오해하지 않기를 바랍니다.

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