TL; DR
Zanim spróbujesz przeczytać cały ten post, wiedz, że:
- znalazłem rozwiązanie dla przedstawionego problemu , ale wciąż chętnie wiem, czy analiza jest poprawna;
- Spakowałem rozwiązanie do
fameta::counter
klasy, która rozwiązuje kilka pozostałych dziwactw. Możesz go znaleźć na github ; - 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
- Czy faktycznie zgadzasz się z moją diagnozą?
- Jeśli tak, czy to nowe zachowanie jest wymagane przez standard? Czy poprzedni był błędem?
- 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.
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 N
i I
któ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.
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ń
- Czy moje obejście jest w ogóle zgodne z C ++, czy udało mi się po prostu odkryć kolejny błąd g ++?
- Jeśli to jest legalne, to czy odkryłem jakieś nieprzyjemne błędy clang ++?
- 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
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/566849Odpowiedzi:
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.
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 ,Y
która jest najwyższym indeksem mniejszym niżN
taki, któryslot<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ł, gdyslot<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:
Zauważ, że
slot<N>()
został zmodyfikowany wslot<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 nimslot<N>
musi wynosić jeden plus wartość powiązana zslot<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:
Podsumowując, poniższy kod działa na wszystkich wersjach g ++ i clang ++.
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 wynikucounter(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#ifdef
może przyjść na ratunek.Dowód na godbolt , screnshotted poniżej.
źródło