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?
std::string
ist eine Klasse, genau wieCreditCard
kein primitiver Typ."abc"
nicht vom Typstd::string
, nicht vom Typchar */const char *
, sondern vom Typ istconst 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.Antworten:
DIE WICHTIGSTE FRAGE ZUERST:
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 :
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 Wertverweis
const
an Folgendes übergeben :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
,bool
oderchar
gibt 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 :
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 sind
my_class
, können Sie eine Überladung in Betracht ziehenfoo()
und eine Version für l-Werte (Akzeptieren eines l-Wert-Verweises aufconst
) und eine Version für r-Werte (Akzeptieren eines r-Wert-Verweises) bereitstellen: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: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::forward
normalerweise 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: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:
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
CreditCard
an Ihren Konstruktor übergeben. Zum Beispiel:Aber es wird nicht funktionieren, wenn Sie versuchen, dies zu tun:
Weil
cc
es 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
CreditCard
einen By-Wert nehmen, zwei Konstruktorüberladungen definieren, von denen eine einen Wertverweis aufconst
(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).
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 aussehenIn gewisser Weise kombiniert dies beide Überladungen, die ich zuvor gezeigt habe, zu einer einzigen Funktion:
C
Wird abgeleitet,CreditCard&
falls Sie einen l-Wert übergeben, und aufgrund der Regeln zum Reduzieren der Referenz wird diese Funktion instanziiert:Dies führt zu einer Kopierkonstruktion von
creditCard
, wie Sie es wünschen. Wenn andererseits ein r-Wert übergeben wird,C
wird daraus abgeleitetCreditCard
, und diese Funktion wird stattdessen instanziiert: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).quelle
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.obj
anstatt eine lokale Kopie zum Erstellen zu erstellen, nein?std::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.std::is_constructible<>
Typmerkmal möglicherweise besiegen, es sei denn, sie sind ordnungsgemäß SFINAE- eingeschränkt - was für manche vielleicht nicht trivial ist.Lassen Sie mich zunächst einige Details korrigieren. Wenn Sie Folgendes sagen:
Das ist falsch. Das Binden an eine rWertreferenz ist keine Bewegung. Es gibt nur einen Zug.
Da
CreditCard
es sich nicht um einen Vorlagenparameter handelt,std::forward<CreditCard>(creditCard)
handelt es sich nur um eine ausführliche Art zu sagenstd::move(creditCard)
.Jetzt...
Wenn Ihre Typen "billige" Bewegungen haben, möchten Sie vielleicht einfach Ihr Leben leichter machen und alles nach Wert und "
std::move
mit" nehmen.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::string
hä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 bereitstellenT&&
. 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.quelle
Account("",0,{brace, initialisation})
mehr schreiben .Zuallererst
std::string
ist es ein ziemlich kräftiger Klassentyp wiestd::vector
. Es ist sicherlich nicht primitiv.Wenn Sie große bewegliche Typen nach Wert in einen Konstruktor aufnehmen, würde ich
std::move
sie in das Mitglied aufnehmen:Genau so würde ich empfehlen, den Konstruktor zu implementieren. Es bewirkt , dass die Mitglieder
number
undcreditCard
seinen 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:
Sie haben Recht, dies beinhaltet eine Kopie von
creditCard
, da diese zuerst als Referenz an den Konstruktor übergeben wird. Aber jetzt können Sie keineconst
Objekte an den Konstruktor übergeben (da die Referenz nicht istconst
) und Sie können keine temporären Objekte übergeben. Zum Beispiel konnten Sie dies nicht tun:Betrachten wir nun:
Hier haben Sie ein Missverständnis von rWertreferenzen und gezeigt
std::forward
. Sie sollten wirklich nur verwenden,std::forward
wenn das Objekt, das Sie weiterleiten,T&&
für einen abgeleiteten Typ deklariert istT
. HierCreditCard
wird nicht abgeleitet (ich nehme an), und sostd::forward
wird das irrtümlich verwendet. Suchen Sie nach universellen Referenzen .quelle
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.
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.
quelle
(*) 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 Objektstd::string
an dieses übergeben wird. Daher müssen Sie sicherstellen, dass eines vorhanden ist.quelle