4 Stimmen

C++11 constexpr Funktion Compilerfehler mit ternärem bedingten Operator (?:)

Was stimmt mit diesem Stück Code nicht?

#include 

template
constexpr unsigned int Log2() {
    return (N <= 1) ? P : Log2();
}

int main()
{
    std::cout << "Log2(8) = " << Log2<8>() << std::endl;
    return 0;
}

Beim Kompilieren mit gcc Version 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5) erhalte ich folgenden Fehler:

log2.cpp: In function ‘constexpr unsigned int Log2() [with unsigned int N = 0u, unsigned int P = 1023u]’:
log2.cpp:5:38: Fehler: Vorlageneinbindetiefe überschreitet das Maximum von 1024 (verwenden Sie -ftemplate-depth=, um das Maximum zu erhöhen) instanziiere ‘constexpr unsigned int Log2() [with unsigned int N = 0u, unsigned int P = 1024u]’
log2.cpp:5:38:   rekursiv instanziiert von ‘constexpr unsigned int Log2() [with unsigned int N = 4u, unsigned int P = 1u]’
log2.cpp:5:38:   instanziiert von ‘constexpr unsigned int Log2() [with unsigned int N = 8u, unsigned int P = 0u]’
log2.cpp:10:37:   hier instanziiert

18voto

Sebastian Redl Punkte 64533

Constexpr funktioniert nicht so.

Einfach ausgedrückt müssen constexpr-Funktionen auch als Laufzeitfunktionen verfügbar sein. Stellen Sie sich vor, Sie würden das constexpr von der Funktion entfernen. Denken Sie dann darüber nach, warum es unmöglich funktionieren kann.

Der Grund ist, dass der Compiler den Körper der Funktion vollständig instantiieren muss; er kann nicht basierend auf der Bedingung im ?: entscheiden, eine Seite nicht zu instanziieren. Deshalb muss er den rekursiven Aufruf immer instantiieren, was zu einer unendlichen Rekursion führt.

Auf jeden Fall verwenden Sie constexpr falsch. Sie verwenden die alte Technik der Template-Metaprogrammierung (Übergeben von Sachen als Template-Parameter), wenn constexpr gedacht war, dies zu ersetzen. Verwenden Sie einfach normale Parameter.

constexpr unsigned Log2(unsigned n, unsigned p = 0) {
    return (n <= 1) ? p : Log2(n / 2, p + 1);
}

std::cout << "Log2(8) = " << Log2(8) << std::endl;

Bearbeitung: Ich werde versuchen, ausführlicher darauf einzugehen, wie dies funktioniert.

Wenn der Compiler Ihren Code erreicht, analysiert er die Template-Funktion und speichert sie in Template-Form. (Wie dies funktioniert, unterscheidet sich zwischen Compilern.) Bisher ist alles in Ordnung.

Als nächstes sieht der Compiler in main den Aufruf Log2<8>(). Er sieht, dass er das Template instantiieren muss, also macht er genau das: er instantiiert Log2<8, 0>. Der Körper des Funktions-Templates lautet wie folgt:

return (N <= 1) ? P : Log2();

OK, der Compiler sieht das, versucht es aber nicht auszuwerten. Warum sollte er das tun? Er instantiiert gerade ein Template, berechnet also keinen Wert. Er setzt einfach die bereitgestellten Werte ein:

return (8 <= 1) ? 0 : Log2<8/2,0+1>();

Hm, hier gibt es eine weitere Template-Instantiierung. Es spielt keine Rolle, dass es sich um einen bedingten Ausdruck handelt oder dass die linke Seite bekannt sein könnte. Die Template-Instantiierung muss vollständig sein. Also berechnet er die Werte für die neue Instantiierung und instantiiert dann Log2<4, 1>:

return (4 <= 1) ? 1 : Log2<4/2,1+1>();

Und das Spiel beginnt von vorne. Es gibt eine Template-Instantiierung darin, und es ist Log2<2, 2>:

return (2 <= 1) ? 2 : Log2<2/2,2+1>();

Und wieder Log2<1,3>():

return (1 <= 1) ? 3 : Log2<1/2,3+1>();

Habe ich erwähnt, dass dem Compiler die semantische Bedeutung dieser Dinge egal ist? Es ist einfach ein weiteres Template, das instantiiert werden soll: Log2<0,4>:

return (0 <= 1) ? 4 : Log2<0/2,4+1>();

Und dann Log2<0,5>:

return (0 <= 1) ? 5 : Log2<0/2,5+1>();

Und so weiter, und so weiter. Irgendwann erkennt der Compiler, dass die Rekrusion nie endet, und gibt auf. Aber zu keinem Zeitpunkt sagt er: "Moment, die Bedingung dieses ternären Operators ist falsch, ich muss die rechte Seite nicht instantiieren." Denn das erlaubt ihm der C++-Standard nicht. Der Funktionskörper muss vollständig instantiiert werden.

Schauen Sie sich jetzt meine Lösung an. Hier gibt es kein Template. Es gibt nur eine Funktion. Der Compiler sieht sie und denkt: "Hey, hier ist eine Funktion. Klasse, lass mich hier einen Aufruf zu dieser Funktion einfügen." Und dann denkt er irgendwann (es kann sofort sein, es kann viel später sein, je nach Compiler): "Moment mal, diese Funktion ist `constexpr` und ich kenne die Parameterwerte, lass mich das auswerten." Jetzt bewertet er also vielleicht (aber in diesem Fall nicht gezwungenermaßen) `Log2(8, 0)`. Denken Sie an den Körper:

return (n <= 1) ? p : Log2(n / 2, p + 1);

"OK", sagt der Compiler, "ich möchte nur wissen, was diese Funktion zurückgibt. Mal sehen, `8 <= 1` ist falsch, also schaue ich auf die rechte Seite. `Log2(4, 1)`, huh? Lass mich das anschauen. OK, `4 <= 1` ist auch falsch, also muss es `Log2(2, 2)` sein. Was ist das, `2 <= 1`? Auch falsch, also ist es `Log2(1, 3)`. Hey, `1 <= 1` ist wahr, also nehme ich das `3` und gebe es zurück. Den ganzen Weg den Aufrufstapel hoch."

Also kommt er auf die Antwort 3. Es gerät nicht in eine endlose Rekursion, weil es die Funktion mit vollem Wissen über Werte und Semantik auswertet, und nicht nur stupide ASTs aufbaut.

Ich hoffe, das hilft.

5voto

stefan Punkte 9985

Wie bereits von anderen gesagt: Der Compiler wird keinen bedingten Ausdruck wie if/else oder einen ternären Operator ?: auswerten. Es gibt jedoch immer noch einen Weg, um diesen bedingten Ausdruck zur Kompilierungszeit zu machen:

#include      // size_t ist kürzer als unsigned int, es ist in diesem Fall eine Geschmacksfrage
#include     // um unsere Ergebnisse anzuzeigen
#include  // benötigt für das mächtige std::enable_if

template
constexpr typename std::enable_if<(N <= 1), size_t>::type Log2()
{
   return P;
}

template
constexpr typename std::enable_if::type Log2()
{
   return Log2();
}

int main()
{
   std::cout << Log2<1>() << "\n";
   std::cout << Log2<2>() << "\n";
   std::cout << Log2<4>() << "\n";
   std::cout << Log2<8>() << "\n";
   std::cout << Log2<16>() << "\n";
}

Was das tut, ist ziemlich offensichtlich: Wenn N <= 1 ist, sollte der erste Zweig ausgewertet werden, also Log2<0, P>() und Log2<1, P>() sollten zu P ausgewertet werden. Wenn N <= 1 ist, wird die obere Methode aktiviert, da dieser Funktionsheader gültig ist. Für alles andere, d.h. N >= 2 oder !(N <= 1), müssen wir rekursiv vorgehen, was durch die zweite Methode erfolgt.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X