我无法理解委托构造函数的用途。简单地说,没有委派构造函数就无法实现的目标?
它可以做这样简单的事情
class M
{
int x, y;
char *p;
public:
M(int v) : x(v), y(0), p(new char [MAX]) {}
M(): M(0) {cout<<"delegating ctor"<<endl;}
};
但我不认为值得为这么简单的事情引入新功能吗?可能是我无法认识到重要的一点。有什么想法吗?
答案 0 :(得分:44)
委托构造函数可以防止代码重复(以及随之而来的所有可能的错误和缺陷:增加维护,降低可读性......),这是一件好事。
这也是委派初始化列表(用于成员和基础初始化)的唯一方法,即您实际上不能通过为构造函数使用共享Init()
方法来替换此功能。
<强>示例:强>
1) N1986 proposal的常见初始化:
class X {
X( int, W& );
Y y_;
Z z_;
public:
X();
X( int );
X( W& );
};
X::X( int i, W& e ) : y_(i), z_(e) { /*Common Init*/ }
X::X() : X( 42, 3.14 ) { SomePostInitialization(); }
X::X( int i ) : X( i, 3.14 ) { OtherPostInitialization(); }
X::X( W& w ) : X( 53, w ) { /* no post-init */ }
2)使用构造函数和复制构造函数的委派,from N1986 proposal:
class FullName {
string firstName_;
string middleName_;
string lastName_;
public:
FullName(string firstName, string middleName, string lastName);
FullName(string firstName, string lastName);
FullName(const FullName& name);
};
FullName::FullName(string firstName, string middleName, string lastName)
: firstName_(firstName), middleName_(middleName), lastName_(lastName)
{
// ...
}
// delegating copy constructor
FullName::FullName(const FullName& name)
: FullName(name.firstName_, name.middleName_, name.lastName_)
{
// ...
}
// delegating constructor
FullName::FullName(string firstName, string lastName)
: FullName(firstName, "", lastName)
{
// ...
}
3) MSDN gives this example,构造函数执行参数验证(如评论所述,此设计存在争议):
class class_c {
public:
int max;
int min;
int middle;
class_c() {}
class_c(int my_max) {
max = my_max > 0 ? my_max : 10;
}
class_c(int my_max, int my_min) {
max = my_max > 0 ? my_max : 10;
min = my_min > 0 && my_min < max ? my_min : 1;
}
class_c(int my_max, int my_min, int my_middle) {
max = my_max > 0 ? my_max : 10;
min = my_min > 0 && my_min < max ? my_min : 1;
middle = my_middle < max && my_middle > min ? my_middle : 5;
}
};
感谢构造函数委托,它简化为:
class class_c {
public:
int max;
int min;
int middle;
class_c(int my_max) {
max = my_max > 0 ? my_max : 10;
}
class_c(int my_max, int my_min) : class_c(my_max) {
min = my_min > 0 && my_min < max ? my_min : 1;
}
class_c(int my_max, int my_min, int my_middle) : class_c (my_max, my_min){
middle = my_middle < max && my_middle > min ? my_middle : 5;
}
};
<强>链接:强>
答案 1 :(得分:18)
除了quantdev的优秀答案(我已经赞成)之外,我还想展示委托构造函数的异常安全问题,这些类型必须在构造函数中显式获取多个资源,并在其析构函数中显式处理多个资源
作为一个例子,我将使用简单的原始指针。请注意,此示例不是非常激励因为在原始指针上使用智能指针将比委派构造函数更加整洁地解决问题。但这个例子很简单。仍然存在更复杂的例子,智能指针无法解决这些问题。
考虑两个类X
和Y
,它们是普通类,除了我用print语句装饰他们的特殊成员以便我们可以看到它们,Y
有一个副本可能抛出的构造函数(在我们的简单示例中,它总是仅出于演示目的而抛出):
#include <iostream>
class X
{
public:
X()
{
std::cout << "X()\n";
}
~X()
{
std::cout << "~X()\n";
}
X(const X&)
{
std::cout << "X(const&)\n";
}
X& operator=(const X&) = delete;
};
class Y
{
public:
Y()
{
std::cout << "Y()\n";
}
~Y()
{
std::cout << "~Y()\n";
}
Y(const Y&)
{
throw 1;
}
Y& operator=(const Y&) = delete;
};
现在,演示类为Z
,其中包含指向X
和Y
的手动管理指针,只是为了创建“多个手动管理的资源。”
class Z
{
X* x_ptr;
Y* y_ptr;
public:
Z()
: x_ptr(nullptr)
, y_ptr(nullptr)
{}
~Z()
{
delete x_ptr;
delete y_ptr;
}
Z(const X& x, const Y& y)
: x_ptr(new X(x))
, y_ptr(new Y(y))
{}
};
现在的Z(const X& x, const Y& y)
构造函数不是异常安全的。为了证明:
int
main()
{
try
{
Z z{X{}, Y{}};
}
catch (...)
{
}
}
输出:
X()
Y()
X(const&)
~Y()
~X()
X
被构造了两次,但只被破坏了一次。有内存泄漏。有几种方法可以使这个构造函数安全,一种方法是:
Z(const X& x, const Y& y)
: x_ptr(new X(x))
, y_ptr(nullptr)
{
try
{
y_ptr = new Y(y);
}
catch (...)
{
delete x_ptr;
throw;
}
}
示例程序现在正确输出:
X()
Y()
X(const&)
~X()
~Y()
~X()
但是,当您将托管资源添加到Z
时,可以很容易地看到,这很快就会变得很麻烦。委托构造函数非常优雅解决了这个问题:
Z(const X& x, const Y& y)
: Z()
{
x_ptr = new X(x);
y_ptr = new Y(y);
}
此构造函数首先委托默认构造函数,该构造函数除了将类置于有效的无资源状态之外什么都不做。一旦默认构造函数完成,Z
现在被认为是完全构造的。因此,如果此构造函数体内的任何内容抛出,~Z()
现在运行(与前面的Z(const X& x, const Y& y)
示例实现不同。~Z()
正确清理已构造的资源(并忽略那些没有)。
如果你必须编写一个在其析构函数中管理多个资源的类,并且出于任何原因你不能使用其他对象来管理这些资源(例如unique_ptr
),我强烈推荐这个成语来管理异常安全
<强>更新强>
也许更具激励性的示例是自定义容器类(std :: lib不提供所有容器)。
您的容器类可能如下所示:
template <class T>
class my_container
{
// ...
public:
~my_container() {clear();}
my_container(); // create empty (resource-less) state
template <class Iterator> my_container(Iterator first, Iterator last);
// ...
};
实现member-template构造函数的一种方法是:
template <class T>
template <class Iterator>
my_container<T>::my_container(Iterator first, Iterator last)
{
// create empty (resource-less) state
// ...
try
{
for (; first != last; ++first)
insert(*first);
}
catch (...)
{
clear();
throw;
}
}
但是我会这样做:
template <class T>
template <class Iterator>
my_container<T>::my_container(Iterator first, Iterator last)
: my_container() // create empty (resource-less) state
{
for (; first != last; ++first)
insert(*first);
}
如果代码审查中有人称后者为不良做法,我会去找那个。
答案 2 :(得分:6)
委托不仅仅减少代码重复的构造函数的一个关键用途是获取指定成员初始化程序所需的其他模板参数包,特别是一系列整数索引:
例如:
struct constant_t;
template <class T, size_t N>
struct Array {
T data[N];
template <size_t... Is>
constexpr Array(constant_t, T const &value, std::index_sequence<Is...>)
: data { (Is,value)... }
{}
constexpr Array(constant_t, T const &value)
: Array(constant_t{}, value, std::make_index_sequence<N>{})
{}
};
通过这种方式,我们可以定义一个构造函数,该构造函数将数组初始化为常量值,而无需首先默认初始化每个元素。据我所知,实现这一目标的唯一方法是将数据成员保留在基类中。
当然,对模板参数包的更好的语言支持可能会使这不必要。
答案 3 :(得分:2)
我在Overload #113中描述了委托构造函数的另一种用法,它简化了Cassio Neri在过载#112中Complex Logic in the Member Initialiser List中描述的解决方案。
与函数体内的代码不同,当您编写构造函数的成员初始化器时,您无法创建局部变量来存储多个成员所需的中间结果。
考虑这样的构造函数:
double some_expensive_calculation(double d);
bar::bar(double d)
: x_(cos(some_expensive_calculation(d))), y_(sin(some_expensive_calculation(d)))
{ }
我们希望避免执行两次昂贵的计算(并且在Cassio描述的原始问题的上下文中,基类也需要计算结果,因此您不能简单地分配给x_
和{{ 1}}在构造函数体中。)
我描述的技巧是计算中间结果并委托给另一个使用该结果的构造函数:
y_
答案 4 :(得分:2)
在我看来,值得一提的是,偶尔会建议通过将公共代码重构为私有init函数来缓解多个构造函数之间的代码重复。问题在于,如果班级有朋友,那些朋友可以多次调用init - 并且不应该多次调用它。委托构造函数可以防止这样的问题,因为构造函数在对象初始化之后无法运行。