Const Überladung unerwartet in gcc aufgerufen. Compiler-Fehler oder Kompatibilitätsfix?

8

Wir haben eine viel größere Anwendung, die auf der Vorlagenüberladung von char- und const char-Arrays beruht. In gcc 7.5, clang und visual studio gibt der folgende Code für alle Fälle "NON-CONST" aus. Für gcc 8.1 und höher wird die Ausgabe jedoch unten angezeigt:

#include <iostream>

class MyClass
{
public:
    template <size_t N>
    MyClass(const char (&value)[N])
    {
        std::cout << "CONST " << value << '\n';
    }

    template <size_t N>
    MyClass(char (&value)[N])
    {
        std::cout << "NON-CONST " << value << '\n';
    }
};

MyClass test_1()
{
    char buf[30] = "test_1";
    return buf;
}

MyClass test_2()
{
    char buf[30] = "test_2";
    return {buf};
}

void test_3()
{
    char buf[30] = "test_3";
    MyClass x{buf};
}

void test_4()
{
    char buf[30] = "test_4";
    MyClass x(buf);
}

void test_5()
{
    char buf[30] = "test_5";
    MyClass x = buf;
}

int main()
{
    test_1();
    test_2();
    test_3();
    test_4();
    test_5();
}

Die Ausgabe von gcc 8 und 9 (von godbolt) lautet:

CONST test_1
NON-CONST test_2
NON-CONST test_3
NON-CONST test_4
NON-CONST test_5

Dies scheint mir ein Compiler-Fehler zu sein, aber ich denke, es könnte sich um ein anderes Problem im Zusammenhang mit einem Sprachwechsel handeln. Weiß jemand definitiv?

Rob L.
quelle
Haben Sie versucht, mit verschiedenen C ++ - Standardversionen zu kompilieren?
n314159
1
g ++ und clang ++ unterscheiden sich: godbolt.org/z/g3cCBL
Ted Lyngmo
@ n314159 Gute Frage, ich habe es gerade getan. -std = c ++ 11 -std = c ++ 14 -std = c ++ 17 und -std = c ++ 2a erzeugen alle das gleiche "schlechte" Ergebnis. Kompiliert nicht mit -std = c ++ 03.
Rob L
1
@ TedLyngmo Ja, ich habe festgestellt, dass Clang wie erwartet funktioniert, ebenso wie Visual Studio.
Rob L

Antworten:

6

Wenn Sie einen einfachen ID-Ausdruck von einer Funktion zurückgeben (die ein lokales Funktionsobjekt festgelegt hat), muss der Compiler die Überladungsauflösung zweimal durchführen. Zuerst behandelt es es so, als wäre es ein Wert und kein Wert. Nur wenn die erste Überlastungsauflösung fehlschlägt, wird sie erneut mit dem Objekt als Wert ausgeführt.

[class.copy.elision]

3 In den folgenden Kopierinitialisierungskontexten kann anstelle einer Kopieroperation eine Verschiebungsoperation verwendet werden:

  • Wenn der Ausdruck in einer return-Anweisung ein (möglicherweise in Klammern stehender) ID-Ausdruck ist, der ein Objekt mit automatischer Speicherdauer benennt, das im body oder in der parameter-declaration-Klausel der innersten umschließenden Funktion oder des Lambda-Ausdrucks deklariert ist, oder

  • ...

Die Überladungsauflösung zur Auswahl des Konstruktors für die Kopie wird zuerst so ausgeführt, als ob das Objekt durch einen r-Wert gekennzeichnet wäre. Wenn die erste Überlastungsauflösung fehlschlägt oder nicht durchgeführt wurde oder wenn der Typ des ersten Parameters des ausgewählten Konstruktors keine r-Wert-Referenz auf den Objekttyp ist (möglicherweise lebenslaufqualifiziert), wird die Überlastungsauflösung erneut durchgeführt, wobei das Objekt als betrachtet wird lWert. [Hinweis: Diese zweistufige Überlastungsauflösung muss unabhängig davon durchgeführt werden, ob eine Kopierelision auftritt. Es bestimmt den aufzurufenden Konstruktor, wenn keine Elision durchgeführt wird, und der ausgewählte Konstruktor muss auch dann zugänglich sein, wenn der Aufruf beendet ist. - Endnote]

Wenn wir eine rvalue-Überladung hinzufügen würden,

template <size_t N>
MyClass (char (&&value)[N])
{
    std::cout << "RVALUE " << value << '\n';
}

Die Ausgabe wird

RVALUE test_1
NON-CONST test_2
NON-CONST test_3
NON-CONST test_4
NON-CONST test_5

und das wäre richtig. Was nicht korrekt ist, ist das Verhalten von GCC, wie Sie es sehen. Die erste Überlastungslösung wird als Erfolg gewertet. Dies liegt daran, dass eine Konstantenwertreferenz möglicherweise an einen Wert gebunden ist. Der Text wird jedoch ignoriert, "oder wenn der Typ des ersten Parameters des ausgewählten Konstruktors kein r-Wert-Verweis auf den Objekttyp ist" . Demnach muss das Ergebnis der ersten Überlastungsauflösung verworfen und erneut ausgeführt werden.

Nun, das ist sowieso die Situation bis C ++ 17. Der aktuelle Standardentwurf sagt etwas anderes.

Wenn die erste Überlastungsauflösung fehlschlägt oder nicht durchgeführt wurde, wird die Überlastungsauflösung erneut durchgeführt, wobei der Ausdruck oder Operand als l-Wert betrachtet wird.

Der Text von bis zu C ++ 17 wurde entfernt. Es ist also ein Zeitreisefehler. GCC implementiert das C ++ 20-Verhalten, dies jedoch auch dann, wenn der Standard C ++ 17 ist.

Geschichtenerzähler - Unslander Monica
quelle
Vielen Dank! Es scheint, dass Sie mir auch eine Problemumgehung gegeben haben, die in diesem speziellen Fall funktioniert, und eine rvalue-Überladung hinzugefügt haben. Ich kann das nur mit char & version identisch machen.
Rob L
1
@RobL - Gerne helfen. Bitte beachten Sie jedoch, dass die Situation etwas nuancierter ist, als ich ursprünglich gedacht hatte. Der Text hat sich tatsächlich geändert. Ich bin froh, dass ich nachgesehen habe.
StoryTeller - Unslander Monica
Ich denke, das bedeutet, dass die clang++C ++ 20-Implementierung auch nicht korrekt ist, da sie in allen Fällen die NON-CONST-Version im Originalcode verwendet.
Ted Lyngmo
2
@ TedLyngmo - Bei Zeitreiseproblemen ist es wirklich eine Frage des Zeitpunkts. Ich kann mir vorstellen, dass die Clang-Entwickler diese Änderung einfach nicht umgesetzt haben. Ich werde es nicht als Fehler an sich bezeichnen. GCC, das das Neue in C ++ 17 macht, ist wahrscheinlich ein Fehler. Hängt davon ab, wie diese Änderung in den Standard eingegeben wurde. Ich glaube nicht, dass es einen Fehlerbericht gab, bei dem dies rückwirkend geändert werden müsste, daher gehe ich davon aus, dass es sich um einen GCC-Fehler handelt. Die Unterstützung mehrerer Standards ist eine differenzierte Arbeit.
StoryTeller - Unslander Monica
1
@RobL - Es ist ein lokales Funktionsobjekt. Nach der return-Anweisung ist es weg. Dies ist ein bewusster Optimierungspunkt. Anstatt zu kopieren, kann das Objekt kannibalisiert werden. Die Standardpraxis besteht darin, Typen zu schreiben, die sich für jede übergebene Wertekategorie korrekt verhalten.
StoryTeller - Unslander Monica
0

Es gibt eine Debatte darüber, ob dies "intuitives Verhalten" in den Kommentaren ist oder nicht, daher dachte ich, ich würde die Gründe für dieses Verhalten auf den Prüfstand stellen.

Bei CPPCON wurde ein ziemlich netter Vortrag gehalten, der mir dies etwas klarer macht { talk , slide ]. Was bedeutet im Grunde eine Funktion, die eine nicht konstante Referenz verwendet? Dass das Eingabeobjekt gelesen / geschrieben werden muss . Noch stärker bedeutet dies, dass ich beabsichtige, dieses Objekt zu ändern. Diese Funktion hat Nebenwirkungen . Ein const ref impliziert schreibgeschützt , und rvalue ref bedeutet, dass ich die Ressourcen nehmen kann . Wenn Sie test_1()am Ende das aufrufen würden,NON-CONST Konstruktor aufgerufen würde, würde dies bedeuten, dass ich beabsichtige, dieses Objekt zu ändern, obwohl es nach Abschluss nicht mehr existiert.Was (glaube ich) ein Fehler wäre (ich denke an einen Fall, in dem die Bindung einer Referenz während der Initialisierung davon abhängt, ob das übergebene Argument const ist oder nicht).

Was mich ein bisschen mehr beschäftigt, ist die Subtilität, die von eingeführt wurde test_2(). Hier Kopie-list-Initialisierung stattfindet , statt den Regeln in Bezug auf [class.copy.elision] zitiert oben. Jetzt sagen Sie wirklich, dass Sie ein Objekt vom Typ MyClass zurückgeben, als hätte ich es initialisiert buf, sodass das NON-CONSTVerhalten aufgerufen wird. Ich habe Init-Listen immer als Mittel gesehen, um prägnanter zu sein, aber hier machen die geschweiften Klammern einen signifikanten semantischen Unterschied. Dies wäre wichtiger, wenn die Konstruktoren für MyClasseine große Anzahl von Argumenten verwenden würden. Angenommen, Sie möchten ein erstellen buf, es ändern und dann mit der großen Anzahl von Argumenten zurückgeben und das CONSTVerhalten aufrufen . Angenommen, Sie haben die Konstruktoren:

template <size_t N>
MyClass(const char (&value)[N], int)
{
    std::cout << "CONST int " << value << '\n';
}

template <size_t N>
MyClass(char (&value)[N], int)
{
    std::cout << "NON-CONST int " << value << '\n';
}

Und testen:

MyClass test_0() {
    char buf[30] = "test_0";
    return {buf,0};
}

Godbolt sagt uns, dass wir NON-CONSTVerhalten bekommen , obwohl CONSTes wahrscheinlich das ist, was wir wollen (nachdem Sie die Cool-Hilfe zur Funktionsargument-Semantik getrunken haben). Aber jetzt macht die Initialisierung der Kopierliste nicht das, was wir möchten. Der folgende Test macht meinen Standpunkt besser:

MyClass test_0() {
    char buf[30] = "test_0";
    buf[0] = 'T';
    const char (&bufR)[30]{buf};
    return {bufR,0};
}
// OUTPUT: CONST int Test_0

Um nun die richtige Semantik bei der Initialisierung der Kopierliste zu erhalten, muss der Puffer am Ende "zurückgebunden" werden. Ich denke, wenn das Ziel wäre, dass dieses Objekt ein anderes MyClassObjekt initialisiert , NON-CONSTwäre es in Ordnung , nur das Verhalten in der Rückgabekopierliste zu verwenden, wenn der Verschiebungs- / Kopierkonstruktor das entsprechende Verhalten aufruft, aber das klingt langsam hübsch zart.

Nathan Chappell
quelle