Jak obsługiwać przypadki awarii w konstruktorze klasy C ++?

21

Mam klasę CPP, której konstruktor wykonuje pewne operacje. Niektóre z tych operacji mogą się nie powieść. Wiem, że konstruktorzy nic nie zwracają.

Moje pytania są

  1. Czy wolno wykonywać inne operacje niż inicjowanie elementów w konstruktorze?

  2. Czy można powiedzieć funkcji wywołującej, że niektóre operacje w konstruktorze nie powiodły się?

  3. Czy mogę new ClassName()ustawić wartość NULL, jeśli wystąpią jakieś błędy w konstruktorze?

MayurK
źródło
22
Możesz zgłosić wyjątek z poziomu konstruktora. To całkowicie poprawny wzór.
Andy,
1
Prawdopodobnie powinieneś rzucić okiem na niektóre wzorce kreacji GoF . Polecam wzór fabryczny.
SpaceTrucker
2
Typowym przykładem nr 1 jest sprawdzanie poprawności danych. IE, jeśli masz klasę Squarez konstruktorem, który pobiera jeden parametr, długość boku, chcesz sprawdzić, czy ta wartość jest większa niż 0.
David mówi Przywróć Monikę
1
W przypadku pierwszego pytania pozwólcie, że ostrzeżę, że funkcje wirtualne mogą zachowywać się w konstruktorach nieumyślnie. To samo z dekonstruktorami. Uważaj na takie połączenia.
1
# 3 - Dlaczego chcesz zwrócić NULL? Jedną z zalet OO NIE jest sprawdzanie zwracanych wartości. Wystarczy złapać () odpowiednie potencjalne wyjątki.
MrWonderful

Odpowiedzi:

42
  1. Tak, choć niektóre standardy kodowania mogą tego zabraniać.

  2. Tak. Zalecanym sposobem jest zgłoszenie wyjątku. Alternatywnie możesz przechowywać informacje o błędach wewnątrz obiektu i podać metody dostępu do tych informacji.

  3. Nie.

Sebastian Redl
źródło
4
O ile obiekt nie jest jeszcze w poprawnym stanie, mimo że jakaś część argumentów konstruktora nie spełnia wymagań i dlatego jest oznaczona jako błąd, 2) tak naprawdę nie jest zalecane. Lepiej jest, gdy obiekt istnieje w poprawnym stanie lub w ogóle go nie ma.
Andy,
@DavidPacker zgodził się, patrz tutaj: stackoverflow.com/questions/77639/... Ale niektóre wytyczne kodowania zabraniają wyjątków, co jest problematyczne dla konstruktorów.
Sebastian Redl
Jakoś już dałem ci głos za tą odpowiedzią, Sebastian. Ciekawy. : D
Andy
10
@ ooxi Nie, to nie jest. Nadpisana nowa jest wywoływana w celu alokacji pamięci, ale wywołanie konstruktora jest wykonywane przez kompilator po powrocie operatora, co oznacza, że ​​nie dostaniesz błędu. Zakłada się, że w ogóle nazywane są nowe; nie dotyczy obiektów alokowanych na stosie, które powinny być większością z nich.
Sebastian Redl
1
W przypadku nr 1 RAII jest częstym przykładem, w którym może być wymagane zrobienie więcej w konstruktorze.
Eric
20

Można utworzyć metodę statyczną, która wykonuje obliczenia i zwraca obiekt w przypadku powodzenia lub niepowodzenia.

W zależności od tego, jak ta konstrukcja obiektu jest wykonywana, może być lepiej utworzyć inny obiekt, który pozwala na konstruowanie obiektów metodą niestatyczną.

Pośrednie wywołanie konstruktora jest często nazywane „fabryką”.

Pozwoliłoby to również zwrócić obiekt zerowy, co może być lepszym rozwiązaniem niż zwrócenie wartości zerowej.

zero
źródło
Dziękuję @ null! Niestety nie mogę tutaj zaakceptować dwóch odpowiedzi :( W przeciwnym razie też bym zaakceptował tę odpowiedź !!
Jeszcze
@MayurK żadnych zmartwień, zaakceptowanej odpowiedź nie jest do oznaczenia na poprawną odpowiedź, ale taki, który pracuje dla Ciebie.
null
3
@ null: W C ++ nie można po prostu wrócić NULL. Na przykład, w int foo() { return NULLrzeczywistości zwracałbyś 0(zero) obiekt jako liczbę całkowitą. W std::string foo() { return NULL; }którą przypadkowo wywołać std::string::string((const char*)NULL)który jest niezdefiniowane zachowanie (NULL nie wskazują na \ 0-zakończony sznurkiem).
MSalters
3
std :: opcjonalne może być daleko, ale zawsze możesz użyć boost :: opcjonalne, jeśli chcesz pójść tą drogą.
Sean Burton,
1
@Vld: W C ++ obiekty nie są ograniczone do typów klas. A przy programowaniu ogólnym nierzadko kończy się na fabrykach int. Np. std::allocator<int>Jest idealnie zdrową fabryką.
MSalters
5

@SebastianRedl podał już proste, bezpośrednie odpowiedzi, ale przydatne może być dodatkowe wyjaśnienie.

TL; DR = istnieje reguła stylu, która upraszcza konstruktorów, są tego powody, ale te powody dotyczą głównie historycznego (lub po prostu złego) stylu kodowania. Obsługa wyjątków w konstruktorach jest dobrze zdefiniowana, a destruktory będą nadal wywoływane dla w pełni skonstruowanych zmiennych lokalnych i elementów, co oznacza, że ​​nie powinno być żadnych problemów w idiomatycznym kodzie C ++. Reguła stylu i tak się utrzymuje, ale zwykle nie stanowi to problemu - nie każda inicjalizacja musi odbywać się w konstruktorze, a szczególnie niekoniecznie w tym konstruktorze.


Jest to powszechna reguła stylu, zgodnie z którą konstruktorzy powinni robić absolutne minimum, jakie mogą, aby ustawić zdefiniowany prawidłowy stan. Jeśli Twoja inicjalizacja jest bardziej złożona, powinna być obsługiwana poza konstruktorem. Jeśli nie ma żadnej taniej do zainicjowania wartości, którą mógłby ustawić twój konstruktor, powinieneś osłabić egzekwujących prawa wymuszone przez twoją klasę, aby ją dodać. Na przykład, jeśli przydzielanie miejsca do zarządzania klasą jest zbyt drogie, dodaj stan nieprzydzielony, ale zerowy, ponieważ oczywiście występowanie stanów specjalnych, takich jak null, nigdy nie spowodowało żadnych problemów. Ahem.

Chociaż powszechne, z pewnością w tej ekstremalnej formie jest bardzo dalekie od absolutnego. W szczególności, jak wskazuje mój sarkazm, jestem w obozie, który mówi, że osłabienie niezmienników jest prawie zawsze zbyt wysoką ceną. Istnieją jednak powody stojące za regułą stylu i istnieją sposoby na uzyskanie zarówno minimalnych konstruktorów, jak i silnych niezmienników.

Przyczyny dotyczą automatycznego czyszczenia destruktora, szczególnie w obliczu wyjątków. Zasadniczo musi istnieć dobrze określony punkt, w którym kompilator staje się odpowiedzialny za wywoływanie destruktorów. Podczas gdy nadal jesteś w wywołaniu konstruktora, obiekt niekoniecznie jest w pełni skonstruowany, więc wywołanie destruktora dla tego obiektu jest nieprawidłowe. Dlatego odpowiedzialność za zniszczenie obiektu przenosi się na kompilator dopiero po pomyślnym zakończeniu konstruktora. Jest to znane jako RAII (Resource Allocation Is Initialization), co nie jest tak naprawdę najlepszą nazwą.

Jeśli wyjątek występuje w konstruktorze, wszystko, co jest częściowo zbudowane, musi zostać jawnie wyczyszczone, zazwyczaj w pliku try .. catch.

Jednak komponenty obiektu, które zostały już pomyślnie zbudowane, są już odpowiedzialnością kompilatorów. Oznacza to, że w praktyce nie jest to nic wielkiego. na przykład

classname (args) : base1 (args), member2 (args), member3 (args)
{
}

Ciało tego konstruktora jest puste. Tak długo, jak konstruktorzy dla base1, member2i member3są bezpieczne wyjątek, nie ma nic się martwić. Na przykład, jeśli konstruktor member2rzuca, ten konstruktor jest odpowiedzialny za samo oczyszczenie. Baza base1została już całkowicie zbudowana, więc jej destruktor zostanie automatycznie wywołany. member3nigdy nie był nawet częściowo zbudowany, więc nie wymaga czyszczenia.

Nawet gdy istnieje ciało, zmienne lokalne, które zostały w pełni skonstruowane przed zgłoszeniem wyjątku, zostaną automatycznie zniszczone, tak jak każda inna funkcja. Konstruktory, które żonglują surowymi wskaźnikami lub „posiadają” jakiś stan niejawny (przechowywany gdzie indziej) - zwykle oznaczający, że wywołanie funkcji rozpoczęcia / nabycia musi być dopasowane do wywołania zakończenia / zwolnienia - może powodować wyjątkowe problemy z bezpieczeństwem, ale prawdziwy problem nie zarządza zasobem poprawnie za pośrednictwem klasy. Na przykład, jeśli zastąpisz surowe wskaźniki unique_ptrkonstruktorem, destruktor dla unique_ptrbędzie wywoływany automatycznie w razie potrzeby.

Są jeszcze inne powody, dla których ludzie preferują konstruktorów wykonujących minimalne czynności. Po pierwsze dlatego, że istnieje reguła stylu, wiele osób uważa, że ​​wywołania konstruktora są tanie. Jednym ze sposobów na uzyskanie tego, ale wciąż silnych niezmienników, jest oddzielna klasa fabryki / konstruktora, która ma osłabione niezmienniki i która ustawia potrzebną wartość początkową przy użyciu (potencjalnie wielu) normalnych wywołań funkcji członka. Po uzyskaniu potrzebnego stanu początkowego przekaż ten obiekt jako argument konstruktorowi klasy z silnymi niezmiennikami. To może „ukraść wnętrzności” obiektu o słabych niezmiennikach - przenieść semantykę - co jest tanią (i zwykle noexcept) operacją.

Oczywiście możesz zawinąć to w make_whatever ()funkcję, więc osoby wywołujące tę funkcję nigdy nie muszą widzieć instancji klasy osłabionej niezmienniki.

Steve314
źródło
Akapit, w którym piszesz „Gdy nadal jesteś w wywołaniu konstruktora, obiekt niekoniecznie jest w pełni skonstruowany, więc wywołanie destruktora dla tego obiektu nie jest prawidłowe. Dlatego odpowiedzialność za zniszczenie obiektu przenosi się tylko na kompilator gdy konstruktor zakończy się pomyślnie ”, może naprawdę skorzystać z aktualizacji dotyczącej delegowania konstruktorów. Obiekt jest w pełni skonstruowany po zakończeniu dowolnego konstruktora najbardziej pochodnego, a destruktor zostanie wywołany, jeśli wystąpi wyjątek w konstruktorze delegującym.
Ben Voigt
Tak więc konstruktor „do-the-minimum” może być prywatny, a funkcja „make_whokolwiek ()” może być innym konstruktorem, który wywołuje prywatny.
Ben Voigt
To nie jest definicja RAII, którą znam. Moje rozumienie RAII polega na celowym pozyskiwaniu zasobów w konstruktorze obiektu (i tylko w nim) i uwalnianiu go w destruktorze. W ten sposób obiekt można wykorzystać na stosie, aby automatycznie zarządzać pozyskiwaniem i uwalnianiem zasobów, które hermetyzuje. Klasycznym przykładem jest zamek, który po zbudowaniu nabywa muteks i uwalnia go po zniszczeniu.
Eric
1
@Eric - Tak, to absolutnie standardowa praktyka - standardowa praktyka, która jest powszechnie nazywana RAII. To nie tylko ja rozciąga definicję - to nawet Stroustrup, w niektórych rozmowach. Tak, RAII polega na łączeniu cykli życia zasobów z cyklami życia obiektów, przy czym model mentalny jest własnością.
Steve314,
1
@Eric - poprzednie odpowiedzi zostały usunięte, ponieważ zostały źle wyjaśnione. W każdym razie same obiekty są zasobami, które mogą być własnością. Wszystko powinno mieć właściciela, w łańcuchu aż do mainfunkcji lub zmiennych statycznych / globalnych. Obiekt przeznaczono użyciu new, nie jest własnością do momentu przypisania tej odpowiedzialności, ale inteligentne wskaźniki właścicielem obiektów sterty przydzielone one odniesienia, a pojemniki są właścicielami swoich struktur danych. Właściciele mogą usunąć wcześniej, właściciel destructor jest ostatecznie odpowiedzialny.
Steve314,