C ++의 안전한 인터페이스 패턴은 무엇입니까


22

참고 : 다음은 C ++ 03 코드이지만 다음 2 년 안에 C ++ 11로 이동할 것으로 예상되므로이를 명심해야합니다.

C ++에서 추상 인터페이스를 작성하는 방법에 대한 지침을 작성 중입니다 (초보자를 위해). 나는 주제에 관한 Sutter의 두 기사를 읽고 인터넷에서 예제와 답변을 검색했으며 몇 가지 테스트를 수행했습니다.

이 코드는 컴파일해서는 안됩니다!

void foo(SomeInterface & a, SomeInterface & b)
{
   SomeInterface c ;               // must not be default-constructible
   SomeInterface d(a);             // must not be copy-constructible
   a = b ;                         // must not be assignable
}

위의 모든 동작은 슬라이싱 에서 문제의 원인을 찾습니다 . 추상화 된 인터페이스 (또는 계층의 비 리프 클래스)는 파생 클래스가 가능하더라도 구성 가능하거나 복사 가능 / 할당 가능하지 않아야합니다.

0 번째 해결책 : 기본 인터페이스

class VirtuallyDestructible
{
   public :
      virtual ~VirtuallyDestructible() {}
} ;

이 솔루션은 평범하고 다소 순진합니다. 모든 제약 조건을 충족시키지 못합니다. 기본 구성, 복사 구성 및 복사 할당이 가능합니다 (이동 생성자와 할당에 대해서는 확실하지 않지만 여전히 2 년이 걸렸습니다. 밖으로).

  1. 소멸자 순수 가상을 인라인으로 유지해야하므로 선언 할 수 없으며 일부 컴파일러는 순수 가상 메소드를 인라인 빈 본문으로 요약하지 않습니다.
  2. 예,이 클래스의 유일한 요점은 구현자를 가상으로 파괴하는 것입니다. 드문 경우입니다.
  3. 추가적인 가상 순수 메소드 (대부분의 경우)가 있어도이 클래스는 여전히 복사 가능합니다.

그래서 안돼...

첫 번째 해결책 : boost :: noncopyable

class VirtuallyDestructible : boost::noncopyable
{
   public :
      virtual ~VirtuallyDestructible() {}
} ;

이 솔루션은 명확하고 명확하며 C ++ (매크로 없음)이므로 최고입니다.

문제는 VirtuallyConstructible을 여전히 기본 구성 할 수 있기 때문에 특정 인터페이스에서 여전히 작동하지 않는다는 것입니다 .

  1. 우리는 소멸자를 순수 가상으로 선언 할 수 없습니다. 인라인을 유지해야하기 때문에 일부 컴파일러는 소화하지 않습니다.
  2. 예,이 클래스의 유일한 요점은 구현자를 가상으로 파괴하는 것입니다. 드문 경우입니다.

또 다른 문제는 복사 할 수없는 인터페이스를 구현하는 클래스가 복사 생성자와 할당 연산자에 메소드가 필요한 경우 명시 적으로 선언 / 정의해야한다는 것입니다 (코드에는 클라이언트가 여전히 액세스 할 수있는 값 클래스가 있습니다) 인터페이스).

이것은 우리가 가고 싶어하는 0의 규칙에 위배됩니다. 기본 구현이 정상이라면, 우리는 그것을 사용할 수 있어야합니다.

두 번째 해결책 : 보호하십시오!

class MyInterface
{
   public :
      virtual ~MyInterface() {}

   protected :
      // With C++11, these methods would be "= default"
      MyInterface() {}
      MyInterface(const MyInterface & ) {}
      MyInterface & operator = (const MyInterface & ) { return *this ; }
} ;

이 패턴은 우리가 가지고있는 기술적 제약을 따릅니다 (최소한 사용자 코드에서는).

또한 클래스 구현에 인위적인 제약을 부과 하지 않습니다.이 클래스 는 Rule of Zero를 자유롭게 따르거나 C ++ 11/14에서 문제없이 몇 가지 생성자 / 연산자를 "= 기본값"으로 선언 할 수도 있습니다.

자, 이것은 매우 장황하며 대안은 다음과 같은 매크로를 사용하는 것입니다.

class MyInterface
{
   public :
      virtual ~MyInterface() {}

   protected :
      DECLARE_AS_NON_SLICEABLE(MyInterface) ;
} ;

보호 범위는 없기 때문에 매크로는 매크로 외부에 있어야합니다.

올바르게 "네임 스페이스"(즉, 회사 또는 제품 이름이 접두사로 사용됨) 매크로는 무해해야합니다.

또한 모든 인터페이스에 복사 붙여 넣기 대신 코드가 한 소스에 포함되어 있다는 장점이 있습니다. 이동 생성자와 이동 할당이 미래에 같은 방식으로 명시 적으로 비활성화되어 있으면 코드가 약간 변경 될 수 있습니다.

결론

  • 인터페이스의 슬라이싱으로부터 코드를 보호하기 위해 편집증이 필요합니까? (나는 내가 아니라고 생각하지만, 아무도 모른다 ...)
  • 위의 방법 중 가장 좋은 해결책은 무엇입니까?
  • 또 다른 더 나은 해결책이 있습니까?

이는 초보자를위한 지침으로 사용되는 패턴이므로 "각 사례마다 구현해야합니다"와 같은 솔루션은 실행 가능한 솔루션이 아닙니다.

바운티 및 결과

나는 질문에 대답하는 데 소요 된 시간과 답변의 관련성 때문에 코어 덤프에 현상금을 수여했습니다 .

문제에 대한 나의 해결책은 아마도 다음과 같이 될 것입니다 :

class MyInterface
{
   DECLARE_CLASS_AS_INTERFACE(MyInterface) ;

   public :
      // the virtual methods
} ;

... 다음 매크로를 사용하여 :

#define DECLARE_CLASS_AS_INTERFACE(ClassName)                                \
   public :                                                                  \
      virtual ~ClassName() {}                                                \
   protected :                                                               \
      ClassName() {}                                                         \
      ClassName(const ClassName & ) {}                                       \
      ClassName & operator = (const ClassName & ) { return *this ; }         \
   private :

이것은 다음과 같은 이유로 내 문제에 대한 실용적인 솔루션입니다.

  • 이 클래스는 인스턴스화 할 수 없습니다 (생성자가 보호됨)
  • 이 클래스는 사실상 파괴 될 수 있습니다
  • 이 클래스는 상속 클래스에 과도한 제약을 가하지 않고 상속 될 수 있습니다 (예 : 상속 클래스는 기본적으로 복사 가능)
  • 매크로를 사용한다는 것은 인터페이스 "선언"이 쉽게 인식 가능하고 검색 가능하다는 것을 의미하며, 코드를 수정하기 쉽게 한 곳에서 고려합니다 (적절한 접두어 이름은 바람직하지 않은 이름 충돌을 제거합니다)

다른 답변은 귀중한 통찰력을 제공했습니다. 기회를 주신 모든 분들께 감사드립니다.

나는이 질문에 여전히 다른 현상금을 넣을 수 있다고 생각합니다. 그리고 나는 그것을 볼 수있을만큼 충분히 깨달음 답변을 가치있게 생각합니다. 나는 그 대답에 할당하기 위해 현상금을 열 것입니다.


5
단순히 인터페이스에서 순수한 가상 기능을 사용할 수 없습니까? virtual void bar() = 0;예를 들어? 인터페이스가 제대로 표시되지 않습니다.
Morwenn

@ Morwenn : 질문에서 말했듯이 99 %의 사례를 해결할 것입니다 (가능한 경우 100 %를 목표로합니다). 누락 된 1 %를 무시하더라도 할당 슬라이싱은 해결되지 않습니다. 그래서, 이것은 좋은 해결책이 아닙니다.
paercebal

@ Morwenn : 심각하게? ... :-D ... 나는 먼저 StackOverflow 에이 질문을 썼고 제출하기 직전에 내 마음을 바꿨습니다. 여기에서 삭제하고 SO에 제출해야한다고 생각하십니까?
paercebal

내가 옳다면 virtual ~VirtuallyDestructible() = 0인터페이스 클래스의 가상 상속과 추상 멤버 만 사용하면됩니다. VirtuallyDestructible을 생략 할 수 있습니다.
Dieter Lücking

5
@paercebal : 컴파일러가 순수한 가상 클래스를 질식 시키면 휴지통에 속합니다. 실제 인터페이스는 순수한 가상입니다.
아무도

답변:


13

C ++에서 인터페이스를 만드는 정식 방법은 순수한 가상 소멸자를 제공하는 것입니다. 이것은 보장합니다

  • C ++에서는 추상 클래스의 인스턴스를 만들 수 없으므로 인터페이스 클래스 자체의 인스턴스를 만들 수 없습니다. 이것은 구성 불가능한 요구 사항 (디폴트 및 복사)을 처리합니다.
  • delete인터페이스에 대한 포인터를 호출 하면 올바른 작업이 수행됩니다. 해당 인스턴스에 대해 가장 파생 된 클래스의 소멸자를 호출합니다.

순수한 가상 소멸자가 있다고해도 인터페이스 참조에 대한 할당을 막을 수는 없습니다. 또한 실패해야하는 경우 보호 된 할당 연산자를 인터페이스에 추가해야합니다.

모든 C ++ 컴파일러는 다음과 같은 클래스 / 인터페이스를 처리 할 수 ​​있어야합니다 (모두 하나의 헤더 파일에서).

class MyInterface {
public:
  virtual ~MyInterface() = 0;
protected:
  MyInterface& operator=(const MyInterface&) { return *this; } // or = default for C++14
};

inline MyInterface::~MyInterface() {}

이것에 질식하는 컴파일러가 있다면 (C ++ 98 이전이어야 함) 옵션 2 (보호 생성자가 있음)는 두 번째로 좋습니다.

boost::noncopyable이 작업에는 다음과 같은 메시지를 보내기 때문에 사용 하지 않는 것이 좋습니다.계층 구조의 모든 클래스는 복사 할 수 없어야한다는 므로 이와 같이 사용하려는 의도에 익숙하지 않은 숙련 된 개발자에게는 혼란을 줄 수 있으므로이 .


If you need [prevent assignment] to fail as well, then you must add a protected assignment operator to your interface.: 이것은 내 문제의 근본입니다. 할당을 지원하기 위해 인터페이스가 필요한 경우는 드 rare니다. 반면, 참조로 인터페이스를 전달 하려는 경우 (NULL은 허용되지 않는 경우) 컴파일이 훨씬 더 큰 no-op 또는 슬라이싱을 피하고 싶습니다.
paercebal

할당 연산자를 호출해서는 안되므로 왜 정의를 제공합니까? 옆으로, 왜 만들지 private않습니까? 또한 default- 및 copy-ctor를 처리 할 수도 있습니다.
중복 제거기

5

내가 편집증입니까?

  • 인터페이스의 슬라이싱으로부터 코드를 보호하기 위해 편집증이 필요합니까? (나는 내가 아니라고 생각하지만, 아무도 모른다 ...)

이것이 위험 관리 문제가 아닙니까?

  • 슬라이싱 관련 버그가 발생할 가능성이 있습니까?
  • 눈에 띄지 않고 복구 할 수없는 버그를 유발할 수 있다고 생각하십니까?
  • 슬라이싱을 피하기 위해 어느 정도까지 기꺼이 하시겠습니까?

최고의 솔루션

  • 위의 방법 중 가장 좋은 해결책은 무엇입니까?

두 번째 솔루션 ( "보호")은 좋아 보이지만 C ++ 전문가는 아닙니다.
적어도 잘못된 사용법은 내 컴파일러 (g ++)에 의해 잘못보고 된 것으로 보입니다.

이제 매크로가 필요합니까? "예"라고 답합니다. 작성하는 지침의 목적이 무엇인지 말하지 않더라도 이것이 제품 코드에 특정 모범 사례를 적용하는 것입니다.

이를 위해 매크로는 사람들이 효과적으로 패턴을 적용하는시기를 감지하는 데 도움이됩니다. 기본 커밋 필터는 매크로의 사용 여부를 알려줍니다.

  • 사용하는 경우 패턴이 적용될 가능성이 있으며, 더 중요한 것은 올바르게 적용되는 것입니다 ( protected키워드 가 있는지 확인 ).
  • 사용하지 않으면 왜 그렇지 않은지 조사 할 수 있습니다.

매크로가 없으면 패턴이 필요하고 모든 경우에 잘 구현되었는지 검사해야합니다.

더 나은 솔루션

  • 또 다른 더 나은 해결책이 있습니까?

C ++에서 슬라이싱하는 것은 언어의 특성에 지나지 않습니다. 가이드 라인을 작성하고 있기 때문에 (초보 초보자를위한), "코딩 규칙"을 열거하는 것이 아니라 가르치는 데 집중해야합니다. 예제와 연습 문제와 함께 슬라이싱이 어떻게, 왜 발생하는지 설명해야합니다 (바퀴를 다시 만들지 말고 책과 튜토리얼에서 영감을 얻으십시오).

예를 들어, 연습 제목은 " C ++의 안전한 인터페이스 패턴은 무엇입니까 ?"

따라서 가장 좋은 방법은 C ++ 개발자가 슬라이싱이 발생할 때 발생하는 상황을 이해하도록하는 것입니다. 나는 그들이 그렇게한다면, 특정 패턴을 공식적으로 시행하지 않아도 코드에서 많은 실수를 저 지르지 않을 것이라고 확신합니다 (그러나 컴파일러 경고는 여전히 유효합니다).

컴파일러 정보

당신은 말합니다 :

이 제품에 대한 컴파일러 선택에 대한 권한이 없습니다.

종종 사람들은 말할 것이다 "나는 [X] 할 수있는 권한이 없습니다" , "나는 [Y] ... 어떻게해야 아니에요" ... 그들이 있기 때문에 생각 이 가능하지, 그리고 그들이 있기 때문에 시도하거나 물었다.

기술 문제에 대한 귀하의 의견을 제공하는 것은 귀하의 직무 설명의 일부일 것입니다. 컴파일러가 문제 도메인에 대한 완벽한 (또는 고유 한) 선택이라고 생각되면 사용하십시오. 그러나 또한 "인라인으로 구현 된 순수한 가상 소멸자는 내가 본 최악의 질식 지점이 아니다"고 말했다 . 내가 이해 한 바에 따르면 컴파일러는 매우 특별하여 지식이 풍부한 C ++ 개발자조차도이를 사용하는 데 어려움이 있습니다. 레거시 / 사내 컴파일러는 이제 기술 부채이며 다른 개발자 및 관리자와 해당 문제를 논의 할 권리가 있습니다 (의무?). .

컴파일러 유지 비용과 다른 컴파일러 사용 비용을 평가하십시오.

  1. 현재 컴파일러는 다른 사람이 할 수없는 것을 무엇입니까?
  2. 다른 컴파일러를 사용하여 제품 코드를 쉽게 컴파일 할 수 있습니까? 왜 안돼 ?

나는 당신의 상황을 모른다. 사실 당신은 아마도 특정 컴파일러에 묶일만한 정당한 이유가있을 것이다.
그러나 이것이 단순한 관성 인 경우, 당신이나 동료가 생산성이나 기술적 부채 문제를보고하지 않으면 상황은 결코 진화하지 않을 것입니다.


Am I paranoid...: "인터페이스를 올바르게 사용하기 쉽고 잘못 사용하기 어렵게하십시오". 누군가 내 정적 메소드 중 하나가 실수로 잘못 사용되었다고보고했을 때 그 특정 원칙을 맛 보았습니다. 생성 된 오류는 관련이없는 것으로 보이며 소스를 찾는 데 여러 시간의 엔지니어가 소요되었습니다. 이 "인터페이스 오류"는 인터페이스 참조를 다른 인터페이스에 할당하는 것과 같습니다. 예, 그런 종류의 오류를 피하고 싶습니다. 또한 C ++에서 철학은 컴파일 타임에 가능한 한 많이 잡아야한다는 것입니다. 언어는 우리에게 그 힘을 제공하므로 우리는 그것을 따라갑니다.
paercebal

Best solution: 동의한다. . . Better solution: 대단한 답변입니다. 내가 할게요 ... 지금, 이것에 대해 Pure virtual classes:이게 뭐죠? C ++ 추상 인터페이스? (상태가없고 순수한 가상 메소드 만있는 클래스?). 이 "순수한 가상 클래스"는 어떻게 슬라이싱을 방지 했습니까? (순수한 가상 메서드는 인스턴스화를 컴파일하지 않고 복사 할당하고 IIRC를 할당합니다.)
paercebal

About the compiler: 우리는 동의하지만, 우리 컴파일러는 내 책임 범위를 벗어납니다. 나는 세부 사항을 공개하지는 않지만 (내가 할 수 있으면 좋겠다) 내부 이유 (테스트 스위트와 같은) 및 외부 이유 (예 : 라이브러리와 클라이언트 연결)와 관련이 있습니다. 결국, 컴파일러 버전을 변경하거나 패치하는 것은 사소한 작업이 아닙니다. 깨진 컴파일러 하나를 최근 gcc로 바꾸지 마십시오.
paercebal

귀하의 의견에 대한 @paercebal 감사; 순수한 가상 클래스에 대해, 당신은 옳습니다. 모든 제약 조건을 해결하지는 못합니다 (이 부분을 제거 할 것입니다). 나는 "인터페이스 오류"부분과 컴파일 타임에 오류를 잡는 것이 유용하다는 것을 이해합니다. 그러나 당신은 편집증인지 물어 보았습니다. 합리적인 점검은 정적 검사의 필요성과 실수가 발생할 가능성의 균형을 맞추는 것이라고 생각합니다. 컴파일러와 행운을 빕니다 :)
coredump

1
특히 가이드 라인이 중학생을 대상으로하기 때문에 매크로에 관심이 없습니다 . 너무나 자주, 나는 맹목적으로 적용하고 실제로 무슨 일이 일어나고 있는지 이해하지 못하는 그런“유능한”도구를받은 사람들을 보았습니다. 그들은 상사가 스스로하기가 너무 어려울 것이라고 생각했기 때문에 매크로가하는 일이 가장 복잡한 일이라고 믿게되었습니다. 또한 매크로는 회사에만 존재하기 때문에 웹 검색을 수행 할 수 없으며 문서화 된 지침에서는 구성원이 선언하는 기능과 그 이유를 문서화 할 수 있습니다.
5gon12eder

2

슬라이싱 문제는 런타임 다형성 인터페이스를 사용자에게 노출 할 때 도입 된 유일한 문제는 아닙니다. 널 포인터, 메모리 관리, 공유 데이터를 생각하십시오. 이들 중 어느 것도 모든 경우에 쉽게 해결되지는 않습니다 (스마트 포인터는 훌륭하지만 은색 총알은 아닙니다). 실제로 게시물 에서 슬라이싱 문제 를 해결 하려는 것이 아니라 사용자가 복사 할 수 없도록 회피하는 것처럼 보입니다 . 슬라이싱 문제에 대한 솔루션을 제공하기 위해 가상 클론 멤버 기능을 추가하기 만하면됩니다. 런타임 다형성 인터페이스를 노출 할 때의 더 큰 문제는 사용자가 값 의미론보다 추론하기 어려운 참조 의미론을 처리하도록 강요한다는 것입니다.

C ++에서 이러한 문제를 피하는 가장 좋은 방법은 유형 삭제 를 사용하는 것 입니다. 이것은 일반적인 클래스 인터페이스 뒤에 런타임 다형성 인터페이스를 숨기는 기술입니다. 이 일반 클래스 인터페이스는 가치 의미론을 가지며 화면 뒤의 모든 다형성 '메시'를 처리합니다. std::function유형 삭제의 주요 예입니다.

사용자에게 상속을 노출하는 것이 나쁜 이유와 유형 삭제가 Sean Parent의 프레젠테이션을 보는 데 도움이되는 방법에 대한 자세한 설명은 다음과 같습니다.

상속은 악의 ​​기본 클래스입니다 (짧은 버전)

가치 의미론 및 개념 기반 다형성 (긴 버전; 따라 가기 쉽지만 소리는 좋지 않음)


0

당신은 편집증이 아닙니다. C ++ 프로그래머로서의 첫 번째 전문 작업은 슬라이싱과 충돌로 이어졌습니다. 나는 다른 사람들을 알고 있습니다. 이에 대한 좋은 해결책은 많지 않습니다.

컴파일러 제약 조건이 주어지면 옵션 2가 가장 좋습니다. 새로운 프로그래머가 이상하고 신비한 매크로를 만드는 대신 코드를 자동 생성하는 스크립트 또는 도구를 제안합니다. 신입 사원이 IDE를 사용하는 경우 인터페이스 이름을 요구하는 "New MYCOMPANY Interface"도구를 만들고 원하는 구조를 만들 수 있어야합니다.

프로그래머가 명령 행을 사용하는 경우 사용 가능한 스크립트 언어를 사용하여 코드를 생성하는 NewMyCompanyInterface 스크립트를 작성하십시오.

과거에는 일반적인 코드 패턴 (인터페이스, 상태 머신 등)에이 접근 방식을 사용했습니다. 좋은 프로그래머는 새로운 프로그래머가 출력을 읽고 쉽게 이해할 수있어 생성 할 수없는 무언가가 필요할 때 필요한 코드를 재생산 할 수 있다는 것입니다.

매크로와 다른 메타 프로그래밍 방식은 현재 일어나고있는 일을 혼란스럽게하는 경향이 있으며, 새로운 프로그래머는 '커튼 뒤'에서 일어나는 일을 배우지 못합니다. 그들이 패턴을 깨뜨려야 할 때, 그들은 예전처럼 잃어 버렸습니다.

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