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

与受保护继承不同,C + + 私有继承进入了 C + + 开发的主流。然而,我仍然没有找到一个很好的用途。

你们什么时候用?

52331 次浏览

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

有时候它可以替代 聚合,例如,如果您想要聚合,但是需要改变可聚合实体的行为(覆盖虚函数)。

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

我发现它对接口(即抽象类)非常有用,因为我不希望其他代码接触接口(只接触继承类)。

[在一个例子中编辑]

例子连接到上面,说那个

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

也就是说 Wilma 要求 Fred 能够调用某些成员函数,或者说,它说的是 薇玛只是一个接口。因此,正如示例中所提到的

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

对程序员需要满足我们的接口要求或破坏代码的期望效果的评论。而且,因为 fredCallsWilma ()是受保护的,所以只有好友和派生类可以接触它,即只有继承类可以接触(和好友)的继承接口(抽象类)。

[在另一个例子中编辑]

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

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

私有继承的一个合法的长期用途是,当您想要构建一个类 Fred 时,它使用 Wilma 类中的代码,而 Wilma 类中的代码需要从您的新类 Fred 中调用成员函数。在这种情况下,弗雷德在威尔马调用非虚拟,而威尔玛调用(通常是纯虚拟)本身,这被弗雷德覆盖。这对于作曲来说要困难得多。

如果有疑问,您应该选择组合而不是私有继承。

答复接受后注意事项: 这不是一个完整的答案。如果你对这个问题感兴趣,可以阅读其他的回答,比如: 如果你对这个问题感兴趣,你可以在这里阅读这个问题的理论和实践。这只是一个可以通过私有继承实现的花哨把戏。趁现在喜欢 这不是问题的答案。

除了在 C + + 常见问题解答中显示的私有继承的基本用法(在其他人的评论中有链接) ,你还可以结合私有继承和虚继承继承来使用 海豹突击队 a 类(在。NET 术语)或者创建一个类 期末考试(Java 术语)。这不是一个常见的用法,但不管怎样,我发现它很有趣:

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

密封的 可以实例化,它从 班级封印者派生,可以直接调用私有构造函数,因为它是朋友。

FaillsToDerive 不能编译,因为它必须直接调用 班级封印者构造函数(虚继承要求) ,但是它不能编译,因为它在 密封的类中是私有的,在这种情况下,失败之源不是 班级封印者的朋友。


剪辑

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

仅仅因为 C + + 有一个特性,并不意味着它是有用的或者它应该被使用。

我觉得你根本不该用它。

如果无论如何都要使用它,那么基本上就违反了封装,降低了内聚力。您将数据放在一个类中,并添加在另一个类中操作数据的方法。

像其他 C + + 特性一样,它可以用来实现一些副作用,比如封装一个类(正如德里贝亚的回答中所提到的) ,但这并不意味着它是一个好特性。

我一直在使用它,我脑海中浮现了几个例子:

  • 当我想公开一些但不是全部的基类接口时。公共继承将是一个谎言,因为 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...
};
  • 在实现 Adapter 模式时,从 Adapted 类私有继承可以节省转发到封闭实例的时间。
  • 实现私有接口。观察者模式经常会出现这种情况。典型的我的观察者类,比如说,带有一些主题的 订阅自己。然后,只有 MyClass 需要进行 MyClass-> Observer 转换。系统的其余部分不需要知道它,因此指定了私有继承。

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

例如:

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

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

class BigClass;


struct SomeCollection
{
iterator begin();
iterator end();
};


class BigClass : private SomeCollection
{
friend struct SomeCollection;
SomeCollection &GetThings() { return *this; }
};

然后,如果 SomCollection 需要访问 BigClass,它可以使用 static_cast<BigClass *>(this)。不需要额外的数据成员占用空间。

如果派生类 需要重用代码和 你不能改变基础课 正在使用基地的成员在一个锁下保护它的方法。

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

当关系不是“ is a”时使用私有继承,但 New 类可以“根据现有类实现”或新类“像”现有类“工作”。

例如“ Andrei Alexandrescu 的 c + + 编码标准,Herb Sutter”:- 考虑到 Square 和 Recangle 两个类都有用于设置其高度和宽度的虚函数。然后 Square 不能正确地继承矩形,因为使用可修改的矩形的代码将假定 SetWidth 不会改变高度(不管矩形是否显式地记录了该契约) ,而 Square: : SetWidth 不能同时保留该契约和它自己的平方不变量。但是,如果 Square 的客户端假设 Square 的面积是其宽度的平方,或者如果它们依赖于矩形不适用的某些其他属性,那么 Recangle 也不能正确地从 Square 继承。

一个正方形“是-a”矩形(数学上) ,但是一个正方形不是一个矩形(行为上)。因此,我们更愿意使用“ works-like-a”(或者,如果您喜欢的话,使用可用作 a)来代替“ is-a”,以减少描述的误解。

类包含一个不变量。不变量由构造函数建立。但是,在许多情况下,拥有对象表示状态的视图是有用的(如果愿意,可以通过网络传输或保存到文件 DTO 中)。REST 最好使用 AgregateType 来完成。如果你的常量是正确的,这一点尤其正确。考虑一下:

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

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

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

需要解决的问题

假设给出了以下 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 式实现风格:

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的 setter,而直接分配给 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就不再有任何公共数据成员了。它使得实现易于正确使用和困难(不可能?)使用错误。

代码片段形成 在科利鲁上编译例子

如果您需要一个 std::ostream与一些小的变化(如在 这个问题) ,您可能需要

  1. 创建一个从 std::streambuf派生的类 MyStreambuf,并在那里实现更改
  2. 创建一个从 std::ostream派生的类 MyOStream,它也初始化和管理 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

该解决方案是在 本对上述问题的回答中提出的,只需先从流缓冲区继承,然后从流继承,然后用 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 }
{}
};