我什么时候应该使用C ++私有继承?

时间:2009-03-17 22:06:07

标签: c++ oop

与受保护的继承不同,C ++私有继承进入主流C ++开发。但是,我仍然没有找到它的好用。

你们什么时候使用它?

14 个答案:

答案 0 :(得分:132)

我一直都在使用它。我脑子里有几个例子:

  • 当我想暴露一些但不是全部的基类接口时。公共继承将是一个谎言,因为Liskov substitutability被打破,而组合意味着编写一堆转发函数。
  • 当我想从没有虚拟析构函数的具体类派生时。公共继承将邀请客户端通过指向base的指针进行删除,从而调用未定义的行为。

一个典型的例子是私下从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类私下继承可以保存必须转发到封闭的实例。
  • 实现私有接口。这经常出现在观察者模式中。通常我的Observer类,MyClass说,使用一些Subject订阅自己。然后,只有MyClass需要做MyClass - &gt;观察者转换。系统的其余部分不需要知道它,因此指示了私有继承。

答案 1 :(得分:53)

接受答复后请注意:这不是一个完整的答案。如果您对此问题感兴趣,请阅读here(概念上)和 here (理论上和实践上)等其他答案。这只是一个可以通过私有继承实现的花哨技巧。虽然它花哨但它不是问题的答案。

除了C ++ FAQ(在其他注释中链接)中显示的私有继承的基本用法之外,您可以使用私有和虚拟继承的组合来密封类(在.NET术语中)或创建一个类 final (用Java术语)。这不是常用的,但无论如何我发现它很有趣:

class ClassSealer {
private:
   friend class Sealed;
   ClassSealer() {}
};
class Sealed : private virtual ClassSealer
{ 
   // ...
};
class FailsToDerive : public Sealed
{
   // Cannot be instantiated
};

密封可以实例化。它派生自 ClassSealer ,可以直接调用私有构造函数,因为它是朋友。

FailsToDerive 将无法编译,因为它必须直接调用 ClassSealer 构造函数(虚拟继承要求),但它不能,因为它在密封<中是私有的/ em> class,在这种情况下, FailsToDerive 不是 ClassSealer 的朋友。


修改

评论中提到,当时使用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 // ...

答案 2 :(得分:27)

私人继承的规范用法是“以”关系实施“(感谢Scott Meyers的'Effective C ++'这个措辞)。换句话说,继承类的外部接口与继承类没有(可见)关系,但它在内部使用它来实现其功能。

答案 3 :(得分:19)

私有继承的一个有用用途是当你有一个实现接口的类,然后在其他对象中注册。您将该接口设置为私有,以便类本身必须注册,并且只有其注册的特定对象才能使用这些函数。

例如:

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的私有方法,而其他外部类则不能。这是处理定义为接口的特定回调的一种很好的模式。

答案 4 :(得分:17)

我认为C++ FAQ Lite的关键部分是:

  

私有继承的合法长期使用是当你想要构建一个使用类Wilma中的代码的Fred时,来自Wilma类的代码需要从你的新类Fred调用成员函数。在这种情况下,Fred在Wilma中调用非虚拟,而Wilma调用(通常是纯虚拟)本身,由Fred重写。这对组合来说要困难得多。

如果有疑问,你应该更喜欢组合而不是私人继承。

答案 5 :(得分:4)

我觉得它对于我继承的接口(即抽象类)非常有用,我不想让其他代码接触接口(只有继承类)。

[在一个例子中编辑]

example链接到上方。说那个

  

[...]类Wilma需要从你的新类Fred调用成员函数。

是说Wilma要求Fred能够调用某些成员函数,或者更确切地说它是 Wilma是一个接口。因此,如示例中所述

  私人继承不是邪恶的;它的维护成本更高,因为它增加了某人更改破坏代码的可能性。

评论程序员需要满足我们的接口要求或破坏代码所需的效果。并且,由于fredCallsWilma()受到保护,只有朋友和派生类可以触及它,即只有继承类可以触及(和朋友)的继承接口(抽象类)。

[在另一个例子中编辑]

This page简要讨论了私有接口(从另一个角度来看)。

答案 6 :(得分:2)

当我想在另一个接口中公开一个较小的接口(例如一个集合)时,我发现使用私有继承很有用,其中集合实现需要访问公开类的状态,类似于Java中的内部类。

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)。无需额外的数据成员占用空间。

答案 7 :(得分:1)

有时它可能是聚合的替代方法,例如,如果您想要聚合但可更改聚合实体的行为(覆盖虚函数)。

但你是对的,现实世界中没有很多例子。

答案 8 :(得分:1)

如果派生类   - 需要重用代码和   - 你不能改变基类和   - 正在使用锁的成员保护其方法。

然后你应该使用私有继承,否则你有通过这个派生类导出的解锁基本方法的危险。

答案 9 :(得分:1)

我找到了一个很好的私有继承应用程序,尽管它的使用有限。

要解决的问题

假设您获得以下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;
};

这使得必须进行以下调整:

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不再拥有任何公共数据成员。它使得实现易于正确使用并且很难(不可能?)使用错误。

代码段形成compiling example on Coliru

答案 10 :(得分:1)

如果您需要对std::ostream进行一些小的更改(例如this question),则可能需要

  1. 创建一个从MyStreambuf派生的类std::streambuf并在那里进行更改
  2. 创建一个类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's answer to the aforementioned question中提出,首先简单地从流缓冲区继承,然后从流继承,然后使用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 }
    {}
};

答案 11 :(得分:0)

当关系不是“是一个”时要使用的私有继承,但是新类可以“在现有类中实现”或新类“像现有类一样工作。”

来自“安德烈亚历山大的C ++编码标准,Herb Sutter”的例子: - 考虑两个类Square和Rectangle都有虚函数来设置它们的高度和宽度。然后Square无法正确地继承Rectangle,因为使用可修改Rectangle的代码将假定SetWidth不会更改高度(Rectangle是否显式记录该合约),而Square :: SetWidth不能保留该合约及其自身的方形不变性同一时间。但是,如果Square的客户端假设Square的区域是其宽度平方,或者它们依赖于其他一些不支持Rectangle的属性,则Rectangle也无法正确地从Square继承。

正方形“is-a”矩形(数学上)但Square不是矩形(行为)。因此,我们宁愿说“工作就像一个”(或者,如果你愿意,“可用作一个”),而不是“is-a”,以使描述不易产生误解。

答案 12 :(得分:0)

一个班级拥有一个不变量。不变量由构造函数建立。但是,在许多情况下,查看对象的表示状态(您可以通过网络传输或保存到文件 - 如果您愿意,可以使用DTO)非常有用。 REST最好用AggregateType完成。如果您是正确的,那么尤其如此。考虑:

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不影响总和);然而,当没有缓存时,值得查看。

私有继承几乎总是由成员建模(如果需要,存储对基数的引用)。以这种方式建模并不总是值得的;有时继承是最有效的表示。

答案 13 :(得分:-1)

仅仅因为C ++有一个功能,并不意味着它有用或者应该使用它。

我会说你根本不应该使用它。

如果你正在使用它,那么,你基本上是在侵犯封装,降低凝聚力。您将数据放在一个类中,并添加在另一个类中操作数据的方法。

与其他C ++功能一样,它可用于实现密封类的副作用(如dribeas的回答中所述),但这并不是一个很好的功能。