보호 된 상속과 달리 C ++ 개인 상속은 주류 C ++ 개발에 적용되었습니다. 그러나 여전히 좋은 용도를 찾지 못했습니다.
언제 사용 하시나요?
보호 된 상속과 달리 C ++ 개인 상속은 주류 C ++ 개발에 적용되었습니다. 그러나 여전히 좋은 용도를 찾지 못했습니다.
언제 사용 하시나요?
답변:
답변 수락 후 참고 사항 : 이것은 완전한 답변이 아닙니다. 질문에 관심이있는 경우 여기 (개념적으로) 및 여기 (이론 및 실제) 와 같은 다른 답변을 읽으십시오 . 이것은 개인 상속으로 얻을 수있는 멋진 속임수 일뿐입니다. 화려 하지만 질문에 대한 답은 아닙니다.
C ++ FAQ (다른 사람의 의견에 링크 됨)에 표시된 개인 상속의 기본 사용법 외에도 개인 및 가상 상속의 조합을 사용 하여 클래스 를 봉인 하거나 (.NET 용어로) 클래스를 최종 (Java 용어로) 만들 수 있습니다. . 이것은 일반적인 사용은 아니지만 어쨌든 흥미로운 것을 발견했습니다.
class ClassSealer {
private:
friend class Sealed;
ClassSealer() {}
};
class Sealed : private virtual ClassSealer
{
// ...
};
class FailsToDerive : public Sealed
{
// Cannot be instantiated
};
Sealed 는 인스턴스화 될 수 있습니다. ClassSealer 에서 파생되며 친구 인 것처럼 개인 생성자를 직접 호출 할 수 있습니다.
FailsToDerive 는 ClassSealer 생성자를 직접 호출해야하므로 컴파일되지 않지만 (가상 상속 요구 사항) Sealed 클래스와이 경우에는 FailsToDerive 에서 비공개 이므로 컴파일 할 수 없습니다. 는 의 친구가 아닙니다 .
편집하다
이것은 CRTP를 사용하여 당시에는 일반적으로 만들 수 없다는 의견에서 언급되었습니다. C ++ 11 표준은 템플릿 인수와 친구가되도록 다른 구문을 제공하여 이러한 제한을 제거합니다.
template <typename T>
class Seal {
friend T; // not: friend class T!!!
Seal() {}
};
class Sealed : private virtual Seal<Sealed> // ...
물론 C ++ 11은 final
정확히이 목적을 위해 문맥 키워드를 제공하기 때문에 이것은 모두 문제입니다 .
class Sealed final // ...
나는 항상 그것을 사용합니다. 내 머릿속에서 몇 가지 예 :
일반적인 예는 STL 컨테이너에서 비공개로 파생하는 것입니다.
class MyVector : private vector<int>
{
public:
// Using declarations expose the few functions my clients need
// without a load of forwarding functions.
using vector<int>::push_back;
// etc...
};
push_back
, MyVector
무료로를 가져옵니다.
template<typename... Args> constexpr decltype(auto) f(Args && ... args) noexcept(noexcept(std::declval<Base &>().f(std::forward<Args>(args)...)) and std::is_nothrow_move_constructible<decltype(std::declval<Base &>().f(std::forward<Args>(args)...))>) { return m_base.f(std::forward<Args>(args)...); }
수도 있고 Base::f;
. 당신이 개인 상속 그 기능과 유연성의 대부분을 원하고 있다면 using
사항이 있습니다, 당신은 각 기능에 대한 그 몬스터를 (약 잊지 마세요 const
및 volatile
과부하!).
개인 상속의 정식 사용은 "구현 된"관계입니다 (이 문구에 대한 Scott Meyers의 'Effective C ++'덕분에). 즉, 상속 클래스의 외부 인터페이스는 상속 된 클래스와 (표시되는) 관계가 없지만 내부적으로이를 사용하여 기능을 구현합니다.
private 상속의 한 가지 유용한 용도는 인터페이스를 구현하는 클래스가있을 때 다른 개체에 등록되는 경우입니다. 해당 인터페이스를 비공개로 설정하여 클래스 자체를 등록하고 등록 된 특정 개체 만 해당 함수를 사용할 수 있도록합니다.
예를 들면 :
class FooInterface
{
public:
virtual void DoSomething() = 0;
};
class FooUser
{
public:
bool RegisterFooInterface(FooInterface* aInterface);
};
class FooImplementer : private FooInterface
{
public:
explicit FooImplementer(FooUser& aUser)
{
aUser.RegisterFooInterface(this);
}
private:
virtual void DoSomething() { ... }
};
따라서 FooUser 클래스는 FooInterface 인터페이스를 통해 FooImplementer의 private 메서드를 호출 할 수 있지만 다른 외부 클래스는 호출 할 수 없습니다. 이것은 인터페이스로 정의 된 특정 콜백을 처리하기위한 훌륭한 패턴입니다.
C ++ FAQ Lite 의 중요한 섹션 은 다음과 같습니다.
개인 상속에 대한 합법적이고 장기적인 사용은 Wilma 클래스의 코드를 사용하는 Fred 클래스를 빌드하고 Wilma 클래스의 코드가 새 클래스 Fred에서 멤버 함수를 호출해야하는 경우입니다. 이 경우 Fred는 Wilma에서 비가 상을 호출하고 Wilma는 자체적으로 (일반적으로 순수 가상)을 호출하며 이는 Fred에 의해 재정의됩니다. 이것은 작곡과 관련하여 훨씬 더 어려울 것입니다.
확실하지 않은 경우 개인 상속보다 구성을 선호해야합니다.
다른 코드가 인터페이스 (상속하는 클래스 만)를 터치하지 않도록 상속하는 인터페이스 (즉, 추상 클래스)에 유용합니다.
[예제에서 편집 됨]
위에 링크 된 예를 사용 하십시오 . 에 대해 말하는 것
[...] 클래스 Wilma는 새 클래스 Fred에서 멤버 함수를 호출해야합니다.
이는 Wilma가 Fred가 특정 멤버 함수를 호출 할 수 있도록 요구하는 것입니다. 또는 Wilma가 인터페이스 라고 말하는 것입니다 . 따라서 예제에서 언급했듯이
사적 유산은 악이 아닙니다. 누군가가 당신의 코드를 깨뜨릴 수있는 무언가를 변경할 가능성을 증가시키기 때문에 유지하는 것은 더 비싸다.
인터페이스 요구 사항을 충족해야하는 프로그래머가 원하는 효과에 대한 의견이나 코드를 깨는 것입니다. 그리고 fredCallsWilma ()는 친구 만 보호되고 파생 클래스는 상속 된 인터페이스 (추상 클래스)를 만질 수 있으며 상속 클래스 만 (및 친구) 만질 수 있습니다.
[다른 예에서 편집 됨]
이 페이지 에서는 비공개 인터페이스에 대해 간략하게 설명합니다 (또 다른 각도에서).
때로는 내부 클래스와 유사한 방식으로 컬렉션 구현이 노출 클래스의 상태에 액세스해야하는 다른 인터페이스에서 더 작은 인터페이스 (예 : 컬렉션)를 노출하려는 경우 개인 상속을 사용하는 것이 유용하다는 것을 알게되었습니다. 자바.
class BigClass;
struct SomeCollection
{
iterator begin();
iterator end();
};
class BigClass : private SomeCollection
{
friend struct SomeCollection;
SomeCollection &GetThings() { return *this; }
};
그런 다음 SomeCollection이 BigClass에 액세스해야하는 경우 static_cast<BigClass *>(this)
. 추가 데이터 멤버가 공간을 차지할 필요가 없습니다.
BigClass
이 예제 에 is there 의 전방 선언이 필요하지 않습니다 . 나는 이것이 흥미 롭다고 생각하지만 내 얼굴에는 끔찍한 소리를 지른다.
제한된 사용이 있지만 개인 상속에 대한 멋진 응용 프로그램을 찾았습니다.
다음 C API가 제공된다고 가정합니다.
#ifdef __cplusplus
extern "C" {
#endif
typedef struct
{
/* raw owning pointer, it's C after all */
char const * name;
/* more variables that need resources
* ...
*/
} Widget;
Widget const * loadWidget();
void freeWidget(Widget const * widget);
#ifdef __cplusplus
} // end of extern "C"
#endif
이제 작업은 C ++를 사용하여이 API를 구현하는 것입니다.
물론 다음과 같이 C-ish 구현 스타일을 선택할 수 있습니다.
Widget const * loadWidget()
{
auto result = std::make_unique<Widget>();
result->name = strdup("The Widget name");
// More similar assignments here
return result.release();
}
void freeWidget(Widget const * const widget)
{
free(result->name);
// More similar manual freeing of resources
delete widget;
}
그러나 몇 가지 단점이 있습니다.
struct
잘못struct
우리는 C ++를 사용할 수 있습니다. 그렇다면 C ++의 모든 기능을 사용하는 것은 어떨까요?
위의 문제는 기본적으로 모두 수동 리소스 관리와 관련이 있습니다. 떠오르는 해결책 은 각 변수에 대한 Widget
파생 클래스에서 리소스 관리 인스턴스를 상속 하고 추가하는 것입니다 WidgetImpl
.
class WidgetImpl : public Widget
{
public:
// Added bonus, Widget's members get default initialized
WidgetImpl()
: Widget()
{}
void setName(std::string newName)
{
m_nameResource = std::move(newName);
name = m_nameResource.c_str();
}
// More similar setters to follow
private:
std::string m_nameResource;
};
이는 다음과 같은 구현을 단순화합니다.
Widget const * loadWidget()
{
auto result = std::make_unique<WidgetImpl>();
result->setName("The Widget name");
// More similar setters here
return result.release();
}
void freeWidget(Widget const * const widget)
{
// No virtual destructor in the base class, thus static_cast must be used
delete static_cast<WidgetImpl const *>(widget);
}
이렇게 우리는 위의 모든 문제를 해결했습니다. 그러나 클라이언트는 여전히의 세터에 대해 잊을 수 WidgetImpl
와 할당Widget
구성원에게 직접 있습니다.
Widget
멤버 를 캡슐화하기 위해 개인 상속을 사용합니다. 안타깝게도 이제 두 클래스간에 캐스트하려면 두 개의 추가 함수가 필요합니다.
class WidgetImpl : private Widget
{
public:
WidgetImpl()
: Widget()
{}
void setName(std::string newName)
{
m_nameResource = std::move(newName);
name = m_nameResource.c_str();
}
// More similar setters to follow
Widget const * toWidget() const
{
return static_cast<Widget const *>(this);
}
static void deleteWidget(Widget const * const widget)
{
delete static_cast<WidgetImpl const *>(widget);
}
private:
std::string m_nameResource;
};
따라서 다음과 같은 조정이 필요합니다.
Widget const * loadWidget()
{
auto widgetImpl = std::make_unique<WidgetImpl>();
widgetImpl->setName("The Widget name");
// More similar setters here
auto const result = widgetImpl->toWidget();
widgetImpl.release();
return result;
}
void freeWidget(Widget const * const widget)
{
WidgetImpl::deleteWidget(widget);
}
이 솔루션은 모든 문제를 해결합니다. 수동 메모리 관리가 없으며 Widget
멋지게 캡슐화되어 있으므로WidgetImpl
가 없으며 더 이상 공용 데이터 멤버가 됩니다. 구현을 올바르게 사용하기 쉽고 잘못 사용하기가 어렵습니다 (불가능합니까?).
코드 조각 은 Coliru 에서 컴파일 예제를 구성 합니다.
파생 클래스-코드를 재사용해야하고-기본 클래스를 변경할 수없고-잠금 상태에서 기본 멤버를 사용하여 해당 메서드를 보호하는 경우.
그런 다음 개인 상속을 사용해야합니다. 그렇지 않으면이 파생 클래스를 통해 내 보낸 잠금 해제 된 기본 메서드의 위험이 있습니다.
관계가 "is a"가 아닐 때 사용할 Private Inheritance를 사용할 수 있지만, New 클래스는 "기존 클래스의 관점에서 구현"하거나 기존 클래스와 "work like"할 수 있습니다.
"C ++ 코딩 표준 by Andrei Alexandrescu, Herb Sutter"의 예 :-두 클래스 Square 및 Rectangle에는 각각 높이와 너비를 설정하는 가상 기능이 있다고 가정합니다. 그러면 수정 가능한 Rectangle을 사용하는 코드는 SetWidth가 높이를 변경하지 않는다고 가정하지만 (Rectangle이 축소를 명시 적으로 문서화하는지 여부에 관계없이) Square :: SetWidth는 해당 계약 및 자체 직각도를 동시. 그러나 Square의 클라이언트가 예를 들어 Square의 영역이 너비의 제곱이라고 가정하거나 Rectangle을 유지하지 않는 다른 속성에 의존하는 경우에도 Rectangle은 Square에서 올바르게 상속 할 수 없습니다.
정사각형 "is-a"직사각형 (수학적)이지만 정사각형은 직사각형이 아닙니다 (동작 적으로). 결과적으로 "is-a"대신 "works-like-a"(또는 원하는 경우 "usable-as-a")라고 말하여 설명이 오해를 덜 받도록합니다.
클래스에는 불변성이 있습니다. 불변은 생성자에 의해 설정됩니다. 그러나 많은 상황에서 객체의 표현 상태를 보는 것이 유용합니다 (네트워크를 통해 전송하거나 파일에 저장할 수 있습니다-원하는 경우 DTO). REST는 AggregateType 측면에서 가장 잘 수행됩니다. const가 맞다면 특히 그렇습니다. 치다:
struct QuadraticEquationState {
const double a;
const double b;
const double c;
// named ctors so aggregate construction is available,
// which is the default usage pattern
// add your favourite ctors - throwing, try, cps
static QuadraticEquationState read(std::istream& is);
static std::optional<QuadraticEquationState> try_read(std::istream& is);
template<typename Then, typename Else>
static std::common_type<
decltype(std::declval<Then>()(std::declval<QuadraticEquationState>()),
decltype(std::declval<Else>()())>::type // this is just then(qes) or els(qes)
if_read(std::istream& is, Then then, Else els);
};
// this works with QuadraticEquation as well by default
std::ostream& operator<<(std::ostream& os, const QuadraticEquationState& qes);
// no operator>> as we're const correct.
// we _might_ (not necessarily want) operator>> for optional<qes>
std::istream& operator>>(std::istream& is, std::optional<QuadraticEquationState>);
struct QuadraticEquationCache {
mutable std::optional<double> determinant_cache;
mutable std::optional<double> x1_cache;
mutable std::optional<double> x2_cache;
mutable std::optional<double> sum_of_x12_cache;
};
class QuadraticEquation : public QuadraticEquationState, // private if base is non-const
private QuadraticEquationCache {
public:
QuadraticEquation(QuadraticEquationState); // in general, might throw
QuadraticEquation(const double a, const double b, const double c);
QuadraticEquation(const std::string& str);
QuadraticEquation(const ExpressionTree& str); // might throw
}
이 시점에서 컨테이너에 캐시 컬렉션을 저장하고 생성시 조회 할 수 있습니다. 실제 처리가 있으면 편리합니다. 캐시는 QE의 일부입니다. QE에 정의 된 작업은 캐시를 부분적으로 재사용 할 수 있음을 의미 할 수 있습니다 (예 : c는 합계에 영향을주지 않음). 그러나 캐시가 없으면 찾아 볼 가치가 있습니다.
개인 상속은 거의 항상 멤버에 의해 모델링 될 수 있습니다 (필요한 경우 기본에 대한 참조 저장). 그런 식으로 모델링하는 것이 항상 가치가있는 것은 아닙니다. 때로는 상속이 가장 효율적인 표현입니다.
이 질문std::ostream
과 같이 약간의 변경 사항 이 필요한 경우 다음을 수행해야 할 수 있습니다.
MyStreambuf
파생되는 클래스 만들기std::streambuf
만들고 거기 변경 사항을 구현합니다.MyOStream
를 만듭니다.std::ostream
MyStreambuf
std::ostream
첫 번째 아이디어는 MyStream
인스턴스를 데이터 멤버로 MyOStream
클래스 에 추가하는 것입니다 .
class MyOStream : public std::ostream
{
public:
MyOStream()
: std::basic_ostream{ &m_buf }
, m_buf{}
{}
private:
MyStreambuf m_buf;
};
그러나 기본 클래스는 데이터 멤버보다 먼저 생성되므로 정의되지 않은 동작 인 아직 생성되지 않은 std::streambuf
인스턴스에 대한 포인터를 전달합니다 std::ostream
.
솔루션은 앞서 언급 한 질문에 대한 Ben의 답변 에서 제안됩니다. 먼저 스트림 버퍼에서 상속 한 다음 스트림에서 상속 한 다음 다음을 사용하여 스트림을 초기화합니다 this
.
class MyOStream : public MyStreamBuf, public std::ostream
{
public:
MyOStream()
: MyStreamBuf{}
, basic_ostream{ this }
{}
};
그러나 결과 클래스 std::streambuf
는 일반적으로 원하지 않는 인스턴스 로도 사용될 수 있습니다 . 개인 상속으로 전환하면이 문제가 해결됩니다.
class MyOStream : private MyStreamBuf, public std::ostream
{
public:
MyOStream()
: MyStreamBuf{}
, basic_ostream{ this }
{}
};
C ++에 기능이 있다고해서 유용하거나 사용해야한다는 의미는 아닙니다.
나는 당신이 그것을 전혀 사용하지 말아야한다고 말하고 싶습니다.
어쨌든 그것을 사용한다면 기본적으로 캡슐화를 위반하고 응집력을 낮추는 것입니다. 한 클래스에 데이터를 넣고 다른 클래스에 데이터를 조작하는 메서드를 추가합니다.
다른 C ++ 기능과 마찬가지로 클래스를 봉인하는 것과 같은 부작용을 달성하는 데 사용할 수 있지만 (dribeas의 답변에서 언급했듯이) 이것은 좋은 기능이 아닙니다.