development

C ++ 개인 상속을 언제 사용해야합니까?

big-blog 2020. 7. 27. 07:15
반응형

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
};

봉인 을 인스턴스화 할 수 있습니다. ClassSealer 에서 파생되며 친구처럼 개인 생성자를 직접 호출 할 수 있습니다.

FailsToDeriveClassSealer 생성자를 직접 호출해야하기 때문에 컴파일되지 않지만 (가상 상속 요구 사항) Sealed 클래스 에서 private 이므로 FailsToDeriveClassSealer 의 친구가 아닙니다 .


편집하다

의견에서 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 // ...

나는 항상 그것을 사용합니다. 내 머리 꼭대기에서 몇 가지 예 :

  • 기본 클래스의 인터페이스 중 일부만 노출하고 싶을 때. Liskov의 대체 가능성 이 깨짐에 따라 공개 상속은 거짓말이 될 수 있지만 구성은 많은 전달 함수를 작성하는 것을 의미합니다.
  • 가상 소멸자가없는 구체적인 수업에서 파생하고 싶을 때. 퍼블릭 상속은 클라이언트가 포인터를베이스로 삭제하여 정의되지 않은 동작을 호출하도록 초대합니다.

일반적인 예는 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...  
};
  • 어댑터 패턴을 구현할 때 Adapted 클래스에서 비공개로 상속하면 동봉 된 인스턴스로 전달하지 않아도됩니다.
  • 개인 인터페이스를 구현합니다. 이것은 종종 관찰자 패턴과 함께 나타납니다. MyClass에 따르면 일반적으로 내 관찰자 클래스 는 일부 주제를 구독합니다 . 그런 다음 MyClass 만 MyClass-> Observer 변환을 수행하면됩니다. 나머지 시스템은 그것에 대해 알 필요가 없으므로 개인 상속이 표시됩니다.

개인 상속의 정식 사용은 "이 용어로 Scott Meyers의 'Effective C ++'덕분에"구현 된 측면 "관계입니다. 다시 말해, 상속 클래스의 외부 인터페이스는 상속 클래스와 (보이는) 관계가 없지만 내부적으로 사용하여 기능을 구현합니다.


개인 상속의 유용한 사용법 중 하나는 인터페이스를 구현하는 클래스가 있고 다른 객체에 등록 된 경우입니다. 클래스 자체를 등록하고 등록 된 특정 객체 만 해당 기능을 사용할 수 있도록 해당 인터페이스를 개인용으로 만듭니다.

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

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의 개인 메소드를 호출 할 수 있지만 다른 외부 클래스는이를 수행 할 수 없습니다. 인터페이스로 정의 된 특정 콜백을 처리하기위한 훌륭한 패턴입니다.


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)합니다. 추가 데이터 멤버가 공간을 차지할 필요가 없습니다.


사용이 제한되어 있지만 개인 상속을위한 멋진 응용 프로그램을 찾았습니다.

해결해야 할 문제

다음과 같은 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 접근

물론 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-ish입니다

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;
};

This makes the following adaptions necessary:

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);
}

This solution solves all the problems. No manual memory management and Widget is nicely encapsulated so that WidgetImpl does not have any public data members anymore. It makes the implementation easy to use correctly and hard (impossible?) to use wrong.

The code snippets form a compiling example on Coliru.


If derived class - needs to reuse code and - you can't change base class and - is protecting its methods using base's members under a lock.

then you should use private inheritance, otherwise you have danger of unlocked base methods exported via this derived class.


Sometimes it could be an alternative to aggregation, for example if you want aggregation but with changed behaviour of aggregable entity (overriding the virtual functions).

But you're right, it has not many examples from the real world.


Private Inheritance to be used when relation is not "is a", But New class can be "implemented in term of existing class" or new class "work like" existing class.

example from "C++ coding standards by Andrei Alexandrescu, Herb Sutter" :- Consider that two classes Square and Rectangle each have virtual functions for setting their height and width. Then Square cannot correctly inherit from Rectangle, because code that uses a modifiable Rectangle will assume that SetWidth does not change the height (whether Rectangle explicitly documents that contract or not), whereas Square::SetWidth cannot preserve that contract and its own squareness invariant at the same time. But Rectangle cannot correctly inherit from Square either, if clients of Square assume for example that a Square's area is its width squared, or if they rely on some other property that doesn't hold for Rectangles.

A square "is-a" rectangle (mathematically) but a Square is not a Rectangle (behaviorally). Consequently, instead of "is-a," we prefer to say "works-like-a" (or, if you prefer, "usable-as-a") to make the description less prone to misunderstanding.


A class holds an invariant. The invariant is established by the constructor. However, in many situations it's useful to have a view of the representation state of the object (which you can transmit over network or save to a file - DTO if you prefer). REST is best done in terms of an AggregateType. This is especially true if you're const correct. Consider:

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
}

At this point, you might just store collections of cache in containers and look it up on construction. Handy if there's some real processing. Note that cache is part of the QE: operations defined on the QE might mean the cache is partially reusable (e.g., c does not affect the sum); yet, when there's no cache, it's worth to look it up.

Private inheritance can almost always modelled by a member (storing reference to the base if needed). It's just not always worth it to model that way; sometimes inheritance is the most efficient representation.


Just because C++ has a feature, doesn't mean it's useful or that it should be used.

I'd say you shouldn't use it at all.

If you're using it anyway, well, you're basically violating encapsulation, and lowering cohesion. You're putting data in one class, and adding methods that manipulates the data in another one.

Like other C++ features, it can be used to achieve side effects such as sealing a class (as mentioned in dribeas' answer), but this doesn't make it a good feature.

참고URL : https://stackoverflow.com/questions/656224/when-should-i-use-c-private-inheritance

반응형