TL; DR
Bevor Sie versuchen, diesen ganzen Beitrag zu lesen, sollten Sie Folgendes wissen:
- Ich habe eine Lösung für das vorgestellte Problem gefunden , bin aber immer noch gespannt, ob die Analyse korrekt ist.
- Ich habe die Lösung in eine
fameta::counter
Klasse gepackt, die einige verbleibende Macken löst. Sie können es auf Github finden ; - Sie können es bei der Arbeit an Godbolt sehen .
Wie alles begann
Seit Filip Roséen 2015 die schwarze Magie entdeckte / erfand, die Zeitzähler zum Kompilieren in C ++ enthält , war ich leicht besessen von dem Gerät. Als die CWG entschied, dass die Funktionalität eingestellt werden musste, war ich enttäuscht, aber immer noch hoffnungsvoll, dass ihr Verstand könnte geändert werden, indem ihnen einige überzeugende Anwendungsfälle gezeigt werden.
Dann, ein paar Jahre habe ich wieder einen Blick auf der Sache haben beschlossen, so dass uberswitch es verschachtelt werden kann - ein interessanter Anwendungsfall, meiner Meinung nach - nur um zu entdecken , dass es würde nicht mehr funktionieren mit den neuen Versionen von die verfügbaren Compiler, obwohl Ausgabe 2118 offen war (und immer noch ist ): Der Code würde kompiliert, aber der Zähler würde sich nicht erhöhen.
Das Problem wurde auf der Roséen-Website und kürzlich auch im Stackoverflow gemeldet: Unterstützt C ++ Zähler für die Kompilierungszeit?
Vor ein paar Tagen habe ich beschlossen, die Probleme erneut anzugehen
Ich wollte verstehen, was sich an den Compilern geändert hat, die dazu geführt haben, dass das scheinbar immer noch gültige C ++ nicht mehr funktioniert. Zu diesem Zweck habe ich im Internet nach jemandem gesucht, der darüber gesprochen hat, aber ohne Erfolg. Also habe ich angefangen zu experimentieren und bin zu einigen Schlussfolgerungen gekommen, die ich hier präsentiere, in der Hoffnung, ein Feedback von den Experten zu bekommen, die hier mehr wissen als ich.
Im Folgenden werde ich der Klarheit halber den Originalcode von Roséen vorstellen. Eine Erklärung der Funktionsweise finden Sie auf seiner Website :
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");
}
Sowohl bei g ++ - als auch bei clang ++ - Compilern der letzten Zeit wird next()
immer 1 zurückgegeben. Nachdem ein wenig experimentiert wurde, scheint das Problem zumindest bei g ++ zu sein, dass, sobald der Compiler die Standardparameter der Funktionsvorlagen beim ersten Aufruf der Funktionen auswertet, jeder nachfolgende Aufruf von Diese Funktionen lösen keine Neubewertung der Standardparameter aus und instanziieren daher niemals neue Funktionen, sondern beziehen sich immer auf die zuvor instanziierten.
Erste Fragen
- Stimmen Sie meiner Diagnose tatsächlich zu?
- Wenn ja, ist dieses neue Verhalten vom Standard vorgeschrieben? War der vorherige ein Fehler?
- Wenn nicht, wo liegt dann das Problem?
Unter Berücksichtigung des oben next()
Gesagten habe ich eine Lösung gefunden : Markieren Sie jeden Aufruf mit einer monoton ansteigenden eindeutigen ID, um ihn an die Callees weiterzuleiten, sodass kein Aufruf derselbe ist, und zwingen Sie den Compiler, alle Argumente neu zu bewerten jedes Mal.
Es scheint eine Belastung zu sein, dies zu tun, aber wenn man daran denkt, könnte man einfach die Standard- __LINE__
oder __COUNTER__
ähnlichen Makros (wo immer verfügbar) verwenden, die in einem counter_next()
funktionsähnlichen Makro versteckt sind .
Deshalb habe ich mir Folgendes ausgedacht, das ich in der einfachsten Form präsentiere, die das Problem zeigt, über das ich später sprechen werde.
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>();
Sie können die Ergebnisse der oben genannten auf Godbolt beobachten , die ich für die Faulen Screenshots gemacht habe.
Und wie Sie sehen können, funktioniert es mit Trunk G ++ und Clang ++ bis 7.0.0! Der Zähler steigt erwartungsgemäß von 0 auf 3, bei einer Clang ++ - Version über 7.0.0 jedoch nicht .
Um die Verletzung zusätzlich zu beleidigen, habe ich es tatsächlich geschafft, clang ++ bis zum Absturz von Version 7.0.0 zum Absturz zu bringen, indem ich einfach einen "Kontext" -Parameter zum Mix hinzugefügt habe, sodass der Zähler tatsächlich an diesen Kontext gebunden ist und als solcher kann Sie können jedes Mal neu gestartet werden, wenn ein neuer Kontext definiert wird, der die Möglichkeit eröffnet, eine möglicherweise unendliche Anzahl von Zählern zu verwenden. Mit dieser Variante stürzt clang ++ über Version 7.0.0 nicht ab, liefert aber immer noch nicht das erwartete Ergebnis. Lebe auf Godbolt .
Ohne einen Hinweis darauf zu haben, was los war, habe ich die Website cppinsights.io entdeckt, auf der man sehen kann, wie und wann Vorlagen instanziiert werden. Ich denke, dass die Verwendung dieses Dienstes dazu führt, dass clang ++ keine der friend constexpr auto counter(slot<N>)
Funktionen definiert, wenn sie writer<N, I>
instanziiert werden.
Der Versuch, explizit counter(slot<N>)
nach einem bestimmten N zu rufen , das bereits instanziiert werden sollte, scheint diese Hypothese zu begründen.
Wenn ich jedoch ausdrücklich instantiate versuchen writer<N, I>
für jede gegebene N
und I
dass bereits instanziiert worden sein sollte, dann klappern ++ beschwert sich über ein neu definiert friend constexpr auto counter(slot<N>)
.
Um dies zu testen, habe ich dem vorherigen Quellcode zwei weitere Zeilen hinzugefügt.
int test1 = counter(slot<11>());
int test2 = writer<11,0>::value;
Sie können alles auf Godbolt selbst sehen . Screenshot unten.
Es scheint also, dass clang ++ glaubt, etwas definiert zu haben, von dem es glaubt, dass es nicht definiert wurde. Welche Art von Kopf dreht sich, nicht wahr?
Zweite Reihe von Fragen
- Ist meine Problemumgehung überhaupt legal für C ++ oder habe ich es gerade geschafft, einen weiteren G ++ - Fehler zu entdecken?
- Wenn es legal ist, habe ich deshalb einige böse Clang ++ - Fehler entdeckt?
- Oder habe ich mich nur in die dunkle Unterwelt des undefinierten Verhaltens vertieft, also bin ich selbst der einzige, der die Schuld trägt?
Auf jeden Fall würde ich jeden herzlich willkommen heißen, der mir helfen wollte, aus diesem Kaninchenbau herauszukommen, und gegebenenfalls Erklärungen für Kopfschmerzen abgeben würde. : D.
next()
Funktion zu übergeben, aber ich kann nicht wirklich herausfinden, wie das funktioniert. Auf jeden Fall habe ich hier eine Antwort auf mein eigenes Problem gefunden: stackoverflow.com/a/60096865/566849Antworten:
Nach weiteren Untersuchungen hat sich herausgestellt, dass eine geringfügige Änderung an der
next()
Funktion vorgenommen werden kann, die dazu führt, dass der Code bei Clang ++ - Versionen über 7.0.0 ordnungsgemäß funktioniert, bei allen anderen Clang ++ - Versionen jedoch nicht mehr funktioniert.Schauen Sie sich den folgenden Code aus meiner vorherigen Lösung an.
Wenn Sie darauf achten, versuchen Sie buchstäblich , den zugeordneten Wert zu lesen
slot<N>
, fügen Sie 1 hinzu und ordnen Sie diesen neuen Wert demselben zuslot<N>
.Wenn
slot<N>
kein zugeordneter Wert vorhanden ist,slot<Y>
wird stattdessen der zugeordnete Wert abgerufen,Y
wobei der höchste Index kleiner ist alsN
derslot<Y>
mit einem zugeordneten Wert.Das Problem mit dem obigen Code ist, dass clang ++ (zu Recht, würde ich sagen?), Obwohl es unter g ++ funktioniert,
reader(0, slot<N>())
permanent alles zurückgibt, was zurückgegeben wurde, wennslot<N>
kein zugeordneter Wert vorhanden war. Dies bedeutet wiederum, dass alle Slots effektiv mit dem Basiswert verknüpft werden0
.Die Lösung besteht darin, den obigen Code in diesen zu transformieren:
Beachten Sie, dass
slot<N>()
in geändert wurdeslot<N-1>()
. Es ist sinnvoll: Wenn ich einen Wert zuordnen möchteslot<N>
, bedeutet dies, dass noch kein Wert zugeordnet ist. Daher ist es nicht sinnvoll, zu versuchen, ihn abzurufen. Außerdem möchten wir einen Zähler erhöhen, und der Wert des zugeordneten Zählersslot<N>
muss eins plus dem zugeordneten Wert seinslot<N-1>
.Eureka!
Dies bricht jedoch clang ++ Versionen <= 7.0.0.
Schlussfolgerungen
Es scheint mir, dass die ursprüngliche Lösung, die ich veröffentlicht habe, einen konzeptionellen Fehler aufweist, so dass:
Zusammenfassend funktioniert der folgende Code für alle Versionen von g ++ und clang ++.
Der Code wie er ist funktioniert auch mit msvc. Die IStGH - Compiler nicht SFINAE auslösen , wenn mit
decltype(counter(slot<N>()))
, lieber in der Lage zu bemängeln nicht zu ,deduce the return type of function "counter(slot<N>)"
weilit has not been defined
. Ich glaube, dies ist ein Fehler , der umgangen werden kann, indem SFINAE auf das direkte Ergebnis von SFINAE angewendet wirdcounter(slot<N>)
. Dies funktioniert auch auf allen anderen Compilern, aber g ++ beschließt, eine Vielzahl sehr ärgerlicher Warnungen auszuspucken, die nicht deaktiviert werden können. So könnte auch in diesem Fall#ifdef
zur Rettung kommen.Der Beweis ist auf Godbolt , siehe unten.
quelle