2014-10-05 1 views
17

Я не могу понять, что такое использование делегирующих конструкторов. Просто, чего не может быть достигнуто без делегирования конструкторов?Почему 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;} 
}; 

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

+17

Это уменьшает дублирование кода, что само по себе можно считать хорошим. Некоторые инициализации не могут быть выполнены в функциональном теле конструктора, поэтому они не могут быть помещены в функцию-член. Чтобы не повторять их, вам нужно либо использовать базовый класс, либо конструктор делегирования (или инициализаторы нестатических элементов данных, но они не могут использовать параметры конструктора и т. Д.). – dyp

+0

Когда у вас есть несколько конструкторов, вы обычно обнаруживаете, что почти весь код среди них одинаковый.Альтернативой является создание функций-членов инициализации. Я хотел бы знать, как самая бесполезная функция на C++ когда-либо попадала в спецификации: throw для функций. – user3344003

+2

Если у вас есть 10 членов для инициализации и 4 конструктора, это становится болью. Даже добавление, удаление изменения члена означает посещение всех ваших конструкторов и отражение изменений там. Лучше иметь возможность сделать это в одном месте. – Galik

ответ

40

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

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


Примеры:

1) Общие инициализации от 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) делегирования как с конструктором, и конструктор копирования, а также 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, с конструкторами, выполняющих проверку аргументов (как это комментировали, эта конструкция является дискуссионным):

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

Ссылки:

+17

Пример делегирования конструкторов показывает, ИМО, плохая практика: более полные конструкторы выполняют часть работы и делегируют некоторые другие работы более простым конструкторам. В языках, поддерживающих делегирование, уже давно считается лучшей практикой иметь единый, полный конструктор, а затем делегировать ему более простые конструкторы (возможно, косвенно). Это сохраняет код инициализации вместе и читабельна и позволяет избежать определенных путей делегирования некоторой части инициализации. Вы показываете пример из MSDN, но связанная статья n1986 показывает лучшие примеры. – bames53

+0

Возможно, пример Bjarne C++ FAQ также является хорошим примером: [Делегирование конструкторов] (http://www.stroustrup.com/C++11FAQ.html#delegating-ctor). –

+0

@ bames53: true, примеры обновлены. – quantdev

16

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

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

Рассмотрит два класса 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; 
}; 

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

Update

Возможно, более мотивация Например, пользовательский контейнерный класс (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); 
} 

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

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

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

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

2

Я описал другое использование делегирующих конструкторов в Overload #113, что упрощает решения, описанные в статье Complex Logic in the Member Initialiser List от Cassio Neri в Overload # 112.

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

Рассмотрим конструктор, как это:

double some_expensive_calculation(double d); 

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

Мы хотим, чтобы избежать выполнения дорогостоящего вычисления дважды (и в контексте исходной задачи, описанной Кассио, базовый класс также хочет, чтобы результат вычисления, поэтому вы не можете просто назначить 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)) 
{ } 
2

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

Смежные вопросы