Why did C++11 introduce delegating constructors?

2020-02-19 02:48发布

I cannot understand what the use is of delegating constructors. Simply, what cannot be achieve without having delegating constructors?

It can do something simple like this

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

But I don't see it is worth introduce a new feature for such a simple thing? May be I couldn't recognize the important point. Any idea?

标签: c++ c++11
5条回答
Deceive 欺骗
2楼-- · 2020-02-19 03:20

One key use of delegating constructors that doesn't merely reduce code duplication is to obtain additional template parameter packs, particular a sequence of integer indices, needed to specify a member initializer:

For example:

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

In this way we can define a constructor that initializes the array to a constant value without first default initializing each element. The only other way to achieve this, as far as I know, would require sticking the data member in a base class.

Of course, better language support for template parameter packs might make this unnecessary.

查看更多
forever°为你锁心
3楼-- · 2020-02-19 03:27

I described another use of delegating constructors in Overload #113 which simplifies the solutions described in Cassio Neri's Complex Logic in the Member Initialiser List in Overload #112.

Unlike code inside a function body, when you are writing a constructor's member initialisers you cannot create a local variable to store an intermediate result that is needed by more than one of the members.

Consider a constructor like this:

double some_expensive_calculation(double d);

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

We want to avoid performing the expensive calculation twice (and in the context of the original problem described by Cassio, a base class also wants the result of the calculation, so you cannot simply assign to x_ and y_ in the constructor body).

The trick I described is to calculate the intermediate result and delegate to another constructor that uses that result:

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))
{ }
查看更多
beautiful°
4楼-- · 2020-02-19 03:35

It seems to me that it's worth mentioning that it has occasionally been suggested that the code duplication between multiple constructors can be alleviated by refactoring the common code into a private init-function. The problem with that is that if the class has friends, those friends can invoke the init multiple times - and it's not supposed to be invoked multiple times. Delegating constructors prevent such problems by the virtue that constructors cannot run after the object has been initialized already.

查看更多
家丑人穷心不美
5楼-- · 2020-02-19 03:41

In addition to quantdev's excellent answer (which I have upvoted), I wanted to also demonstrate the exception safety issues of delegating constructors for those types which must explicitly acquire multiple resources in a constructor, and explicitly dispose of multiple resources in its destructor.

As an example, I will use simple raw pointers. Note that this example is not very motivating because the use of smart pointers over raw pointers will solve the problem more neatly than delegating constructors. But the example is simple. There still exists more complex examples that are not solved by smart pointers.

Consider two classes X and Y, which are normal classes, except that I've decorated their special members with print statements so we can see them, and Y has a copy constructor that might throw (in our simple example it always throws just for demonstration purposes):

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

Now the demo class is Z which holds a manually managed pointer to an X and a Y, just to create "multiple manually managed resources."

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

The Z(const X& x, const Y& y) constructor as it stands is not exception safe. To demonstrate:

int
main()
{
    try
    {
        Z z{X{}, Y{}};
    }
    catch (...)
    {
    }
}

which outputs:

X()
Y()
X(const&)
~Y()
~X()

X got constructed twice, but destructed only once. There is a memory leak. There are several ways to make this constructor safe, one way is:

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

The example program now correctly outputs:

X()
Y()
X(const&)
~X()
~Y()
~X()

However one can easily see that as you add managed resources to Z, this quickly gets cumbersome. This problem is solved very elegantly by delegating constructors:

Z(const X& x, const Y& y)
    : Z()
{
    x_ptr = new X(x);
    y_ptr = new Y(y);
}

This constructor first delegates to the default constructor which does nothing but put the class into a valid, resource-less state. Once the default constructor completes, Z is now considered fully constructed. So if anything in the body of this constructor throws, ~Z() now runs (unlike the previous example implementations of Z(const X& x, const Y& y). And ~Z() correctly cleans up resources that have already been constructed (and ignores those that haven't).

If you have to write a class that manages multiple resources in its destructor, and for whatever reasons you can't use other objects to manage those resources (e.g. unique_ptr), I highly recommend this idiom to manage exception safety.

Update

Perhaps a more motivating example is a custom container class (the std::lib doesn't supply all containers).

Your container class might look like:

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

One way to implement the member-template constructor is:

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

But here is how I would do it:

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

If someone in code review called the latter bad practice, I would go to the mat on that one.

查看更多
老娘就宠你
6楼-- · 2020-02-19 03:46

Delegating constructors prevent code duplication (and all the possible errors and flaws that come with it : increased maintenance, decreased readability...), which is a good thing.

It is also the only way to delegate the initialization list (for members and bases initializations), i.e. you really can't replace this feature by having a shared Init() method for your constructors.


Examples:

1) Common initialization from 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) Delegation with both constructor and copy constructor, also 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, with constructors performing argument validation (as commented, this design is debatable) :

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

Thanks to constructors delegation, it reduces to :

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

Links:

查看更多
登录 后发表回答