与受保护的继承不同,C ++私有继承进入主流C ++开发。但是,我仍然没有找到它的好用。
你们什么时候使用它?
答案 0 :(得分:132)
我一直都在使用它。我脑子里有几个例子:
一个典型的例子是私下从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...
};
答案 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实现方式:
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
不再拥有任何公共数据成员。它使得实现易于正确使用并且很难(不可能?)使用错误。
答案 10 :(得分:1)
如果您需要对std::ostream
进行一些小的更改(例如this question),则可能需要
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'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的回答中所述),但这并不是一个很好的功能。