Wie übergebe ich Parameter richtig?

108

Ich bin ein C ++ Anfänger, aber kein Programmieranfänger. Ich versuche C ++ (c ++ 11) zu lernen und es ist mir irgendwie das Wichtigste unklar: Parameter übergeben.

Ich habe diese einfachen Beispiele betrachtet:

  • Eine Klasse, deren Mitglieder alle primitiven Typen haben:
    CreditCard(std::string number, int expMonth, int expYear,int pin):number(number), expMonth(expMonth), expYear(expYear), pin(pin)

  • Eine Klasse mit primitiven Typen als Mitgliedern + 1 komplexem Typ:
    Account(std::string number, float amount, CreditCard creditCard) : number(number), amount(amount), creditCard(creditCard)

  • Eine Klasse mit primitiven Typen + 1 Sammlung eines komplexen Typs als Mitglieder: Client(std::string firstName, std::string lastName, std::vector<Account> accounts):firstName(firstName), lastName(lastName), accounts(accounts)

Wenn ich ein Konto erstelle, mache ich Folgendes:

    CreditCard cc("12345",2,2015,1001);
    Account acc("asdasd",345, cc);

Offensichtlich wird die Kreditkarte in diesem Szenario zweimal kopiert. Wenn ich diesen Konstruktor umschreibe als

Account(std::string number, float amount, CreditCard& creditCard) 
    : number(number)
    , amount(amount)
    , creditCard(creditCard)

Es wird eine Kopie geben. Wenn ich es umschreibe als

Account(std::string number, float amount, CreditCard&& creditCard) 
    : number(number)
    , amount(amount)
    , creditCard(std::forward<CreditCard>(creditCard))

Es gibt 2 Züge und keine Kopie.

Ich denke, manchmal möchten Sie vielleicht einen Parameter kopieren, manchmal möchten Sie nicht kopieren, wenn Sie dieses Objekt erstellen.
Ich komme aus C # und da ich an Referenzen gewöhnt bin, ist es ein bisschen seltsam für mich und ich denke, es sollte 2 Überladungen für jeden Parameter geben, aber ich weiß, dass ich falsch liege.
Gibt es Best Practices für das Senden von Parametern in C ++, weil ich es wirklich nicht trivial finde? Wie würden Sie mit meinen oben dargestellten Beispielen umgehen?

Jack Willson
quelle
9
Meta: Ich kann nicht glauben, dass jemand gerade eine gute C ++ - Frage gestellt hat. +1.
23
Zu Ihrer Information, std::stringist eine Klasse, genau wie CreditCardkein primitiver Typ.
Chris
7
Aufgrund der Zeichenfolgenverwirrung sollten Sie wissen, dass ein Zeichenfolgenliteral "abc"nicht vom Typ std::string, nicht vom Typ char */const char *, sondern vom Typ ist const char[N](in diesem Fall mit N = 4 aufgrund von drei Zeichen und einer Null). Das ist ein gutes, weit verbreitetes Missverständnis, um aus dem Weg zu gehen.
Chris
10
@chuex: Jon Skeet hat alle C # -Fragen, viel seltener C ++.
Steve Jessop
In hohem Maße verwandt: Wie übergebe ich ein Objekt an Funktionen in C ++?
Legends2k

Antworten:

158

DIE WICHTIGSTE FRAGE ZUERST:

Gibt es Best Practices für das Senden von Parametern in C ++, weil ich es wirklich nicht trivial finde?

Wenn Ihre Funktion das übergebene Originalobjekt ändern muss , damit nach der Rückkehr des Aufrufs Änderungen an diesem Objekt für den Aufrufer sichtbar sind, sollten Sie die lvalue-Referenz übergeben :

void foo(my_class& obj)
{
    // Modify obj here...
}

Wenn Ihre Funktion das ursprüngliche Objekt nicht ändern und keine Kopie davon erstellen muss (mit anderen Worten, sie muss nur den Status beobachten), sollten Sie den Wertverweisconst an Folgendes übergeben :

void foo(my_class const& obj)
{
    // Observe obj here
}

Auf diese Weise können Sie die Funktion sowohl mit lWerten (lWerte sind Objekte mit einer stabilen Identität) als auch mit rWerten (rWerte sind beispielsweise temporäre Werte oder Objekte, von denen Sie sich als Ergebnis eines Aufrufs bewegen möchten std::move()) aufrufen .

Man könnte auch argumentieren , dass für grundlegende Typen oder Arten , für das das Kopieren schnell ist , wie zum Beispiel int, booloder chargibt es keine Notwendigkeit , durch Verweis übergeben wird , wenn die Funktion einfach braucht um den Wert zu beobachten, und von Wert vorbei begünstigt werden soll . Das ist richtig, wenn keine Referenzsemantik benötigt wird, aber was ist, wenn die Funktion irgendwo einen Zeiger auf dasselbe Eingabeobjekt speichern wollte, damit beim zukünftigen Lesen dieses Zeigers die Wertänderungen angezeigt werden, die in einem anderen Teil von ausgeführt wurden Code? In diesem Fall ist die Referenzübergabe die richtige Lösung.

Wenn Ihre Funktion muss nicht das ursprüngliche Objekt ändern, sondern muss eine Kopie dieses Objekts speichern ( möglicherweise das Ergebnis einer Transformation des Eingangs zurückzukehren , ohne die Eingabe zu ändern ), dann könnte man erwägen nach Wert unter :

void foo(my_class obj) // One copy or one move here, but not working on
                       // the original object...
{
    // Working on obj...

    // Possibly move from obj if the result has to be stored somewhere...
}

Das Aufrufen der obigen Funktion führt immer zu einer Kopie, wenn l-Werte übergeben werden, und zu einer Bewegung, wenn r-Werte übergeben werden. Wenn Ihre Funktion dieses Objekt irgendwo speichern muss, könnten Sie einen zusätzlichen führen Umzug von ihm (zum Beispiel in dem Fall foo()ist eine Elementfunktion , dass der Bedarf den Wert in einem Datenelement zu speichern ).

Wenn Verschiebungen für Objekte vom Typ teuer sindmy_class , können Sie eine Überladung in Betracht ziehen foo()und eine Version für l-Werte (Akzeptieren eines l-Wert-Verweises auf const) und eine Version für r-Werte (Akzeptieren eines r-Wert-Verweises) bereitstellen:

// Overload for lvalues
void foo(my_class const& obj) // No copy, no move (just reference binding)
{
    my_class copyOfObj = obj; // Copy!
    // Working on copyOfObj...
}

// Overload for rvalues
void foo(my_class&& obj) // No copy, no move (just reference binding)
{
    my_class copyOfObj = std::move(obj); // Move! 
                                         // Notice, that invoking std::move() is 
                                         // necessary here, because obj is an
                                         // *lvalue*, even though its type is 
                                         // "rvalue reference to my_class".
    // Working on copyOfObj...
}

Die oben genannten Funktionen sind so ähnlich, in der Tat, dass Sie eine einzelne Funktion draus machen könnten: foo()könnten eine Funktion werden Vorlage und Sie könnten verwenden perfekte Weiterleitung zu bestimmen , ob eine Bewegung oder eine Kopie des Objekts wird intern weitergeleitet werden generiert:

template<typename C>
void foo(C&& obj) // No copy, no move (just reference binding)
//       ^^^
//       Beware, this is not always an rvalue reference! This will "magically"
//       resolve into my_class& if an lvalue is passed, and my_class&& if an
//       rvalue is passed
{
    my_class copyOfObj = std::forward<C>(obj); // Copy if lvalue, move if rvalue
    // Working on copyOfObj...
}

Vielleicht möchten Sie mehr über dieses Design erfahren, indem Sie sich diesen Vortrag von Scott Meyers ansehen (beachten Sie jedoch, dass der von ihm verwendete Begriff " Universal References " nicht dem Standard entspricht).

Eine Sache, die Sie beachten sollten, ist, dass dies std::forwardnormalerweise zu einer Bewegung für r-Werte führt. Auch wenn es relativ unschuldig aussieht, kann das mehrfache Weiterleiten desselben Objekts zu Problemen führen - zum Beispiel das zweimalige Bewegen von demselben Objekt! Achten Sie also darauf, dies nicht in eine Schleife zu setzen und dasselbe Argument in einem Funktionsaufruf nicht mehrmals weiterzuleiten:

template<typename C>
void foo(C&& obj)
{
    bar(std::forward<C>(obj), std::forward<C>(obj)); // Dangerous!
}

Beachten Sie auch, dass Sie normalerweise nicht auf die vorlagenbasierte Lösung zurückgreifen, es sei denn, Sie haben einen guten Grund dafür, da dies das Lesen Ihres Codes erschwert. Normalerweise sollten Sie sich auf Klarheit und Einfachheit konzentrieren .

Die oben genannten sind nur einfache Richtlinien, aber meistens weisen sie Sie auf gute Entwurfsentscheidungen hin.


BETREFFEND DEN REST IHRES POSTES:

Wenn ich es als [...] umschreibe, gibt es 2 Züge und keine Kopie.

Das ist nicht richtig. Zunächst kann eine r-Wert-Referenz nicht an einen l-Wert gebunden werden. Dies wird daher nur kompiliert, wenn Sie einen r-Wert vom Typ CreditCardan Ihren Konstruktor übergeben. Zum Beispiel:

// Here you are passing a temporary (OK! temporaries are rvalues)
Account acc("asdasd",345, CreditCard("12345",2,2015,1001));

CreditCard cc("12345",2,2015,1001);
// Here you are passing the result of std::move (OK! that's also an rvalue)
Account acc("asdasd",345, std::move(cc));

Aber es wird nicht funktionieren, wenn Sie versuchen, dies zu tun:

CreditCard cc("12345",2,2015,1001);
Account acc("asdasd",345, cc); // ERROR! cc is an lvalue

Weil cces sich um einen l-Wert handelt und r-Wert-Referenzen nicht an l-Werte gebunden werden können. Darüber hinaus wird beim Binden einer Referenz an ein Objekt keine Verschiebung ausgeführt : Es handelt sich lediglich um eine Referenzbindung. Somit wird es nur einen Zug geben.


Basierend auf den Richtlinien im ersten Teil dieser Antwort können Sie, wenn Sie sich mit der Anzahl der Züge befassen, die generiert werden, wenn Sie CreditCardeinen By-Wert nehmen, zwei Konstruktorüberladungen definieren, von denen eine einen Wertverweis auf const( CreditCard const&) und eine nimmt eine rWertreferenz ( CreditCard&&).

Die Überlastungsauflösung wählt die erstere aus, wenn ein l-Wert übergeben wird (in diesem Fall wird eine Kopie ausgeführt), und die letztere, wenn ein r-Wert übergeben wird (in diesem Fall wird eine Bewegung ausgeführt).

Account(std::string number, float amount, CreditCard const& creditCard) 
: number(number), amount(amount), creditCard(creditCard) // copy here
{ }

Account(std::string number, float amount, CreditCard&& creditCard) 
: number(number), amount(amount), creditCard(std::move(creditCard)) // move here
{ }

Ihre Verwendung von std::forward<>wird normalerweise angezeigt, wenn Sie eine perfekte Weiterleitung erzielen möchten . In diesem Fall würde Ihr Konstruktor tatsächlich ein Konstruktor seine Vorlage , und mehr oder weniger würde wie folgt aussehen

template<typename C>
Account(std::string number, float amount, C&& creditCard) 
: number(number), amount(amount), creditCard(std::forward<C>(creditCard)) { }

In gewisser Weise kombiniert dies beide Überladungen, die ich zuvor gezeigt habe, zu einer einzigen Funktion: CWird abgeleitet, CreditCard&falls Sie einen l-Wert übergeben, und aufgrund der Regeln zum Reduzieren der Referenz wird diese Funktion instanziiert:

Account(std::string number, float amount, CreditCard& creditCard) : 
number(num), amount(amount), creditCard(std::forward<CreditCard&>(creditCard)) 
{ }

Dies führt zu einer Kopierkonstruktion von creditCard, wie Sie es wünschen. Wenn andererseits ein r-Wert übergeben wird, Cwird daraus abgeleitet CreditCard, und diese Funktion wird stattdessen instanziiert:

Account(std::string number, float amount, CreditCard&& creditCard) : 
number(num), amount(amount), creditCard(std::forward<CreditCard>(creditCard)) 
{ }

Dies führt zu einer Verschiebungskonstruktion von creditCard, was Sie möchten (da der übergebene Wert ein r-Wert ist und wir also berechtigt sind, ihn zu verschieben).

Andy Prowl
quelle
Ist es falsch zu sagen, dass es für nicht primitive Parametertypen in Ordnung ist, die Vorlagenversion immer mit forward zu verwenden?
Jack Willson
1
@JackWillson: Ich würde sagen, Sie sollten nur dann auf die Vorlagenversion zurückgreifen, wenn Sie wirklich Bedenken hinsichtlich der Leistung von Zügen haben. Sehen Sie diese Frage von mir , die eine gute Antwort hat, für weitere Informationen. Wenn Sie keine Kopie erstellen müssen und nur beobachten müssen, nehmen Sie im Allgemeinen unter ref to const. Wenn Sie das ursprüngliche Objekt ändern müssen, gehen Sie von ref zu non-`const. Wenn Sie eine Kopie erstellen müssen und Umzüge billig sind, nehmen Sie nach Wert und bewegen Sie sich dann.
Andy Prowl
2
Ich denke, im dritten Codebeispiel muss nicht umgezogen werden. Sie könnten einfach verwenden, objanstatt eine lokale Kopie zum Erstellen zu erstellen, nein?
Juanchopanza
3
@AndyProwl: Sie haben meine +1 Stunden erhalten, aber wenn Sie Ihre (ausgezeichnete) Antwort noch einmal lesen, möchte ich auf zwei Dinge hinweisen: a) Nach Wert wird nicht immer eine Kopie erstellt (wenn sie nicht verschoben wird). Das Objekt könnte direkt auf der Anruferseite erstellt werden, insbesondere bei RVO / NRVO funktioniert dies sogar häufiger als man zunächst denkt. b) Bitte beachten Sie, dass im letzten Beispiel nur einmalstd::forward aufgerufen werden darf . Ich habe Leute gesehen, die es in Schleifen usw. gesteckt haben, und da diese Antwort von vielen Anfängern gesehen wird, sollte es meiner Meinung nach ein dickes "Warning!" - Etikett geben, das ihnen hilft, diese Falle zu umgehen.
Daniel Frey
1
@SteveJessop: Abgesehen davon gibt es technische Probleme (nicht unbedingt Probleme) mit Weiterleitungsfunktionen ( insbesondere Konstruktoren, die ein Argument verwenden ), hauptsächlich die Tatsache, dass sie Argumente jeglichen Typs akzeptieren und das std::is_constructible<>Typmerkmal möglicherweise besiegen, es sei denn, sie sind ordnungsgemäß SFINAE- eingeschränkt - was für manche vielleicht nicht trivial ist.
Andy Prowl
11

Lassen Sie mich zunächst einige Details korrigieren. Wenn Sie Folgendes sagen:

Es gibt 2 Züge und keine Kopie.

Das ist falsch. Das Binden an eine rWertreferenz ist keine Bewegung. Es gibt nur einen Zug.

Da CreditCardes sich nicht um einen Vorlagenparameter handelt, std::forward<CreditCard>(creditCard)handelt es sich nur um eine ausführliche Art zu sagen std::move(creditCard).

Jetzt...

Wenn Ihre Typen "billige" Bewegungen haben, möchten Sie vielleicht einfach Ihr Leben leichter machen und alles nach Wert und " std::movemit" nehmen.

Account(std::string number, float amount, CreditCard creditCard)
: number(std::move(number),
  amount(amount),
  creditCard(std::move(creditCard)) {}

Dieser Ansatz bringt Ihnen zwei Züge, wenn er nur einen ergeben könnte, aber wenn die Züge billig sind, können sie akzeptabel sein.

Während wir uns mit dieser Frage der "billigen Bewegungen" befassen, möchte ich Sie daran erinnern, dass diese std::stringhäufig mit der sogenannten Optimierung kleiner Zeichenfolgen implementiert wird, sodass ihre Bewegungen möglicherweise nicht so billig sind wie das Kopieren einiger Zeiger. Wie bei Optimierungsproblemen üblich, sollten Sie Ihren Profiler und nicht mich fragen, ob es darauf ankommt oder nicht.

Was tun, wenn Sie diese zusätzlichen Bewegungen nicht ausführen möchten? Vielleicht erweisen sie sich als zu teuer, oder schlimmer noch, vielleicht können die Typen nicht wirklich verschoben werden, und es können zusätzliche Kopien entstehen.

Wenn es nur einen problematischen Parameter gibt, können Sie zwei Überladungen mit T const&und bereitstellen T&&. Dadurch werden Referenzen bis zur eigentlichen Elementinitialisierung, bei der eine Kopie oder ein Verschieben erfolgt, ständig gebunden.

Wenn Sie jedoch mehr als einen Parameter haben, führt dies zu einer exponentiellen Explosion der Anzahl der Überlastungen.

Dies ist ein Problem, das mit perfekter Weiterleitung gelöst werden kann. Das heißt, Sie schreiben stattdessen eine Vorlage und verwenden diese std::forward, um die Wertekategorie der Argumente als Mitglieder an ihr endgültiges Ziel zu übertragen.

template <typename TString, typename TCreditCard>
Account(TString&& number, float amount, TCreditCard&& creditCard)
: number(std::forward<TString>(number),
  amount(amount),
  creditCard(std::forward<TCreditCard>(creditCard)) {}
R. Martinho Fernandes
quelle
Es gibt ein Problem mit der Vorlagenversion: Der Benutzer kann nicht Account("",0,{brace, initialisation})mehr schreiben .
IPC
@ipc ah, stimmt. Das ist wirklich ärgerlich und ich glaube nicht, dass es eine einfache skalierbare Problemumgehung gibt.
R. Martinho Fernandes
6

Zuallererst std::stringist es ein ziemlich kräftiger Klassentyp wie std::vector. Es ist sicherlich nicht primitiv.

Wenn Sie große bewegliche Typen nach Wert in einen Konstruktor aufnehmen, würde ich std::movesie in das Mitglied aufnehmen:

CreditCard(std::string number, float amount, CreditCard creditCard)
  : number(std::move(number)), amount(amount), creditCard(std::move(creditCard))
{ }

Genau so würde ich empfehlen, den Konstruktor zu implementieren. Es bewirkt , dass die Mitglieder numberund creditCardseinen Umzug konstruiert, anstatt Kopie aufgebaut. Wenn Sie diesen Konstruktor verwenden, gibt es eine Kopie (oder eine Verschiebung, falls vorübergehend), wenn das Objekt an den Konstruktor übergeben wird, und dann eine Verschiebung, wenn das Element initialisiert wird.

Betrachten wir nun diesen Konstruktor:

Account(std::string number, float amount, CreditCard& creditCard)
  : number(number), amount(amount), creditCard(creditCard)

Sie haben Recht, dies beinhaltet eine Kopie von creditCard, da diese zuerst als Referenz an den Konstruktor übergeben wird. Aber jetzt können Sie keine constObjekte an den Konstruktor übergeben (da die Referenz nicht ist const) und Sie können keine temporären Objekte übergeben. Zum Beispiel konnten Sie dies nicht tun:

Account account("something", 10.0f, CreditCard("12345",2,2015,1001));

Betrachten wir nun:

Account(std::string number, float amount, CreditCard&& creditCard)
  : number(number), amount(amount), creditCard(std::forward<CreditCard>(creditCard))

Hier haben Sie ein Missverständnis von rWertreferenzen und gezeigt std::forward. Sie sollten wirklich nur verwenden, std::forwardwenn das Objekt, das Sie weiterleiten, T&&für einen abgeleiteten Typ deklariert ist T. Hier CreditCardwird nicht abgeleitet (ich nehme an), und so std::forwardwird das irrtümlich verwendet. Suchen Sie nach universellen Referenzen .

Joseph Mansfield
quelle
1

Ich verwende eine recht einfache Regel für den allgemeinen Fall: Verwenden Sie copy für POD (int, bool, double, ...) und const & für alles andere ...

Und ob Sie kopieren möchten oder nicht, wird nicht durch die Methodensignatur beantwortet, sondern vielmehr durch das, was Sie mit den Parametern tun.

struct A {
  A(const std::string& aValue, const std::string& another) 
    : copiedValue(aValue), justARef(another) {}
  std::string copiedValue;
  const std::string& justARef; 
};

Präzision für Zeiger: Ich habe sie fast nie benutzt. Der einzige Vorteil gegenüber & besteht darin, dass sie null sein oder neu zugewiesen werden können.

David Fleury
quelle
2
"Ich verwende eine recht einfache Regel für den allgemeinen Fall: Verwenden Sie copy für POD (int, bool, double, ...) und const & für alles andere." Nein. Nur Nein.
Schuh
Kann hinzugefügt werden, wenn Sie einen Wert mit einem & (keine Konstante) ändern möchten. Sonst sehe ich nicht ... Einfachheit ist gut genug. Aber wenn Sie so sagten ...
David Fleury
Manchmal möchten Sie nach primitiven Referenztypen ändern, und manchmal müssen Sie eine Kopie von Objekten erstellen und manchmal müssen Sie Objekte verschieben. Sie können nicht einfach alles auf POD> nach Wert und UDT> nach Referenz reduzieren.
Schuh
Ok, das ist genau das, was ich danach hinzugefügt habe. Kann eine zu schnelle Antwort sein.
David Fleury
1

Für mich ist das Wichtigste irgendwie unklar: Parameter übergeben.

  • Wenn Sie die in der Funktion / Methode übergebene Variable ändern möchten
    • Sie übergeben es als Referenz
    • Sie übergeben es als Zeiger (*)
  • Wenn Sie den in der Funktion / Methode übergebenen Wert / die Variable lesen möchten
    • Sie übergeben es als const Referenz
  • Wenn Sie den innerhalb der Funktion / Methode übergebenen Wert ändern möchten
    • Sie übergeben es normal, indem Sie das Objekt kopieren (**)

(*) Zeiger können sich auf dynamisch zugewiesenen Speicher beziehen. Wenn möglich, sollten Sie Referenzen Zeigern vorziehen, auch wenn Referenzen letztendlich normalerweise als Zeiger implementiert werden.

(**) "normal" bedeutet durch Kopierkonstruktor (wenn Sie ein Objekt des gleichen Parametertyps übergeben) oder durch normalen Konstruktor (wenn Sie einen kompatiblen Typ für die Klasse übergeben). Wenn Sie beispielsweise ein Objekt übergeben myMethod(std::string), wird der Kopierkonstruktor verwendet, wenn ein Objekt std::stringan dieses übergeben wird. Daher müssen Sie sicherstellen, dass eines vorhanden ist.

Schuh
quelle