2013-03-26 20 views
8

Zadaniem GotW#8 jest zaimplementowanie w strukturze C++ struktury generycznej stosu wyjątków, przy założeniu, że destruktor argumentu szablonu nie zostanie wygenerowany. Sztuką jest obsługa potencjalnie rzucania operacjami z argumentami szablonu (konstruktor, konstruktor kopiowania, przypisanie) w taki sposób, aby pozostawić stos w spójnym stanie, jeśli rzucają.Technika obsługi wyjątków w klasie podstawowej

W rozwiązaniu, Herb Sutter mówi

Aby utrzymać to rozwiązanie prostsze, zdecydowałem nie zademonstrować techniki klasą bazową dla własności zasobów wyjątku bezpieczny.

Po pewnym googling znalazłem this answer przez Dave Abrahams sięga roku 1997. W jego rozwiązanie obsługuje on przydział i kasowanie pamięci w klasie bazowej i realizuje operacje stosu w podklasie. W ten sposób zapewnia, że ​​kopiowanie elementów w konstruktorze kopiowania jest oddzielane od alokacji pamięci, tak więc jeśli kopiowanie się nie powiedzie, wywoływany jest destruktor klasy podstawowej, bez względu na wszystko.

Dla porównania, tutaj jest kopia konstruktor Dave'a z moim komentarzu dodaje:

// v_ refers to the internal array storing the stack elements 
Stack(const Stack& rhs) 
     : StackBase<T>(rhs.Count()) // constructor allocates enough space 
             // destructor calls delete[] appropriately 
{ 
     while (Count() < rhs.Count()) 
      Push(rhs.v_[ Count() ]); // may throw 
} 

Jeżeli konstruktor baza powiedzie, pamięć clean-up w bazowej destructor jest gwarantowana nawet jeśli kopia konstruktor podklasy rzuca.

Moje pytania są następujące:

  1. Czy istnieją inne korzyści z podejściem, z wyjątkiem przypadków opisanych powyżej?
  2. wymyśliłem tej kopii konstruktora kiedy rozwiązać problem na własną rękę:

    // v_ refers to the internal array storing the stack elements 
    // vsize_ is the amount of space allocated in v_ 
    // vused_ is the amount of space used so far in v_ 
    Stack (const Stack &rhs) : 
         vsize_ (0), vused_ (0), v_ (0) { 
        Stack temp (rhs.vused_); // constructor calls `new T[num_elements]` 
              // destructor calls `delete[] v_` 
        std::copy (rhs.v_, rhs.v_ + rhs.vused_, temp.v_); // may throw 
        swap (temp); 
    } 
    void swap (Stack &rhs) { 
        std::swap (v_, rhs.v_); 
        std::swap (vused_, rhs.vused_); 
        std::swap (vsize_, rhs.vsize_); 
    } 
    

    Uważam to nieco kłopotliwe, aby mieć klasę bazową w porównaniu do tego podejścia. Czy istnieje jakikolwiek powód, dla którego technika podstawowa powinna być preferowana w porównaniu z tym podejściem polegającym na kopiowaniu, kopiowaniu i zamianie? Zauważ, że zarówno Dave, jak i ja mamy już członka swap(), ponieważ używamy go w naszym operator=().

  3. Technika Dave'a Abrahamsa nie wydaje się być dobrze znana (według Google). Czy ma inną nazwę, czy to standardowa praktyka, czy coś przeoczyłem?

Uwagi:

  • zakładać Dave Push() w pętli za równoważne do mojego użytkowania std::copy
  • trzymajmy inteligentne kursory z odpowiedzią, a ich wykorzystanie byłoby zabrać punkt zarządzania pamięć jawnie w tym ćwiczeniu

Odpowiedz

1

Zachowanie behawioralnie dwie implementacje są takie same. Oba konfigurują obiekt zarządzanej alokacji pamięci, który będzie czyścić na wyjściu scope, jeśli konstruktor ulegnie awarii. Kopiowanie do zmiennej tymczasowej może być droższe, ale, jak zauważono w komentarzach, prawdopodobnie unieważnienie takich dodatkowych kosztów. W odpowiedzi na konkretne pytania:

  1. Przykład Abrahama przenosi alokację sterty dalej od faktycznych szczegółów implementacji klas.W twoim kodzie, jeśli zrobisz bardziej skomplikowaną manipulację pamięcią przed/po skopiowaniu twojej tablicy, może być nieco trudniej zapewnić poprawne zarządzanie wszystkimi jednostkami. W przeciwnym razie nie widzę żadnych wyraźnych szczegółów, które nie obejmowałyby już stylu w odniesieniu do zachowania pierwszej implementacji.
  2. Implementacja Abrahama upraszcza porządkowanie w jednym miejscu. Jeśli wiele klas używa StackBase<T>, to każdy z nich może bezpiecznie założyć, że ich pamięć dynamiczna zostanie wyczyszczona, jeśli wyrzucą wyjątek. W twojej implementacji będziesz musiał przepisać (przynajmniej części) obiekt tymczasowy i kod wymiany, aby osiągnąć to samo. Skutecznie, ta implementacja zmniejsza liczbę linii do implementacji wielu podklas StackBase. Jednak twoja implementacja pozwala uniknąć dziedziczenia wielokrotnego, jeśli potrzebujesz dodatkowej alokacji pamięci nad inną klasą bazową. Twój kod pozwala także uniknąć nadużywania kodu szablonu/rozmiarów kodu szablonu - chociaż zazwyczaj nie uważam tego za duży negatywny wynik w większości przypadków. Prawdopodobnie użyłbym czegoś blisko twojego kodu, chyba że próbowałem napisać bardzo ogólny kod przypadków użycia.
  3. Nie wiem, czy to podejście ma konkretną nazwę - zaktualizuję to, jeśli znajdę - ale widziałem, że było używane w co najmniej jednej książce programistycznej C++.
+0

Udostępnianie bazy wydaje się być jedynym argumentem przemawiającym za podejściem bazowym. Dam mu dzień lub dwa, żeby sprawdzić, czy ktoś inny nie przedstawi kolejnego argumentu. – Irfy

+0

Podejście klasy bazowej wygrywa, gdy zawartość obiektu jest ciężka, a zamiana na 'temp' jest droga. (Chociaż "ruch" C++ 11 może pomóc) Możesz dodać ten bit do swojej odpowiedzi, jeśli chcesz. – Irfy

Powiązane problemy