为什么C++ 11引入了委托构造函数?

Nay*_*iya 23 c++ c++11

我无法理解委托构造函数的用途.简单地说,没有委派构造函数就无法实现的目标?

它可以做这样简单的事情

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;}
};
Run Code Online (Sandbox Code Playgroud)

但我不认为值得为这么简单的事情引入新功能吗?可能是我无法认识到重要的一点.任何的想法?

qua*_*dev 45

委托构造函数可防止代码重复(以及随之而来的所有可能的错误和缺陷:增加维护,降低可读性......),这是一件好事.

它也是委派初始化列表(用于成员和基础初始化)的唯一方法,即您实际上无法通过Init()为构造函数提供共享方法来替换此功能.


例子:

1)N1986提案的通用初始化:

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 */ } 
Run Code Online (Sandbox Code Playgroud)

2)使用构造函数和复制构造函数委派,也来自N1986提案:

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) 
{ 
 // ... 
} 
Run Code Online (Sandbox Code Playgroud)

3) MSDN给出了这个例子,构造函数执行参数验证(如评论所述,这个设计是有争议的):

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;
    }
};
Run Code Online (Sandbox Code Playgroud)

感谢构造函数委派,它简化为:

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;
}
};
Run Code Online (Sandbox Code Playgroud)

链接:

  • 委托构造函数的示例显示,IMO是一种不好的做法:更完整的构造函数执行部分工作并将其他一些工作委托给更简单的构造函数.在支持委托的语言中,长期以来一直被认为是拥有单个完整构造函数的最佳实践,然后让更简单的构造函数委托给它(可能是间接的).这使初始化代码保持在一起并且可读,并且避免使某些委托路径跳过初始化的某些部分.您正在显示MSDN中的示例,但链接的n1986文件显示了更好的示例. (18认同)

How*_*ant 18

除了quantdev的优秀答案(我已经赞成)之外,我还想展示委托构造函数的异常安全问题,这些类型必须在构造函数中显式获取多个资源,并在其析构函数中显式处理多个资源.

作为一个例子,我将使用简单的原始指针.请注意,此示例不是非常激励因为在原始指针上使用智能指针将比委派构造函数更加整洁地解决问题.但这个例子很简单.仍然存在智能指针无法解决的更复杂的示例.

考虑两个类XY,这是正常上课,但我已经来装饰他们与打印报表的特殊成员,所以我们可以看到他们,并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;
};
Run Code Online (Sandbox Code Playgroud)

现在,演示类Z包含一个指向a X和a 的手动管理指针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))
        {}
};
Run Code Online (Sandbox Code Playgroud)

现在的Z(const X& x, const Y& y)构造函数不是例外.展示:

int
main()
{
    try
    {
        Z z{X{}, Y{}};
    }
    catch (...)
    {
    }
}
Run Code Online (Sandbox Code Playgroud)

哪个输出:

X()
Y()
X(const&)
~Y()
~X()
Run Code Online (Sandbox Code Playgroud)

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

示例程序现在正确输出:

X()
Y()
X(const&)
~X()
~Y()
~X()
Run Code Online (Sandbox Code Playgroud)

但是,当您添加托管资源时Z,可以很容易地看到,这很快就会变得很麻烦.委托构造函数可以非常优雅地解决这个问题:

Z(const X& x, const Y& y)
    : Z()
{
    x_ptr = new X(x);
    y_ptr = new Y(y);
}
Run Code Online (Sandbox Code Playgroud)

此构造函数首先委托给默认构造函数,该构造函数除了将类置于有效的无资源状态之外什么都不做.一旦默认构造函数完成,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);
    // ...
};
Run Code Online (Sandbox Code Playgroud)

实现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;
    }
}
Run Code Online (Sandbox Code Playgroud)

但这是我将如何做到这一点:

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);
}
Run Code Online (Sandbox Code Playgroud)

如果代码审查中有人称后者为不良做法,我会在那个问题上进行讨论.


jbm*_*bms 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>{})
    {}
};
Run Code Online (Sandbox Code Playgroud)

通过这种方式,我们可以定义一个构造函数,该构造函数将数组初始化为常量值,而无需首先默认初始化每个元素.据我所知,实现这一目标的唯一方法是将数据成员保留在基类中.

当然,对模板参数包的更好的语言支持可能使这不必要.