Liczniki czasu kompilacji C ++, ponownie

28

TL; DR

Zanim spróbujesz przeczytać cały ten post, wiedz, że:

  1. znalazłem rozwiązanie dla przedstawionego problemu , ale wciąż chętnie wiem, czy analiza jest poprawna;
  2. Spakowałem rozwiązanie do fameta::counterklasy, która rozwiązuje kilka pozostałych dziwactw. Możesz go znaleźć na github ;
  3. widać to podczas pracy nad godbolt .

Jak to się wszystko zaczęło

Odkąd Filip Roséen odkrył / wynalazł w 2015 r. Czarną magię, która kompiluje liczniki czasu, jest w C ++ , miałem niewielką obsesję na punkcie urządzenia, więc kiedy CWG zdecydowało, że funkcjonalność musi odejść, byłem rozczarowany, ale nadal mam nadzieję, że ich umysł można zmienić, pokazując im kilka interesujących przypadków użycia.

Kilka lat temu postanowiłem ponownie rzucić okiem na to, aby uberswitch es mógł zostać zagnieżdżony - moim zdaniem interesujący przypadek użycia - tylko po to, by odkryć, że nie będzie już działać z nowymi wersjami dostępne kompilatory, chociaż problem 2118 był (i nadal jest ) w stanie otwartym: kod się skompiluje, ale licznik się nie zwiększy.

Problem został zgłoszony na stronie internetowej Roséen, a ostatnio także na stackoverflow: Czy C ++ obsługuje liczniki czasu kompilacji?

Kilka dni temu postanowiłem spróbować rozwiązać problemy ponownie

Chciałem zrozumieć, co się zmieniło w kompilatorach, które sprawiły, że pozornie poprawne C ++ przestało działać. W tym celu szukałem szerokiej i dalekiej sieci, aby znaleźć kogoś, kto by o tym mówił, ale bezskutecznie. Zacząłem więc eksperymentować i doszedłem do pewnych wniosków, które przedstawiam tutaj w nadziei, że uzyskam informację zwrotną od bardziej znanej mi osoby.

Poniżej prezentuję oryginalny kod Roséen w celu zachowania przejrzystości. Wyjaśnienie, jak to działa, można znaleźć na jego stronie internetowej :

template<int N>
struct flag {
  friend constexpr int adl_flag (flag<N>);
};

template<int N>
struct writer {
  friend constexpr int adl_flag (flag<N>) {
    return N;
  }

  static constexpr int value = N;
};

template<int N, int = adl_flag (flag<N> {})>
int constexpr reader (int, flag<N>) {
  return N;
}

template<int N>
int constexpr reader (float, flag<N>, int R = reader (0, flag<N-1> {})) {
  return R;
}

int constexpr reader (float, flag<0>) {
  return 0;
}

template<int N = 1>
int constexpr next (int R = writer<reader (0, flag<32> {}) + N>::value) {
  return R;
}

int main () {
  constexpr int a = next ();
  constexpr int b = next ();
  constexpr int c = next ();

  static_assert (a == 1 && b == a+1 && c == b+1, "try again");
}

Zarówno najnowsze kompilatory g ++, jak i clang ++ next()zawsze zwracają 1. Po odrobinie eksperymentu wydaje się, że przynajmniej w przypadku g ++ wydaje się, że gdy kompilator oceni domyślne parametry szablonów funkcji przy pierwszym wywołaniu funkcji, każde kolejne wywołanie funkcje te nie wyzwalają ponownej oceny domyślnych parametrów, dlatego nigdy nie tworzą nowych funkcji, ale zawsze odnoszą się do wcześniej utworzonych.


Pierwsze pytania

  1. Czy faktycznie zgadzasz się z moją diagnozą?
  2. Jeśli tak, czy to nowe zachowanie jest wymagane przez standard? Czy poprzedni był błędem?
  3. Jeśli nie, to w czym problem?

Mając powyższe na uwadze, wymyśliłem obejście: oznaczaj każde next()wywołanie monotonicznie rosnącym unikalnym identyfikatorem, aby przekazać odbiorcom, aby żadne połączenie nie było takie samo, zmuszając kompilator do ponownej oceny wszystkich argumentów za każdym razem.

Wydaje się, że jest to ciężkie zadanie, ale myśląc o tym, można po prostu użyć standardowych __LINE__lub __COUNTER__podobnych (tam, gdzie jest to możliwe) makr, ukrytych w counter_next()makrze podobnej do funkcji.

Więc wymyśliłem następujące, które przedstawiam w najbardziej uproszczonej formie, która pokazuje problem, o którym będę mówić później.

template <int N>
struct slot;

template <int N>
struct slot {
    friend constexpr auto counter(slot<N>);
};

template <>
struct slot<0> {
    friend constexpr auto counter(slot<0>) {
        return 0;
    }
};

template <int N, int I>
struct writer {
    friend constexpr auto counter(slot<N>) {
        return I;
    }

    static constexpr int value = I-1;
};

template <int N, typename = decltype(counter(slot<N>()))>
constexpr int reader(int, slot<N>, int R = counter(slot<N>())) {
    return R;
};

template <int N>
constexpr int reader(float, slot<N>, int R = reader(0, slot<N-1>())) {
    return R;
};

template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N>())+1>::value) {
    return R;
}

int a = next<11>();
int b = next<34>();
int c = next<57>();
int d = next<80>();

Możesz obserwować wyniki powyższego na godbolt , który zrzut ekranu dla leniwców.

wprowadź opis zdjęcia tutaj

I jak widać, z bagażnikiem g ++ i clang ++ do wersji 7.0.0 działa! , licznik wzrasta zgodnie z oczekiwaniami od 0 do 3, ale w wersji clang ++ powyżej 7.0.0 tak nie jest .

Aby dodać zniewagę do obrażeń, udało mi się doprowadzić do awarii clang ++ aż do wersji 7.0.0, po prostu dodając do miksu parametr „kontekstu”, tak że licznik jest faktycznie związany z tym kontekstem i jako taki może być restartowane za każdym razem, gdy definiowany jest nowy kontekst, który otwiera się na możliwość użycia potencjalnie nieskończonej ilości liczników. W tym wariancie klang ++ powyżej wersji 7.0.0 nie ulega awarii, ale nadal nie daje oczekiwanego rezultatu. Żyj na godbolt .

Po utracie jakichkolwiek wskazówek na temat tego, co się dzieje, odkryłem stronę cppinsights.io , która pozwala zobaczyć, jak i kiedy tworzone są szablony. Korzystając z tej usługi, myślę, że dzieje się tak, że clang ++ nie definiuje żadnej z friend constexpr auto counter(slot<N>)funkcji za każdym razem, gdy writer<N, I>jest tworzona.

counter(slot<N>)Wydaje się, że próba jawnego wezwania do podania dowolnego N, który powinien już zostać utworzony, stanowi podstawę tej hipotezy.

Jednakże, jeśli staram się wyraźnie instancję writer<N, I>dla danego Ni Iktóre powinny już instancja, potem szczęk ++ skarży się na nowo friend constexpr auto counter(slot<N>).

Aby przetestować powyższe, dodałem jeszcze dwa wiersze do poprzedniego kodu źródłowego.

int test1 = counter(slot<11>());
int test2 = writer<11,0>::value;

Możesz to wszystko zobaczyć na Godbolt . Zrzut ekranu poniżej.

clang ++ uważa, że ​​zdefiniował coś, czego uważa, że ​​nie zdefiniował

Wygląda więc na to, że clang ++ wierzy, że zdefiniował coś, czego uważa, że ​​nie zdefiniował , co powoduje, że kręci Ci się w głowie, prawda?


Druga partia pytań

  1. Czy moje obejście jest w ogóle zgodne z C ++, czy udało mi się po prostu odkryć kolejny błąd g ++?
  2. Jeśli to jest legalne, to czy odkryłem jakieś nieprzyjemne błędy clang ++?
  3. A może po prostu zagłębiłem się w mroczny podziemny świat Nieokreślonego Zachowania, więc jestem winien tylko ja?

W każdym razie serdecznie witam każdego, kto chciałby mi pomóc wydostać się z tej króliczej nory, w razie potrzeby udzielając wyjaśnień na temat głowy. :RE

Fabio A.
źródło
2
Powiązane: stackoverflow.com/questions/51601439/…
HolyBlackCat
2
Z tego co pamiętam, ludzie z komitetu standardowego mają wyraźny zamiar zabraniać wszelkich konstrukcji, kształtów i form w czasie kompilacji, które nie dają dokładnie tego samego wyniku za każdym razem (hipotetycznie) ocenianiu. Może to być błąd kompilatora, może to być przypadek „źle sformułowany, nie wymaga diagnozy” lub może to być coś, czego standard nie zauważył. Niemniej jednak jest to sprzeczne z „duchem standardu”. Przepraszam. Chciałbym też skompilować liczniki czasu.
bolov
@HolyBlackCat Muszę wyznać, że bardzo trudno mi się skupić na tym kodzie. Wygląda na to, że można by uniknąć konieczności jawnego przekazywania monotonicznie rosnącej liczby jako parametru do next()funkcji, jednak tak naprawdę nie mogę zrozumieć, jak to działa. W każdym razie znalazłem odpowiedź na mój problem tutaj: stackoverflow.com/a/60096865/566849
Fabio A.
@FabioA. Ja też nie do końca rozumiem tę odpowiedź. Od czasu zadania tego pytania zdałem sobie sprawę, że nie chcę już dotykać liczników constexpr.
HolyBlackCat
Chociaż jest to mały, zabawny eksperyment myślowy, ktoś, kto faktycznie używał tego kodu, musiałby oczekiwać, że nie zadziała w przyszłych wersjach C ++, prawda? W tym sensie wynik określa się jako błąd.
Aziuth,

Odpowiedzi:

5

Po dalszych badaniach okazuje się, że istnieje niewielka modyfikacja, którą można wykonać w next()funkcji, co powoduje, że kod działa poprawnie na wersjach clang ++ powyżej 7.0.0, ale powoduje, że przestaje działać dla wszystkich innych wersji clang ++.

Zobacz poniższy kod zaczerpnięty z mojego poprzedniego rozwiązania.

template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N>())+1>::value) {
    return R;
}

Jeśli zwrócisz na to uwagę, dosłownie robi to, próbując odczytać wartość powiązaną slot<N>, dodać 1, a następnie powiązać tę nową wartość z tą samą slot<N> .

Gdy slot<N>nie ma żadnej powiązanej wartości, slot<Y>zamiast niej pobierana jest wartość powiązana z , Yktóra jest najwyższym indeksem mniejszym niż Ntaki, który slot<Y>ma powiązaną wartość.

Problem z powyższym kodem polega na tym, że nawet jeśli działa on na g ++, clang ++ (słusznie, powiedziałbym?) Sprawia, że ​​na reader(0, slot<N>()) stałe zwraca wszystko, co zwrócił, gdy slot<N>nie miał powiązanej wartości. To z kolei oznacza, że ​​wszystkie gniazda są skutecznie kojarzone z wartością podstawową 0.

Rozwiązaniem jest przekształcenie powyższego kodu w ten:

template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N-1>())+1>::value) {
    return R;
}

Zauważ, że slot<N>()został zmodyfikowany w slot<N-1>(). Ma to sens: jeśli chcę powiązać wartość slot<N>, oznacza to, że żadna wartość nie jest jeszcze powiązana, dlatego nie ma sensu próbować jej odzyskiwać. Ponadto chcemy zwiększyć licznik, a wartość licznika powiązanego z nim slot<N>musi wynosić jeden plus wartość powiązana z slot<N-1>.

Eureka!

To jednak łamie wersje clang ++ <= 7.0.0.

Wnioski

Wydaje mi się, że opublikowane przeze mnie oryginalne rozwiązanie zawiera błąd koncepcyjny, taki jak:

  • g ++ ma dziwactwo / błąd / relaksację, która anuluje błąd mojego rozwiązania i ostatecznie sprawia, że ​​kod działa.
  • clang ++ wersje> 7.0.0 są bardziej rygorystyczne i nie podoba im się błąd w oryginalnym kodzie.
  • clang ++ wersje <= 7.0.0 zawierają błąd, który powoduje, że poprawione rozwiązanie nie działa.

Podsumowując, poniższy kod działa na wszystkich wersjach g ++ i clang ++.

#if !defined(__clang_major__) || __clang_major__ > 7
template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N-1>())+1>::value) {
    return R;
}
#else
template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N>())+1>::value) {
    return R;
}
#endif

Kod as-działa również z msvc. Kompilator icc nie uruchamia SFINAE podczas używania decltype(counter(slot<N>())), wola narzekać na brak możliwości, deduce the return type of function "counter(slot<N>)"ponieważ it has not been defined. Uważam, że jest to błąd , który można obejść, wykonując SFINAE na podstawie bezpośredniego wyniku counter(slot<N>). Działa to również na wszystkich innych kompilatorach, ale g ++ decyduje się wypluć mnóstwo bardzo irytujących ostrzeżeń, których nie można wyłączyć. Także w tym przypadku #ifdefmoże przyjść na ratunek.

Dowód na godbolt , screnshotted poniżej.

wprowadź opis zdjęcia tutaj

Fabio A.
źródło
2
Myślę, że ta odpowiedź w pewnym stopniu zamyka temat, ale nadal chciałbym wiedzieć, czy mam rację w mojej analizie, dlatego poczekam, zanim zaakceptuję własną odpowiedź jako poprawną, mając nadzieję, że ktoś inny przejdzie i da mi lepszą wskazówkę lub potwierdzenie. :)
Fabio A.