C ++ - Kompilierungszeitzähler, überarbeitet

28

TL; DR

Bevor Sie versuchen, diesen ganzen Beitrag zu lesen, sollten Sie Folgendes wissen:

  1. Ich habe eine Lösung für das vorgestellte Problem gefunden , bin aber immer noch gespannt, ob die Analyse korrekt ist.
  2. Ich habe die Lösung in eine fameta::counterKlasse gepackt, die einige verbleibende Macken löst. Sie können es auf Github finden ;
  3. 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

  1. Stimmen Sie meiner Diagnose tatsächlich zu?
  2. Wenn ja, ist dieses neue Verhalten vom Standard vorgeschrieben? War der vorherige ein Fehler?
  3. 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.

Geben Sie hier die Bildbeschreibung ein

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 Nund Idass 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.

clang ++ glaubt, dass es etwas definiert hat, von dem es glaubt, dass es es nicht definiert hat

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

  1. Ist meine Problemumgehung überhaupt legal für C ++ oder habe ich es gerade geschafft, einen weiteren G ++ - Fehler zu entdecken?
  2. Wenn es legal ist, habe ich deshalb einige böse Clang ++ - Fehler entdeckt?
  3. 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.

Fabio A.
quelle
2
Siehe auch
HolyBlackCat
2
Wie ich mich erinnere, haben die Leute des Standardkomitees die klare Absicht, Konstrukte zur Kompilierungszeit jeglicher Art, Form oder Gestalt zu verbieten, die nicht jedes Mal, wenn sie (hypothetisch) bewertet werden, genau das gleiche Ergebnis liefern. Es könnte sich also um einen Compiler-Fehler handeln, um einen Fall "schlecht geformt, keine Diagnose erforderlich" oder um etwas, das der Standard übersehen hat. Trotzdem widerspricht es dem "Geist des Standards". Tut mir leid. Ich hätte auch gerne Zeitzähler kompiliert.
Bolov
@HolyBlackCat Ich muss gestehen, dass es mir sehr schwer fällt, mich mit diesem Code auseinanderzusetzen. Es sieht so aus, als könnte die Notwendigkeit vermieden werden, eine monoton ansteigende Zahl als Parameter explizit an die 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/566849
Fabio A.
@ FabioA. Auch ich verstehe diese Antwort nicht ganz. Seit ich diese Frage gestellt habe, ist mir klar geworden, dass ich constexpr-Zähler nie wieder berühren möchte.
HolyBlackCat
Während dies ein lustiges kleines Gedankenexperiment ist, müsste jemand, der diesen Code tatsächlich verwendet hat, damit rechnen, dass er in zukünftigen Versionen von C ++ nicht funktioniert, oder? In diesem Sinne definiert sich das Ergebnis als Fehler.
Aziuth

Antworten:

5

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.

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

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 zu slot<N> .

Wenn slot<N>kein zugeordneter Wert vorhanden ist, slot<Y>wird stattdessen der zugeordnete Wert abgerufen, Ywobei der höchste Index kleiner ist als Nder slot<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, wenn slot<N>kein zugeordneter Wert vorhanden war. Dies bedeutet wiederum, dass alle Slots effektiv mit dem Basiswert verknüpft werden 0.

Die Lösung besteht darin, den obigen Code in diesen zu transformieren:

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

Beachten Sie, dass slot<N>()in geändert wurde slot<N-1>(). Es ist sinnvoll: Wenn ich einen Wert zuordnen möchte slot<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ählers slot<N>muss eins plus dem zugeordneten Wert sein slot<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:

  • g ++ hat eine Eigenart / Fehler / Entspannung, die mit dem Fehler meiner Lösung behoben wird und schließlich dazu führt, dass der Code trotzdem funktioniert.
  • Clang ++ - Versionen> 7.0.0 sind strenger und mögen den Fehler im Originalcode nicht.
  • Clang ++ - Versionen <= 7.0.0 haben einen Fehler, der dazu führt, dass die korrigierte Lösung nicht funktioniert.

Zusammenfassend funktioniert der folgende Code für alle Versionen von g ++ und 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

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>)"weil it has not been defined. Ich glaube, dies ist ein Fehler , der umgangen werden kann, indem SFINAE auf das direkte Ergebnis von SFINAE angewendet wird counter(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 #ifdefzur Rettung kommen.

Der Beweis ist auf Godbolt , siehe unten.

Geben Sie hier die Bildbeschreibung ein

Fabio A.
quelle
2
Ich denke, diese Antwort schließt das Thema, aber ich würde trotzdem gerne wissen, ob ich in meiner Analyse Recht habe. Deshalb werde ich warten, bevor ich meine eigene Antwort als richtig akzeptiere, in der Hoffnung, dass jemand anderes vorbeikommt und mir einen besseren Hinweis gibt oder eine Bestätigung. :)
Fabio A.