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


116

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

你们什么时候使用它?

c++  oop 

Answers:


60

接受答案后的注意事项:这不是完整的答案。阅读其他答案喜欢这里(概念),并在这里(包括理论和PRACTIC),如果你有兴趣的问题。这只是可以通过私有继承实现的花哨技巧。虽然很花哨,但这并不是问题的答案。

除了C ++ FAQ中显示的仅私有继承的基本用法(在他人的评论中链接),您还可以使用私有继承和虚拟继承的组合来密封类(.NET术语)或使类定型(使用Java术语) 。这不是常见的用法,但是无论如何我发现它很有趣:

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

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

FailsToDerive将无法编译,因为它必须调用ClassSealer直接构造(虚拟继承的要求),但它不能,因为它是在私人密封类,并在这种情况下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 // ...

那是一个很棒的技术。我将在上面写一个博客条目。

1
问题:如果我们不使用虚拟继承,则FailsToDerive将进行编译。正确?

4
+1。@Sasha:需要正确的虚拟继承,因为派生最多的类总是直接调用所有虚拟继承类的构造函数,而普通继承则不是这种情况。
j_random_hacker

5
可以使其通用,而无需为要密封的每个类创建自定义的ClassSealer!检出:class ClassSealer {protected:ClassSealer(){}}; 就这样。

+1 Iraimbilanja,非常酷!顺便说一句,我看到了您先前关于使用CRTP的评论(现已删除):我认为这实际上应该可行,正确地获取模板朋友的语法非常棘手。但是无论如何,您的非模板解决方案都很棒:)
j_random_hacker 2009年

138

我用它所有的时间。举几个例子:

  • 当我想公开一些但不是全部的基类接口时。公共继承将是一个谎言,因为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说,通常我的Observer类会订阅一些Subject。然后,只有MyClass需要执行MyClass-> Observer转换。系统的其余部分不需要了解它,因此指示了私有继承。

4
@Krsna:实际上,我不这么认为。这里只有一个原因:懒惰,除了最后一个之外,解决起来会比较棘手。
Matthieu M.

11
没有那么多懒惰(除非您以很好的方式表示)。这允许创建已公开的新函数重载,而无需进行任何额外工作。如果在C ++ 1x中它们向添加了3个新的重载push_back,则MyVector可以免费获得它们。
大卫·斯通

@DavidStone,您不能使用模板方法吗?
朱利安__

5
@Julien__:是的,您可以编写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语句也给你,你有怪物每个功能(不要忘记constvolatile过载!)。
戴维·斯通

2
我说大多数功能是因为您仍在调用using语句版本中不存在的一个额外的move构造函数。通常,您希望可以对其进行优化,但是从理论上讲,该函数可以按值返回不可移动的类型。转发功能模板还具有额外的模板实例化和constexpr深度。这可能会导致您的程序遇到实施限制。
戴维·斯通

31

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


6
可能值得一提的是在这种情况下使用它的原因之一:这允许执行空的基类优化,如果该类是成员而不是基类,则不会发生优化。
jalf

2
它的主要用途是在真正重要的地方减少空间消耗,例如在策略控制的字符串类或压缩对中。实际上,boost :: compressed_pa​​ir使用了受保护的继承。
Johannes Schaub-litb

杰夫:嘿,我没有意识到。我认为当您需要访问类的受保护成员时,非公共继承主要用作黑客。我想知道为什么在使用合成时空的对象会占用任何空间。可能是针对通用寻址能力的

3
使类不可复制也很方便-只是私自从不可复制的空类继承。现在,您不必完成声明但不必定义私有副本构造函数和赋值运算符的繁琐工作。迈耶斯也谈到了这一点。
Michael Burr

我没有意识到这个问题实际上是关于私有继承而不是受保护的继承。是的,我猜想它有很多应用。虽然://似乎很少使用,但我想不出很多保护继承的例子。
Johannes Schaub-litb

23

私有继承的一种有用用法是,当您有一个实现接口的类,然后将该类注册到其他某个对象时。您可以将该接口设为私有,以便类本身必须注册,并且只有其注册时使用的特定对象才能使用这些功能。

例如:

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


1
确实,私有继承是私有IS-A。
curiousguy

18

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

私有继承的合法长期使用是当您想要构建一个使用Wilma类中的代码的Fred类时,而Wilma类中的代码需要从您的新类Fred中调用成员函数。在这种情况下,Fred在Wilma中调用非虚拟机,而Wilma本身调用(通常是纯虚拟机),它们被Fred覆盖。这将很难与组成。

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


4

我发现它对于我继承的接口(即抽象类)很有用,我不希望其他代码接触该接口(仅继承类)。

[在示例中编辑]

以上面链接的示例为例。这样说

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

这表示Wilma要求Fred能够调用某些成员函数,或者说Wilma是接口。因此,如示例中所述

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

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

[在另一个示例中编辑]

本页简要讨论了私有接口(从另一个角度)。


听起来真的没有用...您能举个例子

我想我知道您的去向...一个典型的用例可能是Wilma是某种类型的实用工具类,需要在Fred中调用虚函数,但是其他类不需要知道Fred是按术语实现的,威尔玛 对?
j_random_hacker

是。我应该指出,据我所知,术语“接口”在Java中更常用。当我第一次听说它的时候,我想应该给它起一个更好的名字。因为在此示例中,我们有一个界面,没有人以我们通常认为的单词的方式与之交互。
偏置

@Noos:是的,我认为您的“ Wilma是一个接口”的说法有点含糊,因为大多数人会认为这是Wilma是Fred打算向全世界提供的接口,而不是仅与Wilma签订的合同。
j_random_hacker

@j_这就是为什么我认为接口是一个坏名字。接口这个术语并不需要像人们想的那样对世界意味着意义,而是功能的保证。实际上,我对程序设计课程中的术语接口存有争议。但是,我们使用所得到的...
偏向

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


BigClass在此示例中是否不需要前向声明?我觉得这很有趣,但是它在我的脸上尖叫着黑涩。
Thomas Eding

2

我发现了一个很好的私有继承应用程序,尽管它的用法很有限。

要解决的问题

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

Cish方法

当然,我们可以选择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不再有任何公共数据成员。它使实现易于正确使用,而难以(不可能?)使用错误。

这些代码片段构成了Coliru上的一个编译示例


1

如果派生类-需要重用代码,并且-您不能更改基类,并且-正在使用锁中的base成员保护其方法。

那么您应该使用私有继承,否则就有通过此派生类导出的未锁定基本方法的危险。


1

有时,它可能是聚合的一种替代方法,例如,如果您想要聚合但可聚合实体的行为发生了变化(覆盖虚拟功能),则可以选择聚合

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


0

当关系不是“是”时使用私有继承,但是新类可以“根据现有类实现”或新类“像”现有类那样实现。

例如,“ Herb Sutter的Andrei Alexandrescu的C ++编码标准”:-考虑到两个类Square和Rectangle各自具有用于设置其高度和宽度的虚函数。然后Square无法正确地从Rectangle继承,因为使用可修改Rectangle的代码将假定SetWidth不更改高度(Rectangle是否显式记录了收缩的内容),而Square :: SetWidth无法保留该收缩及其自身的矩形不变性同时。但是,如果Square的客户假设正方形的面积是其宽度的平方,或者如果他们依赖于Rectangles所不具有的其他属性,则Rectangle不能正确地从Square继承。

正方形“是一个”矩形(数学上),但正方形不是矩形(行为上)。因此,我们宁愿说“类似作品”(或者,如果您更喜欢“按原样使用”),也可以使描述不易被误解。


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不会影响总和)。但是,当没有缓存时,值得对其进行查找。

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


0

如果您需要对a std::ostream进行一些小的更改(例如在此问题中),则可能需要

  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对上述问题的回答中提出的,首先简单地从流缓冲区继承,然后从流继承,然后使用以下方法初始化流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 }
    {}
};

-1

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

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

如果您仍在使用它,那么您基本上是在破坏封装并降低内聚力。您将数据放在一个类中,并添加了对另一类数据进行操作的方法。

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


你在讽刺吗?我仅有的是-1!无论如何,即使它获得了-100票,我也不会删除它
Hasen

9
您基本上违反了封装 ”您可以举个例子吗?
curiousguy

1
一类中的数据和另一类中的行为听起来像灵活性的增加,因为可以有一个以上的行为类和客户,并选择他们需要满足他们想要的行为的客户
makar 2016年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.