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ę.
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
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
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