Jaka jest różnica koncepcyjna między wreszcie a destruktorem?


12

Po pierwsze, dobrze wiem, dlaczego w C ++ nie ma konstrukcji „nareszcie”? ale długotrwała dyskusja na temat komentarza wydaje się uzasadniać osobne pytanie.

Oprócz problemu, że finallyw języku C # i Javie może istnieć tylko raz (== 1) na zakres, a pojedynczy zakres może mieć wiele (== n) destrukterów C ++, myślę, że są one zasadniczo takie same. (Z pewnymi różnicami technicznymi.)

Jednak inny użytkownik argumentował :

... Próbowałem powiedzieć, że dtor jest z natury narzędziem (Release sematics), a na koniec z natury narzędziem (Commit semantyka). Jeśli nie rozumiesz, dlaczego: zastanów się, dlaczego uzasadnione jest umieszczanie wyjątków jeden na drugim w ostatecznie blokach i dlaczego to samo nie dotyczy niszczycieli. (W pewnym sensie jest to kwestia kontroli danych i danych. Destruktory służą do uwalniania danych, wreszcie do kontroli. Są różne; szkoda, że ​​C ++ je łączy).

Czy ktoś może to wyjaśnić?

Odpowiedzi:


6
  • Transakcja ( try)
  • Błąd wyjścia / odpowiedzi ( catch)
  • Błąd zewnętrzny ( throw)
  • Błąd programisty ( assert)
  • Wycofywanie (najbliższą rzeczą mogą być osłony zakresów w językach, które obsługują je natywnie)
  • Uwalnianie zasobów (destruktory)
  • Różne niezależne od transakcji sterowanie przepływem ( finally)

Nie można wymyślić lepszego opisu finallyniż inny niezależny od transakcji przepływ kontroli. Niekoniecznie musi być tak bezpośrednio odwzorowany na jakąkolwiek koncepcję wysokiego poziomu w kontekście sposobu myślenia o transakcjach i usuwaniu błędów, zwłaszcza w języku teoretycznym, który ma zarówno destrukatory, jak i finally.

Najbardziej nieodłącznie brakuje mi funkcji języka, która bezpośrednio reprezentuje koncepcję wycofywania zewnętrznych efektów ubocznych. Strażnicy zakresów w językach takich jak D są najbliższą rzeczą, o której myślę, że jest to bliskie przedstawienia tej koncepcji. Z punktu widzenia przepływu kontroli wycofanie w zakresie określonej funkcji musiałoby odróżnić ścieżkę wyjątkową od zwykłej, jednocześnie automatyzując wycofanie w sposób dorozumiany wszelkich skutków ubocznych spowodowanych przez funkcję w przypadku niepowodzenia transakcji, ale nie w przypadku, gdy transakcja się powiedzie. . Jest to dość łatwe w przypadku destruktorów, jeśli, powiedzmy, ustawimy wartość logiczną na wartość succeededtrue na końcu naszego bloku try, aby zapobiec logice cofania w destruktorze. Ale jest to raczej okrągły sposób na zrobienie tego.

Choć mogłoby się wydawać, że nie zaoszczędziłoby tak wiele, odwrócenie efektów ubocznych jest jedną z najtrudniejszych rzeczy do zrobienia (np. Co sprawia, że ​​tak trudno jest napisać ogólny kontener bezpieczny w wyjątkach).


4

W pewnym sensie są - w taki sam sposób, w jaki zarówno Ferrari, jak i tranzyt mogą być używane do zjadania sklepów na kufel mleka, mimo że są one przeznaczone do różnych zastosowań.

Możesz umieścić konstrukcję try / wreszcie w każdym zasięgu i wyczyścić wszystkie zmienne zdefiniowane w zakresie w bloku na końcu, aby emulować destruktor C ++. Jest to koncepcyjnie to, co robi C ++ - kompilator automatycznie wywołuje destruktor, gdy zmienna wykracza poza zakres (tj. Na końcu bloku zakresu). Będziesz musiał ustawić swoją próbę / w końcu, aby próba była pierwszą rzeczą i wreszcie ostatnią rzeczą w każdym zakresie. Trzeba też zdefiniować standard dla każdego obiektu, aby miał metodę o konkretnej nazwie, której używa do czyszczenia swojego stanu, który wywołałby w ostatnim bloku, ale myślę, że można zostawić normalne zarządzanie pamięcią, które zapewnia Twój język oczyść teraz opróżniony obiekt, kiedy chce.

Nie byłoby to jednak przyjemne i chociaż .NET wprowadził IDispose jako ręcznie zarządzany niszczyciel i użycie bloków jako próby ułatwienia tego ręcznego zarządzania, nadal nie jest to coś, co chciałbyś robić w praktyce .


4

Z mojego punktu widzenia główna różnica polega na tym, że destruktor w c ++ jest domyślnym mechanizmem (automatycznie wywoływanym) do zwalniania przydzielonych zasobów, podczas gdy try ... w końcu może być użyty jako jawny mechanizm do tego.

W programach c ++ programista jest odpowiedzialny za zwolnienie przydzielonych zasobów. Zwykle jest to implementowane w destruktorze klasy i wykonywane natychmiast, gdy zmienna wykracza poza zakres lub gdy wywoływane jest usuwanie.

Gdy w c ++ tworzona jest zmienna lokalna klasy bez korzystania newz zasobów tych instancji są one domyślnie zwalniane przez destruktor, gdy istnieje wyjątek.

// c++
void test() {
    MyClass myClass(someParameter);
    // if there is an exception the destructor of MyClass is called automatically
    // this does not work with
    // MyClass* pMyClass = new MyClass(someParameter);

} // on test() exit the destructor of myClass is implicitly called

W java, c # i innych systemach z automatycznym zarządzaniem pamięcią system śmieciowy decyduje o zniszczeniu instancji klasy.

// c#
void test() {
    MyClass myClass = new MyClass(someParameter);
    // if there is an exception myClass is NOT destroyed so there may be memory/resource leakes

    myClass.destroy(); // this is never called
}

Nie ma w tym żadnego ukrytego mechanizmu, więc musisz to zaprogramować bezpośrednio za pomocą try w końcu

// c#
void test() {
    MyClass myClass = null;

    try {
        myClass = new MyClass(someParameter);
        ...
    } finally {
        // explicit memory management
        // even if there is an exception myClass resources are freed
        myClass.destroy();
    }

    myClass.destroy(); // this is never called
}

Dlaczego w C ++ wywoływany jest destruktor automatycznie tylko z obiektem stosu, a nie z obiektem sterty w przypadku wyjątku?
Giorgio,

@Giorgio Ponieważ zasoby sterty znajdują się w obszarze pamięci, który nie jest bezpośrednio powiązany ze stosem wywołań. Na przykład wyobraź sobie aplikację wielowątkową z 2 wątkami Ai B. Jeśli jeden wątek wyrzuci, wycofanie A'stransakcji nie powinno zniszczyć przydzielonych zasobów B, np. - stany wątku są od siebie niezależne, a pamięć trwała na stercie jest niezależna od obu. Jednak zazwyczaj w C ++ pamięć sterty jest nadal powiązana z obiektami na stosie.

@Giorgio Na przykład std::vectorobiekt może znajdować się na stosie, ale wskazywać na pamięć na stercie - zarówno obiekt wektorowy (na stosie), jak i jego zawartość (na stercie) zostaną w tym przypadku zwolnione podczas cofania stosu, ponieważ zniszczenie wektora na stosie wywołałoby destruktor, który zwolni powiązaną pamięć na stercie (i podobnie zniszczy te elementy stosu). Zazwyczaj w celu zapewnienia bezpieczeństwa wyjątków większość obiektów C ++ żyje na stosie, nawet jeśli są one tylko uchwytami wskazującymi pamięć na stercie, automatyzując proces zwalniania pamięci stosu i stosu podczas rozwijania stosu.

4

Cieszę się, że opublikowałeś to jako pytanie. :)

Próbowałem powiedzieć, że destruktory i finallysą koncepcyjnie różne:

  • Niszczyciele służą do uwalniania zasobów ( danych )
  • finallysłuży do powrotu do dzwoniącego ( kontrola )

Rozważmy, powiedzmy, ten hipotetyczny pseudo-kod:

try {
    bar();
} finally {
    logfile.print("bar has exited...");
}

finallytutaj rozwiązuje się całkowicie problem kontroli, a nie problem zarządzania zasobami.
Nie ma sensu robić tego w destruktorze z różnych powodów:

  • Nie rzeczą jest „nabyte” lub „stworzony”
  • Niepowodzenie drukowania do pliku dziennika nie spowoduje wycieków zasobów, uszkodzenia danych itp. (Zakładając, że plik dziennika tutaj nie jest przekazywany z powrotem do programu w innym miejscu)
  • Upadek jest uzasadniony logfile.print, podczas gdy zniszczenie (koncepcyjnie) nie może zawieść

Oto kolejny przykład, tym razem jak w JavaScript:

var mo_document = document, mo;
function observe(mutations) {
    mo.disconnect();  // stop observing changes to prevent re-entrance
    try {
        /* modify stuff */
    } finally {
        mo.observe(mo_document);  // continue observing (conceptually, this can fail)
    }
}
mo = new MutationObserver(observe);
return observe();

W powyższym przykładzie ponownie nie ma żadnych zasobów do zwolnienia.
W rzeczywistości finallyblok gromadzi zasoby wewnętrznie, aby osiągnąć swój cel, co może potencjalnie zakończyć się niepowodzeniem. Dlatego nie ma sensu używać destruktora (jeśli JavaScript miał taki).

Z drugiej strony w tym przykładzie:

b = get_data();
try {
    a.write(b);
} finally {
    free(b);
}

finallyniszczy zasobu b. To problem z danymi. Problem nie polega na czystym zwróceniu kontroli dzwoniącemu, ale raczej na uniknięciu wycieków zasobów.
Awaria nie jest opcją i nigdy (koncepcyjnie) nigdy nie powinna wystąpić.
Każde wydanie bjest koniecznie połączone z akwizycją i sensowne jest użycie RAII.

Innymi słowy, tylko dlatego, że możesz użyć jednego z nich do symulacji, nie oznacza to, że oba są jednym i tym samym problemem lub że oba są odpowiednimi rozwiązaniami dla obu problemów.


Dzięki. Nie zgadzam się, ale hej :-) Myślę, że będę w stanie dodać dokładną odpowiedź przeciwnego widoku w ciągu najbliższych dni ...
Martin Ba

2
W jaki sposób wpływa na to fakt, który finallyjest najczęściej wykorzystywany do zwalniania zasobów (innych niż pamięć)?
Bart van Ingen Schenau

1
@BartvanIngenSchenau: Nigdy nie twierdziłem, że każdy istniejący język ma filozofię lub implementację pasującą do tego, co opisałem. Ludzie jeszcze nie skończyli wymyślać wszystkiego, co mogłoby istnieć. Argumentowałem tylko, że rozdzielenie dwóch pojęć byłoby korzystne, ponieważ są to różne pomysły i różne przypadki użycia. Aby zaspokoić twoją ciekawość, wierzę, że D ma jedno i drugie. Prawdopodobnie są też inne języki. Nie uważam tego za istotne i nie obchodzi mnie, dlaczego np. Java była na korzyść finally.
user541686,

1
Praktycznym przykładem, z którym spotkałem się w JavaScript, są funkcje, które tymczasowo zmieniają wskaźnik myszy na klepsydrę podczas długiej operacji (która może spowodować wyjątek), a następnie zmieniają ją z powrotem na normalną w finallyklauzuli. Światopogląd C ++ wprowadziłby klasę zarządzającą tym „zasobem” przypisania do zmiennej pseudo-globalnej. Co to ma sens konceptualny? Ale destruktory są młotem C ++ wymaganym do wykonania kodu końca bloku.
dan04,

1
@ dan04: Dziękuję bardzo, to idealny przykład tego. Mógłbym przysiąc, że natrafiłem na tak wiele sytuacji, w których RAII nie miało sensu, ale tak ciężko mi było o nich myśleć.
user541686,

1

Odpowiedź k3b naprawdę dobrze to wyraża:

destruktor w c ++ jest domyślnym mechanizmem (automatycznie wywoływanym) do zwalniania przydzielonych zasobów, podczas gdy try ... w końcu może być użyty jako jawny mechanizm do tego.

Jeśli chodzi o „zasoby”, chciałbym odnieść się do Jona Kalba: RAII powinno oznaczać, że przejęcie odpowiedzialności to inicjalizacja .

W każdym razie, jeśli chodzi o domniemane vs. jawne, wydaje się, że tak naprawdę jest:

  • D'tor to narzędzie do definiowania, jakie operacje będą się odbywać - domyślnie - po zakończeniu życia obiektu (co często pokrywa się z końcem zakresu)
  • Blok na koniec to narzędzie do bezpośredniego definiowania, jakie operacje będą się odbywać na końcu zakresu.
  • Dodatkowo, technicznie, zawsze możesz rzucić w końcu, ale patrz poniżej.

Myślę, że to tyle w części konceptualnej ...


... teraz jest kilka interesujących szczegółów IMHO:

Nie sądzę też, aby c'tor / d'tor musiał koncepcyjnie „zdobywać” lub „tworzyć” cokolwiek, poza odpowiedzialnością za uruchomienie kodu w destruktorze. Co ostatecznie robi również: uruchom jakiś kod.

I chociaż kod w końcu może z pewnością rzucić wyjątek, nie jest to dla mnie wystarczające rozróżnienie, aby powiedzieć, że są one koncepcyjnie różne od jawnych do niejawnych.

(Plus, wcale nie jestem przekonany, że „dobry” kod powinien w końcu rzucić - może to kolejne pytanie do siebie.)


Co myślisz o moim przykładzie z Javascriptem?
user541686,

W odniesieniu do innych argumentów: „Czy naprawdę chcielibyśmy rejestrować to samo bez względu na to?” Tak, to tylko przykład i w pewnym sensie brakuje Ci sensu i tak, nikt nigdy nie zabronił rejestrowania bardziej szczegółowych informacji dla każdej sprawy. Chodzi o to, że z pewnością nie możesz twierdzić, że nigdy nie ma sytuacji, w której chciałbyś zarejestrować coś, co jest wspólne dla obu. Niektóre wpisy dziennika są ogólne, niektóre są szczegółowe; chcesz obu. I znowu, zupełnie jakbyś nie rozumiał, skupiając się na rejestrowaniu. Motywowanie 10-liniowych przykładów jest trudne; proszę nie przegapić sensu.
user541686,

Nigdy nie
zwracałeś się do

@ Mehrdad - nie odniosłem się do twojego przykładu w javascript, ponieważ zajęłoby mi to kolejną stronę do omówienia, co o nim myślę. (Próbowałem, ale zajęło mi to tak długo, aby sformułować coś spójnego, że pominąłem to :-)
Martin Ba

@ Mehrdad - podobnie jak w przypadku innych punktów - wydaje się, że musimy zgodzić się z tym. Rozumiem, do czego zmierzasz z tą różnicą, ale po prostu nie jestem przekonany, że są one czymś odmiennym koncepcyjnie: Głównie dlatego, że jestem głównie w obozie, który uważa, że ​​rzucanie w końcu jest naprawdę złym pomysłem ( uwaga : ja również pomyśl w swoim observerprzykładzie, że rzucanie byłoby naprawdę złym pomysłem.) Jeśli chcesz porozmawiać o tym dalej, możesz otworzyć czat. Na pewno fajnie było myśleć o twoich argumentach. Twoje zdrowie.
Martin Ba,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.