Почему С++ 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;}
};

Но я не вижу, что стоит добавить новую функцию для такой простой вещи? Может быть, я не мог распознать важный момент. Любая идея?

Ответ 1

Делегирующие конструкторы предотвращают дублирование кода (и все возможные ошибки и недостатки, которые приходят с ним: увеличение обслуживания, снижение удобочитаемости...), что хорошо.

Это также единственный способ делегировать список инициализации (для инициализации членов и баз), т.е. вы действительно не можете заменить эту функцию с помощью общего метода 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;
}
};

Ссылки:

Ответ 2

В дополнение к превосходному ответу на квант (который у меня есть выше) я хотел также продемонстрировать проблемы безопасности исключений делегирования конструкторов для тех типов, которые должны явно получать несколько ресурсов в конструкторе и явно распоряжаться несколькими ресурсами в своем деструкторе.

В качестве примера я буду использовать простые исходные указатели. Обратите внимание, что этот пример не очень мотивирован, потому что использование интеллектуальных указателей над необработанными указателями решает проблему более аккуратно, чем делегирование конструкторов. Но пример прост. Все еще существуют более сложные примеры, которые не решаются с помощью интеллектуальных указателей.

Рассмотрим два класса X и Y, которые являются нормальными классами, за исключением того, что я украсил их специальные элементы операторами печати, чтобы мы могли их видеть, а 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;
};

Теперь класс demo 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);
}

Если кто-то в обзоре кода назвал последнюю плохую практику, я бы пошел на коврик на этом.

Ответ 3

Одним из ключевых вариантов делегирования конструкторов, который не просто уменьшает дублирование кода, является получение дополнительных пакетов параметров шаблона, в частности последовательность целых индексов, необходимая для указания инициализатора элемента:

Например:

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

Таким образом, мы можем определить конструктор, который инициализирует массив постоянным значением без первоначальной инициализации каждого элемента по умолчанию. Единственный способ добиться этого, насколько мне известно, потребует приклеивания элемента данных в базовом классе.

Конечно, лучшая поддержка языков для пакетов параметров шаблонов может сделать это ненужным.

Ответ 4

Я описал другое использование делегирующих конструкторов в Overload # 113, которое упрощает решения, описанные в Cassio Neri Сложная логика в списке инициализаторов участников в режиме перегрузки # 112.

В отличие от кода внутри тела функции, когда вы пишете инициализаторы-члены конструктора, вы не можете создать локальную переменную для хранения промежуточного результата, который необходим более чем одному из членов.

Рассмотрим конструктор, подобный этому:

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

Ответ 5

Мне кажется, что стоит упомянуть, что иногда высказывалось предположение о том, что дублирование кода между несколькими конструкторами можно облегчить путем реорганизации общего кода в частную init-функцию. Проблема в том, что если у класса есть друзья, эти друзья могут вызывать init несколько раз - и он не должен вызываться несколько раз. Делегирующие конструкторы предотвращают такие проблемы благодаря тому, что конструкторы не могут работать после того, как объект уже инициализирован.