Dziwne wyliczenie w destruktorze

83

Obecnie czytam kod źródłowy Protocol Bufferi znalazłem jeden dziwne enumkody zdefiniowane tutaj

  ~scoped_ptr() {
    enum { type_must_be_complete = sizeof(C) };
    delete ptr_;
  }

  void reset(C* p = NULL) {
    if (p != ptr_) {
      enum { type_must_be_complete = sizeof(C) };
      delete ptr_;
      ptr_ = p;
    }
  }

Dlaczego enum { type_must_be_complete = sizeof(C) };tutaj zdefiniowano? do czego jest to używane?

zangw
źródło
2
Jeśli chcę być tego pewien, wolałbym raczej użyć ptr_siebie w sizeofas sizeof(*ptr_)zamiast sizeof(C).
Nawaz

Odpowiedzi:

81

Ta sztuczka pozwala uniknąć UB, zapewniając, że definicja C jest dostępna podczas kompilacji tego destruktora. W przeciwnym razie kompilacja zakończy się niepowodzeniem, ponieważ sizeofnie można określić niekompletnego typu (zadeklarowane typy do przodu), ale można użyć wskaźników.

W skompilowanym pliku binarnym ten kod zostałby zoptymalizowany i nie miałby żadnego efektu.

Zauważ, że: Usunięcie niekompletnego typu może być niezdefiniowanym zachowaniem z 5.3.5 / 5 :.

jeśli usuwany obiekt ma niekompletny typ klasy w momencie usunięcia, a cała klasa ma nietrywialny destruktor lub funkcję cofnięcia alokacji, zachowanie jest niezdefiniowane .

g++ wydaje nawet następujące ostrzeżenie:

ostrzeżenie: możliwy problem wykryty w wywołaniu operatora usuwania:
ostrzeżenie : `` p '' ma niekompletny typ
ostrzeżenie: deklaracja przekazania `` struct C ''

Mohit Jain
źródło
1
„Usunięcie niepełnego typu jest niezdefiniowanym działaniem” jest nieprawidłowe. To UB tylko wtedy, gdy typ ma nietrywialny destruktor. Problem, który rozwiązuje ta mała sztuczka, polega właśnie na tym, że usunięcie niekompletnego typu nie zawsze jest UB, więc język to obsługuje.
Pozdrawiam i hth. - Alf
Dzięki @ Cheersandhth.-Alf Chodziło mi o to, że może to być UB, więc generalnie ta linia kodu to UB. Edytowano.
Mohit Jain
32

sizeof(C)zakończy się niepowodzeniem w czasie kompilacji, jeśli Cnie jest kompletnym typem. Ustawienie zakresu lokalnego enumna to sprawia, że ​​instrukcja jest niegroźna w czasie wykonywania.

Jest to sposób programisty chroniącego się przed samym sobą: zachowanie kolejnego delete ptr_na niekompletnym typie jest niezdefiniowane, jeśli ma nietrywialny destruktor.

Batszeba
źródło
1
Czy możesz wyjaśnić, dlaczego w tym momencie C musi być typem kompletnym - czy konieczne jest posiadanie pełnej definicji typu, aby ją wywołać delete? A jeśli tak, to dlaczego i tak kompilator tego nie łapie?
Peter Hull
1
Czy nie chodzi raczej o unikanie C = void? Gdyby Cbył tylko niezdefiniowanym typem, czy deleteinstrukcja już nie zawiodłaby?
Kerrek SB
1
Wygląda na to, że Mohit Jain ma odpowiedź.
Peter Hull
1
−1 "Ustawienie wyliczenia zakresu lokalnego na to sprawia, że ​​instrukcja jest niegroźna w czasie wykonywania." jest bez znaczenia. Przepraszam.
Pozdrawiam i hth. - Alf
1
@SteveJessop thanks. Brakowało mi tego, że usunięcie niekompletnego typu to UB.
Peter Hull
28

Aby zrozumieć enum, zacznij od rozważenia destruktora bez niego:

~scoped_ptr() {
    delete ptr_;
}

gdzie ptr_jest C*. Jeśli typ Cjest niekompletna w tym momencie, to znaczy, że wszystko jest kompilator wie struct C;, to (1) domyślne generowane zrobić-nic destructor służy do instancji wskazał C. Jest mało prawdopodobne, aby to była właściwa czynność w przypadku obiektu zarządzanego przez inteligentny wskaźnik.

Jeśli usuwanie przez wskaźnik do niekompletnego typu zawsze miało niezdefiniowane zachowanie, to standard może po prostu wymagać, aby kompilator zdiagnozował to i zakończył się niepowodzeniem. Ale jest dobrze zdefiniowane, gdy prawdziwy destruktor jest trywialny: wiedza, którą programista może mieć, ale kompilator nie ma. Dlaczego język definiuje i na to pozwala, jest poza mną, ale C ++ obsługuje wiele praktyk, które dziś nie są uważane za najlepsze praktyki.

Typ kompletny ma znany rozmiar i dlatego sizeof(C)będzie kompilowany wtedy i tylko wtedy, gdy Cjest typem pełnym - ze znanym destruktorem. Więc może być używany jako strażnik. Jeden sposób byłby prosty

(void) sizeof(C);  // Type must be complete

Ja przypuszczam , że z jakiegoś kompilatora oraz opcje kompilator optymalizuje ją zanim zdążyła zauważyć, że nie należy skompilować i że enumjest sposób, aby uniknąć takiego niezgodnych zachowanie kompilatora:

enum { type_must_be_complete = sizeof(C) };

Alternatywnym wyjaśnieniem wyboru, enuma nie tylko odrzuconego wyrażenia, są po prostu osobiste preferencje.

Lub, jak sugeruje James T. Hugget w komentarzu do tej odpowiedzi: „Wyliczenie może być sposobem na utworzenie pseudo-przenośnego komunikatu o błędzie w czasie kompilacji”.


(1) Domyślnie generowany destruktor nic nie rób dla niekompletnego typu był problemem ze starym std::auto_ptr. Było tak podstępne, że trafiło do artykułu GOTW na temat idiomu PIMPL , napisanego przez przewodniczącego międzynarodowej komisji normalizacyjnej C ++, Herba Suttera. Oczywiście w dzisiejszych czasach to std::auto_ptrjest przestarzałe, zamiast tego użyjemy innego mechanizmu.

Pozdrawiam i hth. - Alf
źródło
4
Wyliczenie może być sposobem na utworzenie pseudo-przenośnego komunikatu o błędzie w czasie kompilacji.
Brice M. Dempsey
1
Myślę, że ta odpowiedź bardzo dobrze wyjaśnia motywację kodu, ale chciałbym dodać, że (1) niektóre kompilatory sizeof(T)szacują na 0 dla niekompletnych typów zamiast niepowodzenia kompilacji. Jest to jednak zachowanie niezgodne z zasadami. (2) Od C ++ 11 użycie static_assert((sizeof(T) > 0), "T must be a complete type");byłoby lepszym (i idiomatycznym) rozwiązaniem.
5gon12eder
@ 5gon12eder; Podaj przykład kompilatora C ++, który „ sizeof(T)ocenia do 0 dla niekompletnych typów”.
Pozdrawiam i hth. - Alf
1
Nigdy nie korzystałem z takiego kompilatora i nie zdziwiłbym się, gdyby do tej pory wymarły. A nawet jeśli tak się nie stało, brak dbałości o niezgodną implementację jest słuszną decyzją. Niemniej jednak, zarówno libstdc ++ , jak i libc ++ używają static_assert(sizeof(T) > 0, "…");w swoich odpowiednich implementacjach of, std::unique_ptraby upewnić się, że typ jest kompletny…
5gon12eder
1
… Więc myślę, że można bezpiecznie powiedzieć, że jest to idiomatyczne. W każdym razie, ponieważ ocenianie sizeof(T)w kontekście boolowskim jest dokładnie równoważne z testowaniem sizeof(T) > 0, nie ma to większego znaczenia, może poza względami estetycznymi.
5gon12eder
3

Może sztuczka, aby mieć pewność, Czostała zdefiniowana.

Jerome
źródło