与受保护的继承不同,C++私有继承已经被引入到主流C++开发中。然而,我仍然没有找到一个好的用途。
你们什么时候使用它呢?
我经常使用它。以下是我的几个举例:
一个典型的示例是私有地从 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...
};
除了C++ FAQ中显示的仅使用私有继承的基本用法之外,您还可以使用私有和虚拟继承的组合来在.NET术语中“封装”一个类或在Java术语中使一个类“final”。这不是常见的用法,但我觉得它很有趣:
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不是ClassSealer的友元。
template <typename T>
class Seal {
friend T; // not: friend class T!!!
Seal() {}
};
class Sealed : private virtual Seal<Sealed> // ...
final
,正好可以满足这个目的。class Sealed final // ...
私有继承的典型用法是“基于实现”关系(感谢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的私有方法,而其他外部类则不能。这是处理特定回调的优秀模式,这些回调被定义为接口。FooUser ff; FooInterface *i = new FooImplementer(ff);
但是出现了编译错误 - error: ‘FooInterface’ is an inaccessible base of ‘FooImplementer’. 我不明白这是什么意思。你能帮忙解决吗? - awakenedFooImplementer
赋值给 FooInterface
,因为它使用了私有继承。你的 FooImplementer
类需要有一个静态工厂函数来创建 FooImplementer
,但是通过 FooInteface
指针或返回 FooInterface*
的成员函数返回它。可以使用 return this;
。 - Dominik Grabiec我认为来自C++ FAQ Lite的关键部分是:
私有继承的一个合法、长期的用途是当你想要构建一个类Fred,它使用类Wilma中的代码,并且来自类Wilma的代码需要调用你的新类Fred的成员函数。在这种情况下,Fred调用Wilma中的非虚函数,而Wilma则调用(通常是纯虚函数)自身中的函数,这些函数被Fred覆盖。使用组合将使此过程更加困难。
如果不确定,您应该优先选择组合而不是私有继承。
我发现对于我要继承的接口(即抽象类),它非常有用,因为我不希望其他代码触及接口(只能触及继承类)。
[在示例中进行编辑]
以上面链接的示例为例。说
[...]类Wilma需要从您的新类Fred调用成员函数。
这意味着Wilma需要调用某些成员函数,或者更确切地说,它是在说Wilma是一个接口。因此,正如示例中提到的那样
私有继承并不邪恶;它只是更昂贵的维护方式,因为它增加了某个人会更改某些将破坏您代码的东西的概率。
对需要满足我们接口要求的程序员达到期望效果或破坏代码进行了评论。而且,由于fredCallsWilma()只保护朋友和派生类才能触及,即仅继承类可以触及的继承接口(抽象类)(和朋友)。
[在另一个示例中进行编辑]
这个页面简要讨论了私有接口(从另一个角度)。
#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
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;
}
我们可以使用 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
不再有任何公共数据成员。它使得实现容易正确使用,难以(或不可能)错误使用。std::ostream
进行一些小的更改(例如这个问题),则需要:
std::streambuf
派生的类MyStreambuf
,并在其中实现更改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
实例,这是未定义的行为。
解决方案在Ben的答案中提出,只需先从流缓冲区继承,然后从流继承,然后使用this
初始化流:
class MyOStream : public MyStreamBuf, public std::ostream
{
public:
MyOStream()
: MyStreamBuf{}
, basic_ostream{ this }
{}
};
class MyOStream : private MyStreamBuf, public std::ostream
{
public:
MyOStream()
: MyStreamBuf{}
, basic_ostream{ this }
{}
};
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
的前向声明,是吗?我觉得这很有趣,但它让我感觉像是一个hackish。 - Thomas Eding
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
的重载!)。 - David Stone