Pytanie Kod C ++ o niezdefiniowanym zachowaniu, kompilator generuje wyjątek std ::


Natknąłem się na interesującą bezpieczną regułę kodowania w C ++, która stwierdza:

Nie wprowadzaj ponownie funkcji podczas inicjowania deklaracji zmiennej statycznej. Jeśli funkcja zostanie przywrócona podczas ciągłej inicjalizacji obiektu statycznego wewnątrz tej funkcji, zachowanie programu jest niezdefiniowane. Nieskończona rekursja nie jest wymagana, aby wywołać niezdefiniowane zachowanie, funkcja musi być powtarzana tylko raz w ramach inicjalizacji.

Niezgodny przykład tego samego jest:

#include <stdexcept>

int fact(int i) noexcept(false) {
  if (i < 0) {
    // Negative factorials are undefined.
    throw std::domain_error("i must be >= 0");
  }

  static const int cache[] = {
    fact(0), fact(1), fact(2), fact(3), fact(4), fact(5),
    fact(6), fact(7), fact(8), fact(9), fact(10), fact(11),
    fact(12), fact(13), fact(14), fact(15), fact(16)
  };

  if (i < (sizeof(cache) / sizeof(int))) {
    return cache[i];
  }

  return i > 0 ? i * fact(i - 1) : 1;
}

który według źródła podaje błąd:

terminate called after throwing an instance of '__gnu_cxx::recursive_init_error'
  what():  std::exception

po wykonaniu w Visual Studio 2013. Próbowałem podobny kod własnych i dostałem ten sam błąd (skompilowany przy użyciu g ++ i wykonywane, na Ubuntu).

Mam wątpliwości, czy moje zrozumienie jest poprawne w odniesieniu do tego pojęcia, ponieważ nie jestem dobrze zorientowany w C ++. Według mnie, ponieważ tablica pamięci podręcznej jest stała, co oznacza, że ​​może być tylko do odczytu i musi być zainicjalizowana tylko raz jako statyczna, jest ona inicjowana ponownie i ponownie, ponieważ wartości tej tablicy są wartością zwracaną przez każdą z nich. oddzielone przecinkami wywołania funkcji rekurencyjnych, które są sprzeczne z zachowaniem zadeklarowanej tablicy. Daje więc niezdefiniowane zachowanie, które jest również określone w regule.

Jakie jest lepsze wytłumaczenie tego?


14
2017-10-28 17:33


pochodzenie


Czy rozumiem cię poprawnie: czy zależy ci od implementacji, dlaczego ten konkretny przypadek nieokreślonego zachowania zachowuje się w ten właśnie sposób? - cadaniluk
@cad: masz rację! .... :) - POOJA GUPTA


Odpowiedzi:


Aby wykonać fact(), musisz najpierw zainicjalizować statycznie fact::cache[]. Aby na początku fact::cache, musisz wykonać fact(). Istnieje tam zależność cykliczna, która prowadzi do zachowania, które widzisz. cache zostanie zainicjowany tylko raz, ale wymaga inicjalizacji w celu zainicjowania. Nawet wpisywanie tego sprawia, że ​​moja głowa się kręci.

Właściwym sposobem wprowadzenia tabeli pamięci podręcznej w ten sposób jest rozdzielenie jej na inną funkcję:

int fact(int i) noexcept(false) {
  if (i < 0) {
    // Negative factorials are undefined.
    throw std::domain_error("i must be >= 0");
  }

  return i > 0 ? i * fact(i - 1) : 1;
} 

int memo_fact(int i) noexcept(false) {
  static const int cache[] = {
    fact(0), fact(1), fact(2), fact(3), fact(4), fact(5),
    fact(6), fact(7), fact(8), fact(9), fact(10), fact(11),
    fact(12), fact(13), fact(14), fact(15), fact(16)
  };

  if (i < (sizeof(cache) / sizeof(int))) {
    return cache[i];
  }
  else {
    return fact(i);
  }    
} 

Tutaj, memo_fact::cache[] zostanie zainicjowany tylko raz - ale inicjalizacja nie jest już zależna od niego samego. Więc nie mamy problemu.


17
2017-10-28 17:40



dziękuję za wyjaśnienie :) .. Rozumiem z tego, że tablica pamięci podręcznej, która ma być statycznie zainicjowana raz, zostaje ponownie zainicjowana ze względu na zależność od rekurencyjnego wywołania funkcji fact (). Dlatego funkcja ta ponownie przechodzi do jej inicjalizacji. - POOJA GUPTA


Standard C ++, §6.7 / 4, mówi o inicjalizacji zmiennych blokowych o statycznym czasie przechowywania:

Jeśli kontrola ponownie przyjmuje deklarację rekurencyjnie, gdy zmienna jest   podczas inicjalizacji zachowanie jest niezdefiniowane.

Podano następujący przykład informacyjny:

int foo(int i) {
static int s = foo(2*i); // recursive call - undefined
return i+1;
}

Dotyczy to również twojego przykładu. fact(0) jest rekursywnym wywołaniem, więc deklaracja cache jest ponownie wprowadzane. Nieokreślone zachowanie jest wywoływane.

Ważne jest, aby pamiętać, co oznacza niezdefiniowane zachowanie. Niezdefiniowane zachowanie oznacza to wszystko może się zdarzyć, a "wszystko" całkiem naturalnie zawiera wyjątki, które są rzucane.

Niezdefiniowane zachowanie oznacza również, że nie można już rozumować o niczym innym w kodzie, chyba że naprawdę chcemy przejść do szczegółów implementacji kompilatora. Ale nie mówisz już o C ++ pod względem używania języka programowania, ale o tym, jak wdrożyć ten język.


6
2017-10-28 17:51



dziękuję za wyjaśnienie :) ... - POOJA GUPTA