2014-10-05 15 views
17

Nie mogę zrozumieć, jaki jest pożytek z delegowania konstruktorów. Po prostu, czego nie można osiągnąć bez delegowania konstruktorów?Dlaczego C++ 11 wprowadził delegujące konstruktory?

To może zrobić coś prostego, jak to

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

Ale nie widzę warto wprowadzić nową funkcję o takiej prostej rzeczy? Być może nie mogłem rozpoznać ważnej kwestii. Dowolny pomysł?

+17

Zmniejsza duplikację kodu, co samo w sobie może być uznane za dobrą rzecz. Niektórych inicjalizacji nie można wykonać w ciele funkcji konstruktora, więc nie można ich wstawić do funkcji składowej. Aby tego nie powtarzać, musisz użyć klasy bazowej lub konstruktora delegującego (lub niestatycznego inicjalizatora elementów danych, ale nie mogą używać parametrów konstruktora, itp.). – dyp

+0

Kiedy masz wielu konstruktorów, zazwyczaj okazuje się, że prawie cały kod jest taki sam.Alternatywą jest utworzenie funkcji członka inicjalizacji. Chciałbym wiedzieć, jak najbardziej bezużyteczna funkcja w C++ kiedykolwiek znalazła się w specyfikacji: rzuć specyfikacje dla funkcji. – user3344003

+2

Jeśli masz 10 członków do zainicjowania i 4 konstruktorów, to staje się to problemem. Nawet dodanie, usunięcie zmiany członka oznacza odwiedzenie wszystkich twoich konstruktorów i odzwierciedlenie tamtej zmiany. Lepiej być w stanie to zrobić w jednym miejscu. – Galik

Odpowiedz

40

Delegowanie konstruktorów zapobiega duplikacji kodu (i wszystkim możliwym błędom i błędom, które mu towarzyszą: zwiększonej konserwacji, zmniejszonej czytelności ...), co jest dobre.

Jest to również jedyny sposób na delegowanie listy inicjalizacyjnej (dla członków i baz inicjalizacyjnych), tj. Naprawdę nie można zastąpić tej funkcji przez udostępnienie metody Init() dla konstruktorów.


Przykłady:

1) wspólne startowe z 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) przekazanie ze zarówno konstruktora kopiującym również 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 z konstruktorów wykonujących walidacji argument (jak zauważyli, ten projekt jest dyskusyjna):

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

Dzięki konstruktorów delegacji, to sprowadza się do:

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

Linki:

+17

Przykład delegowania konstruktorów show, IMO, zła praktyka: bardziej kompletne konstruktory wykonujące część pracy i delegujące inne prace do prostszych konstruktorów. W językach, które obsługują delegowanie, od dawna uważano, że najlepszą praktyką jest posiadanie pojedynczego, kompletnego konstruktora, a następnie przekazywanie mu prostszych konstruktorów (być może pośrednio). Dzięki temu kod inicjujący jest ze sobą połączony i czytelny, dzięki czemu unika się pewnych ścieżek delegowania, które pomijają część inicjalizacji. Pokazujesz przykład z MSDN, ale połączony papier n1986 pokazuje lepsze przykłady. – bames53

+0

Być może przykład Bjarne C++ FAQ jest również dobrym przykładem: [Delegowanie konstruktorów] (http://www.stroustrup.com/C++11FAQ.html#delegating-ctor). –

+0

@ bames53: true, przykłady zaktualizowane. – quantdev

16

Poza doskonałą odpowiedź quantdev (który mam upvoted), chciałem również wykazać kwestie bezpieczeństwa wyjątek delegowania konstruktorów dla tych typów, które musi wyraźnie nabyć wiele zasobów w konstruktorze i jawnie pozbywa się wielu zasobów w swoim destruktorze.

Jako przykład wykorzystam proste wskaźniki surowe. Zauważ, że ten przykład nie jest zbyt motywujący, ponieważ użycie inteligentnych wskaźników zamiast surowych wskaźników rozwiąże problem bardziej starannie niż delegowanie konstruktorów. Ale przykład jest prosty. Nadal istnieją bardziej złożone przykłady, które nie są rozwiązywane za pomocą inteligentnych wskaźników.

Rozważmy dwie klasy X i Y, które są normalne zajęcia, oprócz tego, że mam urządzone swoje specjalne członkom sprawozdania drukujących, dzięki czemu możemy je zobaczyć, a Y ma konstruktora kopii, które mogłyby rzucić (w naszym prostym przykładzie zawsze rzuca tylko w celach demonstracyjnych):

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

teraz klasa demo jest Z który posiada ręcznie zarządzanego wskaźnik do X i Y, żeby stworzyć „wiele ręcznie zarządzanych zasobów.”

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

Konstruktor Z(const X& x, const Y& y) w swoim stanie nie jest wyjątkiem wyjątkowym. Aby wykazać, że:

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

która wyprowadza:

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

X został zbudowany dwa razy, ale zniszczona tylko raz. Występuje przeciek pamięci. Istnieje kilka sposobów, aby to konstruktor bezpieczne, jednym ze sposobów jest:

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

Przykładowy program teraz poprawnie wyświetla:

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

Jednak można łatwo zobaczyć, że w miarę dodawania udało zasobów Z, tym szybko staje się nieporęczna. Ten problem został rozwiązany bardzo elegancko delegując konstruktorów:

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

Ten konstruktor pierwszych delegatów do konstruktora domyślnego, który nie robi nic poza umieścić klasę na ważnej, stanu zasobów mniej. Po zakończeniu domyślnego konstruktora, Z jest teraz uważany za w pełni skonstruowany. Więc jeśli coś w ciele tego konstruktora rzuty, ~Z() teraz działa (w przeciwieństwie do poprzedniego przykładu implementacji Z(const X& x, const Y& y). I ~Z() prawidłowo sprząta zasobów, które zostały już zbudowane (i pomija te, które nie mają).

Jeśli trzeba napisać klasę, która zarządza wielu zasobów w destructor oraz z jakichkolwiek przyczyn nie można korzystać z innych obiektów do zarządzania tymi zasobami (np unique_ptr) Gorąco polecam ten idiom do zarządzania bezpieczeństwem wyjątku.

Aktualizacja

Być może bardziej motywacyjny Przykładem jest niestandardowa klasa kontenera (std :: lib nie dostarcza wszystkich kontenerów).

Twoja klasa pojemnik może wyglądać następująco:

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

Jednym ze sposobów realizacji konstruktor członkiem-szablonu jest:

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

Ale o to, w jaki sposób to zrobić:

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

Jeśli ktoś w przeglądzie kodu nazwał tę drugą złą praktykę, poszłabym na matę.

6

Jeden klucz stosowanie delegowania konstruktorów, które nie tylko zmniejszają powielania kodu jest uzyskanie dodatkowych pakietów parametr szablonu szczególną sekwencję indeksów całkowitą, potrzebne do określenia inicjator użytkownika:

na przykład:

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

W ten sposób możemy zdefiniować konstruktor, który inicjuje tablicę na stałą wartość bez domyślnego inicjowania każdego elementu. Jedyny inny sposób na osiągnięcie tego, o ile mi wiadomo, wymagałby przyklejenia elementu danych do klasy bazowej.

Oczywiście lepsza obsługa językowa pakietów parametrów szablonów może sprawić, że nie będzie to konieczne.

2

Opisałem inne użycie delegujących konstruktorów w Overload #113, co upraszcza rozwiązania opisane w Cassio Neri'a w Complex Logic in the Member Initialiser List w Przeciążeniu # 112.

W przeciwieństwie do kodu wewnątrz treści funkcji, podczas pisania inicjalizatorów elementu konstruktora nie można utworzyć zmiennej lokalnej do przechowywania wyniku pośredniego wymaganego przez więcej niż jeden element.

Rozważmy konstruktor tak:

double some_expensive_calculation(double d); 

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

Chcemy uniknąć wykonywania kosztownych obliczeń dwukrotnie (w kontekście pierwotnego problemu opisanego przez Cassio, klasa bazowa chce również wynik obliczeń, więc nie można po prostu przypisać do x_ i y_ w treści konstruktora).

Sztuką opisałem to w celu obliczenia wyniku pośredniego i przekazać do innego konstruktora, który używa tego rezultatu:

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

Wydaje mi się, że warto wspomnieć, że od czasu do czasu sugerowano, że powielanie kodu pomiędzy wiele konstruktorów może zostać złagodzonych przez refaktoryzację wspólnego kodu w prywatną funkcję inicjującą. Problem polega na tym, że jeśli klasa ma przyjaciół, ci przyjaciele mogą wielokrotnie wywoływać init - i nie powinno się go uruchamiać wiele razy. Delegowanie konstruktorów zapobiega takim problemom z powodu tego, że konstruktory nie mogą być uruchamiane po tym, jak obiekt został już zainicjowany.