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

Why did C++11 introduce delegating constructors?

本文关键字:构造函数 C++11 为什么      更新时间:2023-10-16

我不明白委托构造函数有什么用。简单地说,没有委派构造函数就无法实现什么?

它可以做这样简单的事情

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;}
};

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

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

这也是委托初始化列表(用于成员和基初始化(的唯一方法,即您确实无法通过为构造函数使用共享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 */ } 

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) 
{ 
 // ... 
} 

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;
    }
};

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

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;
}
};

链接:

  • 委派构造函数 (r3( 提案 - N1986
  • 斯特
  • 劳斯特鲁普C++常见问题解答:委派构造函数

除了 quantdev 的出色答案(我已经投了赞成票(之外,我还想演示那些必须在构造函数中显式获取多个资源并在其析构函数中显式释放多个资源的类型的委托构造函数的异常安全问题。

例如,我将使用简单的原始指针。 请注意,此示例不是很有动力,因为使用智能指针而不是原始指针将比委托构造函数更巧妙地解决问题。 但例子很简单。 仍然存在更复杂的示例,这些示例无法通过智能指针解决。

考虑两个类 XY ,它们是普通类,除了我用 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);
    // ...
};

实现成员模板构造函数的一种方法是:

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);
}

如果代码审查中有人称后者为不良做法,我会去那个垫子上

委派构造函数的一个关键用途不仅仅是减少代码重复,而是获取指定成员初始值设定项所需的其他模板参数包,特别是整数索引序列:

例如:

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>{})
    {}
};

通过这种方式,我们可以定义一个构造函数,将数组初始化为常量值,而无需先默认初始化每个元素。 据我所知,实现此目的的唯一其他方法需要将数据成员粘贴在基类中。

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

我在重

载 #113 中描述了委托构造函数的另一种用法,它简化了重载 #112 中成员初始化器列表中的 Cassio Neri 复杂逻辑中描述的解决方案。

与函数体内的代码不同,在编写构造函数的成员初始化器时,不能创建局部变量来存储多个成员所需的中间结果。

考虑这样的构造函数:

double some_expensive_calculation(double d);
bar::bar(double d)
: x_(cos(some_expensive_calculation(d))), y_(sin(some_expensive_calculation(d)))
{ }

我们希望避免两次执行昂贵的计算(在 Cassio 描述的原始问题的上下文中,基类也需要计算结果,因此您不能简单地分配给构造函数体中的x_y_(。

我描述的技巧是计算中间结果并委托给使用该结果的另一个构造函数:

class bar {
  struct tag { };
  ...
  bar(double result, tag);
public:
  bar(double d);
};
bar::bar(double d)
: bar(some_expensive_calculation(d), tag{})
{ }
bar::bar(double result, tag)
: x_(cos(result)), y_(sin(result))
{ }

在我看来,值得一提的是,偶尔有人建议通过将公共代码重构为私有 init 函数来缓解多个构造函数之间的代码重复。这样做的问题是,如果类有朋友,这些朋友可以多次调用 init - 它不应该被多次调用。委托构造函数可以防止此类问题,因为构造函数在对象初始化后无法运行。