Why did C++11 introduce delegating constructors?

前端 未结 5 1015
小鲜肉
小鲜肉 2020-12-24 08:34

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

It can do something simple li

5条回答
  •  星月不相逢
    2020-12-24 08:59

    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 
    
    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 my_container
    {
        // ...
    public:
        ~my_container() {clear();}
        my_container();  // create empty (resource-less) state
        template  my_container(Iterator first, Iterator last);
        // ...
    };
    

    One way to implement the member-template constructor is:

    template 
    template 
    my_container::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 
    template 
    my_container::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.

提交回复
热议问题