Soll ich const-Objekte zurückgeben?

78

Effective C++Verwenden Sie in Punkt 03 nach Möglichkeit const.

class Bigint
{
  int _data[MAXLEN];
  //...
public:
  int& operator[](const int index) { return _data[index]; }
  const int operator[](const int index) const { return _data[index]; }
  //...
};

const int operator[]macht einen Unterschied von int& operator[].

Aber was ist mit:

int foo() { }

und

const int foo() { }

Scheint so, als wären sie gleich.

Meine Frage ist, warum wir const int operator[](const int index) conststatt verwenden int operator[](const int index) const?

abcdabcd987
quelle
4
Gute Frage - würde die Methode in diesem Fall in beiden Fällen normalerweise keine Referenz zurückgeben?
Ken Wayne VanderLinde
Gute Frage. Ich schlage vor, Sie ändern es in int operator[](int i)versus const int operator[](const int i).
Alexander Chertov
1
@ KenWayneVanderLinde, nein. Die aufgerufene Methode hängt davon ab, ob wir ein Bigint&oder haben a const Bigint&.
Alexander Chertov
1
@AlexanderChertov nein: er bittet um den Rückgabetyp nicht mit dem Argumente , Typ, so dass die Frage auf , dass nur konzentrieren sollte
stijn
Warum? Ich persönlich frage mich, warum jemand einen const int iStreiter haben sollte.
Alexander Chertov

Antworten:

87

Lebenslaufqualifizierer der obersten Ebene für Rückgabetypen, die nicht vom Klassentyp sind, werden ignoriert. Was bedeutet, dass selbst wenn Sie schreiben:

int const foo();

Der Rückgabetyp ist int. Wenn der Rückgabetyp eine Referenz ist, ist dies natürlich constnicht mehr die oberste Ebene, und die Unterscheidung zwischen:

int& operator[]( int index );

und

int const& operator[]( int index ) const;

ist wichtig. (Beachten Sie auch, dass in Funktionsdeklarationen wie den oben genannten auch alle Lebenslaufqualifizierer der obersten Ebene ignoriert werden.)

Die Unterscheidung ist auch für Rückgabewerte vom Klassentyp relevant: Wenn Sie zurückgeben T const, kann der Aufrufer keine nicht konstanten Funktionen für den zurückgegebenen Wert aufrufen, z.

class Test
{
public:
    void f();
    void g() const;
};

Test ff();
Test const gg();

ff().f();             //  legal
ff().g();             //  legal
gg().f();             //  **illegal**
gg().g();             //  legal
James Kanze
quelle
6
ff und gg werden als Funktionen deklariert, die Test zurückgeben. Wenn ja, sollten die Aufrufe ff (). F () usw. sein. Wenn ff eine Instanz der Testklasse ist, sollten die Klammern aus der Deklaration entfernt werden (+1 jedoch :)
user396672
@ user396672 (und BJ ...) Ja. Ich habe das ()nach ffund vergessen gg. Ich werde sie hinzufügen. Danke für die Verbesserung.
James Kanze
+1 für alles nach "Die Unterscheidung ist auch für Rückgabewerte des Klassentyps relevant". Diese Anwendung von constist nützlich, aber viele Leute scheinen sie entweder zu übersehen oder darauf zu bestehen, dass die Rückgabe eines constWertes ohne Nutzen ist.
underscore_d
4
−1 „Lebenslaufqualifizierer der obersten Ebene für Rückgabetypen, die nicht vom Klassentyp sind, werden ignoriert“ ist falsch. Sie sind immer noch Teil des Funktionstyps. Die Lebenslaufqualifizierung eines Wertes vom Nicht-Klassentyp wird jedoch ignoriert, so dass ein Aufruf einer Funktion int const, die beispielsweise zurückgibt , einem Aufruf dieser Funktion entspricht, der für die Rückgabe geändert wurde int. Standardese in C ++ 11 §3.10 / 4, „Klassenwerte können cv-qualifizierte Typen haben; Nicht-Klassen-Werte haben immer cv-unqualifizierte Typen. “
Prost und hth. - Alf
38

Sie sollten klar zwischen der Verwendung von const für Rückgabewerte, Parameter und der Funktion selbst unterscheiden.

Rückgabewerte

  • Wenn die Funktion als Wert zurückgegeben wird , spielt die Konstante keine Rolle, da die Kopie des Objekts zurückgegeben wird. In C ++ 11 ist dies jedoch mit Verschiebungssemantik von Bedeutung.
  • Es spielt auch für Basistypen keine Rolle, da sie immer kopiert werden.

Überlegen Sie const std::string SomeMethod() const. Die (std::string&&)Funktion kann nicht verwendet werden, da ein nicht konstanter Wert erwartet wird. Mit anderen Worten, die zurückgegebene Zeichenfolge wird immer kopiert.

  • Wenn die Funktion als Referenz zurückgegeben wird , wird constdas zurückgegebene Objekt vor Änderungen geschützt.

Parameter

  • Wenn Sie einen Parameter als Wert übergeben , verhindert die Konstante die Änderung des angegebenen Werts nach Funktion in Funktion . Die Originaldaten des Parameters können ohnehin nicht geändert werden, da Sie nur eine Kopie haben.
  • Da die Kopie immer erstellt wird, hat die Konstante nur eine Bedeutung für den Funktionskörper und wird daher nur in der Funktionsdefinition und nicht in der Deklaration (Schnittstelle) überprüft.
  • Wenn Sie einen Parameter als Referenz übergeben , gilt dieselbe Regel wie bei den Rückgabewerten

Funktion selbst

  • Wenn die Funktion constam Ende steht, kann sie nur andere constFunktionen ausführen und keine Änderung oder Änderung von Klassendaten zulassen. Wenn es also als Referenz zurückgegeben wird, muss die zurückgegebene Referenz const sein. Es können nur const-Funktionen für ein Objekt oder einen Verweis auf ein Objekt aufgerufen werden, das const selbst ist. Auch die veränderlichen Felder können geändert werden.
  • Das vom Compiler erstellte Verhalten ändert den thisVerweis auf T const*. Die Funktion kann immer const_cast this, aber dies sollte natürlich nicht durchgeführt werden und wird als unsicher angesehen.
  • Es ist natürlich sinnvoll, diesen Bezeichner nur für Klassenmethoden zu verwenden. Globale Funktionen mit const am Ende lösen einen Kompilierungsfehler aus.

Fazit

Wenn Ihre Methode die Klassenvariablen nicht ändert und niemals ändert, markieren Sie sie als const und stellen Sie sicher, dass alle erforderlichen Kriterien erfüllt sind. Dadurch kann mehr sauberer Code geschrieben werden, wodurch er konstant bleibt . Allerdings setzt constüberall , ohne es zu jedem Gedanken sicherlich zu geben , ist nicht der Weg zu gehen.

Bartek Banachewicz
quelle
4
Zwei Korrekturen: Erstens wird bei der Rückgabe nach Wert die oberste Ebene für Nichtklassentypenconst ignoriert . Es ist wichtig für Klassentypen. Bei Parametern, die als Wert übergeben werden, wird die oberste Ebene in Funktionsdeklarationen ignoriert. Es hat nur Bedeutung in der Definition. const
James Kanze
Ich werde diesen Kommentar in meine Antwort aufnehmen, wenn Sie nichts dagegen haben. Ich denke, dann wird es viel klarer.
Bartek Banachewicz
Wie für consteine Funktion: Es ändert den Typ von this, was ist T const*, anstatt T*. Alle anderen Effekte ergeben sich daraus. (Und es verhindert nicht jede Änderung des Objekts: mutableDaten können immer noch geändert werden, und die Funktion kann const legal wegwerfen und alles ändern, was sie wünscht.)
James Kanze
"Wenn die Funktion als Referenz zurückgegeben wird, schützt sie das zurückgegebene Objekt vor Änderungen" false
NoSenseEtAl
Autsch, ich meinte ", [const] schützt das Objekt" ... Ich werde das korrigieren, um besser lesbar zu sein.
Bartek Banachewicz
30

Es gibt wenig Wert bei der Zugabe constQualifikationen nicht-reference / Nicht-Zeiger rvalues, und kein Punkt in Zugabe zu Einbauten.

Im Fall von benutzerdefinierten Typen , eine constwird Qualifizierungs Anrufer verhindert eine nicht Aufruf constMemberfunktion auf dem Objekt zurückgegeben. Zum Beispiel gegeben

const std::string foo();
      std::string bar();

dann

foo().resize(42);

wäre verboten, während

bar().resize(4711);

wäre erlaubt.

Für Einbauten wie intmacht dies überhaupt keinen Sinn, da solche Werte sowieso nicht geändert werden können.

(Ich erinnere mich jedoch an Effective C ++ , in dem es darum ging , den Rückgabetyp operator=()einer constReferenz zu erstellen, und dies ist zu berücksichtigen.)


Bearbeiten:

Es scheint, dass Scott diesen Rat tatsächlich gegeben hat . Wenn ja, dann finde ich es aus den oben genannten Gründen selbst für C ++ 98 und C ++ 03 fraglich . Für C ++ 11 halte ich es eindeutig für falsch , wie Scott selbst herausgefunden zu haben scheint. In den Errata für Effective C ++, 3. Aufl. schreibt er (oder zitiert andere, die sich beschwert haben):

Der Text impliziert, dass alle By-Value-Rückgaben const sein sollten, aber Fälle, in denen Nicht-Const-By-Value-Rückgaben ein gutes Design haben, sind nicht schwer zu finden, z. B. Rückgabetypen von std :: vector, bei denen Aufrufer Swap mit einem leeren Vektor verwenden um den Inhalt des Rückgabewerts zu "greifen", ohne ihn zu kopieren.

Und später:

Durch das Deklarieren von Rückgabewerten für By-Value-Funktionen const wird verhindert, dass sie an rvalue-Referenzen in C ++ 0x gebunden werden. Da rvalue-Referenzen dazu dienen, die Effizienz von C ++ - Code zu verbessern, ist es wichtig, das Zusammenspiel von const-Rückgabewerten und die Initialisierung von rvalue-Referenzen bei der Angabe von Funktionssignaturen zu berücksichtigen.

sbi
quelle
1
Ich liebe die Erwähnung von Errata mit Scotts Rat. Hilft wirklich dabei, etwas Licht in das zu bringen, was für Leute, die neu in Effective C ++ sind, sonst verwirrend sein könnte
siehe
2
@sehe: Ich habe Scott eine E-Mail geschickt, in der ich nach diesem Problem gefragt habe, und seine Antwort war, dass diese Errata-Kommentare seine aktuellen Überlegungen zu diesem Problem widerspiegeln.
sbi
17

Sie könnten den Punkt von Meyers Rat verpassen. Der wesentliche Unterschied besteht im constModifikator für die Methode.

Dies ist eine nicht konstante Methode (Hinweis Nr. constAm Ende), was bedeutet, dass der Status der Klasse geändert werden darf.

int& operator[](const int index)

Dies ist eine const-Methode (Hinweis constam Ende)

const int operator[](const int index) const

Was ist mit den Parametertypen und dem Rückgabewert? Es gibt einen geringfügigen Unterschied zwischen intund const int, der jedoch für den Hinweis nicht relevant ist. Was Sie beachten sollten, ist, dass die Nicht-Konstanten-Überladung zurückgegeben wird, int&was bedeutet, dass Sie sie beispielsweise zuweisen können num[i]=0, und dass die Konstanten-Überladung einen nicht modifizierbaren Wert zurückgibt (unabhängig davon, ob der Rückgabetyp intoder ist const int).

Meiner persönlichen Meinung nach , wenn ein Objekt übergeben wird durch Wert , constModifikator ist überflüssig. Diese Syntax ist kürzer und erreicht das gleiche

int& operator[](int index);
int operator[](int index) const;
Andrey
quelle
5
Eigentlich geht es um die Rückgabe eines constWertes.
Juanchopanza
@juanchopanza: Ich bezweifle, dass OP genau das gemeint hat. Vergleichen Sie sein fooBeispiel mit einem operator[]Beispiel. Er könnte den Punkt übersehen, an dem der constModifikator für die Methode den Unterschied macht, nicht den Rückgabetyp.
Andrey
13

Der Hauptgrund für die Rückgabe von Werten als const ist, dass Sie so etwas nicht sagen können foo() = 5;. Dies ist eigentlich kein Problem bei primitiven Typen, da Sie keine Werte für primitive Typen zuweisen können, aber es ist ein Problem bei benutzerdefinierten Typen (z. B. (a + b) = c;bei überladenen Typen operator+).

Ich habe die Rechtfertigung dafür immer als ziemlich schwach empfunden. Sie können niemanden aufhalten, der beabsichtigt, umständlichen Code zu schreiben, und diese besondere Art von Zwang hat meiner Meinung nach keinen wirklichen Vorteil.

Mit C ++ 11 schadet diese Redewendung tatsächlich sehr : Die Rückgabe von Werten als const verhindert Verschiebungsoptimierungen und sollte daher nach Möglichkeit vermieden werden. Grundsätzlich würde ich dies jetzt als Anti-Muster betrachten.

Hier ist ein tangential verwandter Artikel zu C ++ 11.

Kerrek SB
quelle
Was ist mit der Aussage, dass es sinnvoll ist, den Rückgabewert zu ändern? Wie MyClass.isInitialized (). Wenn der Aufrufer den Rückgabewert ändert, würde ich bei der Codeüberprüfung WTF verwenden.
NoSenseEtAl
@NoSenseEtAl: Wer soll ich wissen, was sinnlos ist?
Nach
Ich habe ein Beispiel hinzugefügt. Zu meinem letzten Kommentar. Und Sie sind Designer der Klasse, damit Sie den Operator = Ihrer Rückgabetypen verstehen: D. Sie wissen also, ob es für den Aufrufer bedeutungslos ist, das Ergebnis Ihrer Methoden zu ändern.
NoSenseEtAl
9

Als dieses Buch geschrieben wurde, war der Rat von geringem Nutzen, diente jedoch dazu, den Benutzer beispielsweise daran zu hindern, zu schreiben foo() = 42;und zu erwarten , dass er etwas Beständiges ändert.

Im Fall von operator[]kann dies etwas verwirrend sein, wenn Sie nicht auch eine Nichtüberladung constangeben, die eine Nichtreferenz zurückgibt const, obwohl Sie diese Verwirrung möglicherweise verhindern können, indem Sie eine constReferenz oder ein Proxyobjekt anstelle eines Werts zurückgeben.

constHeutzutage ist es ein schlechter Rat, da Sie das Ergebnis nicht an eine (Nicht- ) Wertreferenz binden können.

(Wie in den Kommentaren ausgeführt, ist die Frage bei der Rückgabe eines primitiven Typs wie " int, da die Sprache Sie daran hindert, einem rvaluesolchen Typ zuzuweisen, umstritten. Ich spreche über den allgemeineren Fall, der die Rückgabe benutzerdefinierter Typen umfasst. )

Mike Seymour
quelle
10
Sein Beispiel kehrte zurück int. foo() = 42;ist illegal, unabhängig davon, ob der Rückgabetyp deklariert ist intoder int const. Ich glaube nicht, dass Scott für die Verwendung von constStellen wie Rückgabetypen und Parametern in Funktionsdeklarationen argumentiert hat , wo sie vom Compiler ignoriert werden.
James Kanze
@ JamesKanze: Danke, dass du darauf hingewiesen hast; Ich hätte klarstellen sollen, dass ich allgemeiner sprach.
Mike Seymour
2

Für primitive Typen (wie int) spielt die Konstanz des Ergebnisses keine Rolle. Bei Klassen kann sich das Verhalten ändern. Beispielsweise können Sie möglicherweise keine nicht konstante Methode für das Ergebnis Ihrer Funktion aufrufen:

class Bigint {
    const C foo() const { ... }
     ...
}

Bigint b;
b.foo().bar();

Das Obige ist verboten, wenn bar()es sich um eine const-Member-Funktion von handelt C. Wählen Sie im Allgemeinen, was Sinn macht.

Grzegorz Herman
quelle
0

Schau dir das an:

const int operator[](const int index) const

das constam Ende der Erklärung. Es beschreibt, dass diese Methode für Konstanten aufgerufen werden kann.

In der anderen Hand, wenn Sie nur schreiben

int& operator[](const int index)

Es kann nur für Instanzen ohne Konstante aufgerufen werden und bietet außerdem:

big_int[0] = 10;

Syntax.

Hauleth
quelle
0

Eine Ihrer Überladungen besteht darin, einen Verweis auf ein Element im Array zurückzugeben, das Sie dann ändern können.

int& operator[](const int index) { return _data[index]; }

Die andere Überladung gibt einen Wert zurück, den Sie verwenden können.

const int operator[](const int index) const { return _data[index]; }

Da Sie jede dieser Überladungen auf dieselbe Weise aufrufen, wird der Wert bei Verwendung niemals geändert.

int foo = myBigInt[1]; // Doesn't change values inside the object.
myBigInt[1] = 2; // Assigns a new value at index `
Ästhet
quelle
0

Im Beispiel des Array-Indexierungsoperators ( operator[]) macht es einen Unterschied.

Mit

int& operator[](const int index) { /* ... */ }

Sie können die Indizierung verwenden, um den Eintrag im Array direkt zu ändern, z. B. wie folgt:

mybigint[3] = 5;

Der zweite const int operator[](const int)Operator wird nur zum Abrufen des Werts verwendet.

Als Rückgabewert von Funktionen spielt es jedoch für einfache Typen wie z. B. intkeine Rolle. Wenn es darauf ankommt, geben Sie komplexere Typen zurück, z. B. a std::vector, und möchten nicht, dass der Aufrufer der Funktion den Vektor ändert.

Ein Programmierer
quelle
0

Der constRückgabetyp ist hier nicht so wichtig. Da int Provisorien nicht veränderbar sind, gibt es keine offensichtlichen Unterschiede im Umgang mit intvs const int. Sie würden einen Unterschied sehen, wenn Sie ein komplexeres Objekt verwenden würden, das geändert werden könnte.

Ken Wayne VanderLinde
quelle
0

Es gibt einige gute Antworten, die sich auf die Technik der beiden Versionen beziehen. Für einen primitiven Wert macht es keinen Unterschied.

Allerdings habe ich immer als constfür den Programmierer , dort zu sein , anstatt die Compiler. Wenn Sie schreiben const, sagen Sie ausdrücklich "das sollte sich nicht ändern". Seien wir ehrlich, constkann normalerweise sowieso umgangen werden, oder?

Wenn Sie a zurückgeben const, teilen Sie dem Programmierer, der diese Funktion verwendet, mit, dass sich der Wert nicht ändern soll. Und wenn er es ändert, macht er wahrscheinlich etwas falsch, weil er / sie es nicht muss.

EDIT: Ich denke auch, " constwann immer möglich verwenden" ist ein schlechter Rat. Sie sollten es dort verwenden, wo es Sinn macht.

Luchian Grigore
quelle