与受保护的继承不同,C ++私有继承已进入主流C ++开发。但是,我仍然没有找到很好的用处。
你们什么时候使用它?
与受保护的继承不同,C ++私有继承已进入主流C ++开发。但是,我仍然没有找到很好的用处。
你们什么时候使用它?
Answers:
接受答案后的注意事项:这不是完整的答案。阅读其他答案喜欢这里(概念),并在这里(包括理论和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 // ...
我用它所有的时间。举几个例子:
一个典型的示例是从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...
};
push_back
,则MyVector
可以免费获得它们。
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
过载!)。
私有继承的规范用法是“按关系实现”(这要感谢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()受保护,只有朋友和派生类可以触摸它,即只有继承类可以触摸(和朋友)的继承接口(抽象类)。
[在另一个示例中编辑]
本页简要讨论了私有接口(从另一个角度)。
有时,当我想在另一个接口中公开较小的接口(例如集合)时,使用私有继承很有用,在该接口中,集合的实现需要访问暴露类的状态,类似于在内部类中的方式。 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
在此示例中是否不需要前向声明?我觉得这很有趣,但是它在我的脸上尖叫着黑涩。
我发现了一个很好的私有继承应用程序,尽管它的用法很有限。
假设您获得以下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实现样式,如下所示:
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 ++,那么为什么不使用它的全部功能呢?
以上问题基本上都与手动资源管理有关。想到的解决方案是从每个变量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
不再有任何公共数据成员。它使实现易于正确使用,而难以(不可能?)使用错误。
如果派生类-需要重用代码,并且-您不能更改基类,并且-正在使用锁中的base成员保护其方法。
那么您应该使用私有继承,否则就有通过此派生类导出的未锁定基本方法的危险。
当关系不是“是”时使用私有继承,但是新类可以“根据现有类实现”或新类“像”现有类那样实现。
例如,“ Herb Sutter的Andrei Alexandrescu的C ++编码标准”:-考虑到两个类Square和Rectangle各自具有用于设置其高度和宽度的虚函数。然后Square无法正确地从Rectangle继承,因为使用可修改Rectangle的代码将假定SetWidth不更改高度(Rectangle是否显式记录了收缩的内容),而Square :: SetWidth无法保留该收缩及其自身的矩形不变性同时。但是,如果Square的客户假设正方形的面积是其宽度的平方,或者如果他们依赖于Rectangles所不具有的其他属性,则Rectangle不能正确地从Square继承。
正方形“是一个”矩形(数学上),但正方形不是矩形(行为上)。因此,我们宁愿说“类似作品”(或者,如果您更喜欢“按原样使用”),也可以使描述不易被误解。
一个类拥有一个不变式。不变式由构造函数建立。但是,在许多情况下,查看对象的表示状态很有用(可以通过网络传输或保存到文件-如果需要,可以保存到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不会影响总和)。但是,当没有缓存时,值得对其进行查找。
私有继承几乎总是可以由成员建模(如果需要,可以存储对基础的引用)。以这种方式建模并不总是值得的;有时继承是最有效的表示。
如果您需要对a std::ostream
进行一些小的更改(例如在此问题中),则可能需要
MyStreambuf
派生自std::streambuf
并在那里执行更改的类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 }
{}
};
仅仅因为C ++具有功能,并不意味着它有用或应该使用。
我想说你根本不应该使用它。
如果您仍在使用它,那么您基本上是在破坏封装并降低内聚力。您将数据放在一个类中,并添加了对另一类数据进行操作的方法。
像其他C ++功能一样,它可用于实现诸如密封类之类的副作用(如dribeas的回答中所述),但这并不是一个好的功能。