참고 : 다음은 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 년이 걸렸습니다. 밖으로).
- 소멸자 순수 가상을 인라인으로 유지해야하므로 선언 할 수 없으며 일부 컴파일러는 순수 가상 메소드를 인라인 빈 본문으로 요약하지 않습니다.
- 예,이 클래스의 유일한 요점은 구현자를 가상으로 파괴하는 것입니다. 드문 경우입니다.
- 추가적인 가상 순수 메소드 (대부분의 경우)가 있어도이 클래스는 여전히 복사 가능합니다.
그래서 안돼...
첫 번째 해결책 : boost :: noncopyable
class VirtuallyDestructible : boost::noncopyable
{
public :
virtual ~VirtuallyDestructible() {}
} ;
이 솔루션은 명확하고 명확하며 C ++ (매크로 없음)이므로 최고입니다.
문제는 VirtuallyConstructible을 여전히 기본 구성 할 수 있기 때문에 특정 인터페이스에서 여전히 작동하지 않는다는 것입니다 .
- 우리는 소멸자를 순수 가상으로 선언 할 수 없습니다. 인라인을 유지해야하기 때문에 일부 컴파일러는 소화하지 않습니다.
- 예,이 클래스의 유일한 요점은 구현자를 가상으로 파괴하는 것입니다. 드문 경우입니다.
또 다른 문제는 복사 할 수없는 인터페이스를 구현하는 클래스가 복사 생성자와 할당 연산자에 메소드가 필요한 경우 명시 적으로 선언 / 정의해야한다는 것입니다 (코드에는 클라이언트가 여전히 액세스 할 수있는 값 클래스가 있습니다) 인터페이스).
이것은 우리가 가고 싶어하는 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 :
이것은 다음과 같은 이유로 내 문제에 대한 실용적인 솔루션입니다.
- 이 클래스는 인스턴스화 할 수 없습니다 (생성자가 보호됨)
- 이 클래스는 사실상 파괴 될 수 있습니다
- 이 클래스는 상속 클래스에 과도한 제약을 가하지 않고 상속 될 수 있습니다 (예 : 상속 클래스는 기본적으로 복사 가능)
- 매크로를 사용한다는 것은 인터페이스 "선언"이 쉽게 인식 가능하고 검색 가능하다는 것을 의미하며, 코드를 수정하기 쉽게 한 곳에서 고려합니다 (적절한 접두어 이름은 바람직하지 않은 이름 충돌을 제거합니다)
다른 답변은 귀중한 통찰력을 제공했습니다. 기회를 주신 모든 분들께 감사드립니다.
나는이 질문에 여전히 다른 현상금을 넣을 수 있다고 생각합니다. 그리고 나는 그것을 볼 수있을만큼 충분히 깨달음 답변을 가치있게 생각합니다. 나는 그 대답에 할당하기 위해 현상금을 열 것입니다.
virtual ~VirtuallyDestructible() = 0인터페이스 클래스의 가상 상속과 추상 멤버 만 사용하면됩니다. VirtuallyDestructible을 생략 할 수 있습니다.
virtual void bar() = 0;예를 들어? 인터페이스가 제대로 표시되지 않습니다.