16 Stimmen

Was ist der bevorzugte Weg der Übergabe Zeiger / Verweis auf bestehende Objekt in einem Konstruktor?

Ich werde mit einem Beispiel beginnen. Es gibt eine schöne "Tokenizer"-Klasse in boost. Sie nimmt eine Zeichenkette, die tokenisiert werden soll, als Parameter in einem Konstruktor an:

std::string string_to_tokenize("a bb ccc ddd 0");
boost::tokenizer<boost::char_separator<char> > my_tok(string_to_tokenize);
/* do something with my_tok */

Die Zeichenkette wird im Tokenizer nicht verändert, also wird sie per const-Objektreferenz übergeben. Daher kann ich dort ein temporäres Objekt übergeben:

boost::tokenizer<boost::char_separator<char> > my_tok(std::string("a bb ccc ddd 0"));
/* do something with my_tok */

Alles sieht gut aus, aber wenn ich versuche, den Tokenizer zu verwenden, kommt es zu einer Katastrophe. Nach kurzer Untersuchung wurde mir klar, dass die Tokenizer-Klasse den Verweis, den ich ihr gegeben habe, speichert und bei weiterer Verwendung verwendet. Natürlich kann es nicht gut funktionieren für den Verweis auf temporäre Objekt.

In der Dokumentation steht nicht explizit, dass das im Konstruktor übergebene Objekt später verwendet wird, aber ok, es steht auch nicht drin, dass es nicht verwendet wird :) Ich kann das also nicht annehmen, mein Fehler.

Es ist jedoch ein wenig verwirrend. Im allgemeinen Fall, wenn ein Objekt ein anderes durch eine Konstantenreferenz aufnimmt, suggeriert es, dass ein temporäres Objekt dort gegeben werden kann. Was meinen Sie dazu? Ist dies eine schlechte Konvention? Vielleicht sollte in solchen Fällen ein Zeiger auf ein Objekt (statt einer Referenz) verwendet werden? Oder noch weitergehend - wäre es nicht nützlich, ein spezielles Schlüsselwort für ein Argument zu haben, das die Übergabe eines temporären Objekts als Parameter erlaubt/verbietet?

EDIT: Die Dokumentation (Version 1.49) ist eher minimalistisch und der einzige Teil, der auf ein solches Problem hinweisen könnte, ist:

Hinweis: Bei der Erstellung wird kein Parsing durchgeführt. Das Parsing wird bei Bedarf durchgeführt, wenn auf die Token über den von begin bereitgestellten Iterator zugegriffen wird.

Es wird jedoch nicht ausdrücklich gesagt, dass dasselbe Objekt verwendet wird, das angegeben wurde.

Der Sinn dieser Frage ist jedoch eher eine Diskussion über den Codierungsstil in einem solchen Fall, dies ist nur ein Beispiel, das mich inspiriert hat.

11voto

Frerich Raabe Punkte 85859

Wenn eine Funktion (z.B. ein Konstruktor) ein Argument als Referenz-zu-Konst annimmt, sollte sie soit

  • Dokumentieren Sie eindeutig, dass die Lebensdauer des referenzierten Objekts bestimmten Anforderungen genügen muss (z. B. "Wird nicht zerstört, bevor dies und das passiert").

ou

  • Erstellen Sie intern Kopien, wenn Sie das gegebene Objekt zu einem späteren Zeitpunkt verwenden müssen.

In diesem besonderen Fall (der boost::tokenizer Klasse) würde ich annehmen, dass letzteres aus Leistungsgründen und/oder um die Klasse mit Containertypen nutzbar zu machen, die gar nicht erst kopierbar sind, nicht gemacht wird. Aus diesem Grund würde ich dies als Dokumentationsfehler betrachten.

8voto

Steve Jessop Punkte 264569

Ich persönlich halte das für eine schlechte Idee, und es wäre besser, den Konstruktor entweder so zu schreiben, dass er die Zeichenkette kopiert, oder dass er eine const std::string* stattdessen. Es ist nur ein zusätzliches Zeichen, das der Anrufer eingeben muss, aber dieses Zeichen verhindert, dass er versehentlich ein temporäres Zeichen verwendet.

Generell gilt: Übertragen Sie Personen keine Verantwortung für die Pflege von Objekten, ohne deutlich zu machen, dass sie diese Verantwortung haben.

Ich denke, ein spezielles Schlüsselwort wäre nicht vollständig genug, um eine Änderung der Sprache zu rechtfertigen. Das Problem sind nicht die Provisorien, sondern jedes Objekt, das kürzer lebt als das zu konstruierende Objekt. Unter bestimmten Umständen wäre ein Temporäres in Ordnung (zum Beispiel wenn das tokenizer Objekt selbst auch ein temporäres in demselben Vollausdruck wäre). Ich möchte nicht wirklich mit der Sprache herumspielen, nur um eine halbe Lösung zu finden, und es gibt umfassendere Lösungen (z.B. eine shared_ptr (obwohl das seine eigenen Probleme hat).

"Also kann ich das nicht annehmen, mein Fehler"

Ich glaube nicht, dass es wirklich Ihr Fehler ist. Ich stimme mit Frerich überein, dass es nicht nur gegen meinen persönlichen Styleguide verstößt, dies überhaupt zu tun, sondern dass es in jedem vernünftigen Styleguide ein Dokumentationsfehler ist, wenn Sie es tun und nicht dokumentieren.

Es ist absolut notwendig, dass die erforderliche Lebensdauer von Parametern für By-Referenz-Funktionen dokumentiert wird, wenn sie nicht "mindestens so lange wie der Funktionsaufruf" beträgt. Dies ist etwas, das in den Dokumenten oft lax gehandhabt wird, und es muss richtig gemacht werden, um Fehler zu vermeiden.

Selbst in Sprachen mit Müllsammlung, in denen die Lebensdauer selbst automatisch gehandhabt wird und daher eher vernachlässigt wird, ist es wichtig, ob Sie Ihr Objekt ändern oder wiederverwenden können, ohne das Verhalten eines anderen Objekts zu ändern, dem Sie es irgendwann in der Vergangenheit als Methode übergeben haben. Daher sollten Funktionen dokumentieren, ob sie einen Alias für ihre Argumente in tout Sprache, der es an referenzieller Transparenz mangelt. Dies gilt insbesondere für C++, wo die Lebensdauer von Objekten das Problem des Aufrufers ist.

Leider ist der einzige Mechanismus, mit dem man tatsächlich sicherstellen dass Ihre Funktion keinen Verweis beibehalten kann, ist die Übergabe nach Wert, die mit Leistungseinbußen verbunden ist. Wenn Sie eine Sprache erfinden können, die Aliasing normalerweise erlaubt, aber auch eine C-ähnliche restrict Eigenschaft, die zur Kompilierzeit im const-Stil erzwungen wird, um zu verhindern, dass Funktionen Verweise auf ihre Argumente verschwinden lassen, dann viel Glück und melden Sie mich an.

3voto

Tamás Szelei Punkte 22099

Wie schon gesagt, die boost::tokenizer Beispiel ist entweder das Ergebnis eines Fehlers in der tokenizer oder eine Warnung, die in der Dokumentation fehlt.

Um die Frage allgemein zu beantworten, fand ich die folgende Prioritätenliste nützlich. Wenn man eine Option aus irgendeinem Grund nicht wählen kann, geht man zum nächsten Punkt über.

  1. Wertübergabe (mit vertretbarem Aufwand kopierbar, ohne dass das Originalobjekt geändert werden muss)
  2. Übergabe durch Konst-Referenz (das ursprüngliche Objekt muss nicht geändert werden)
  3. Übergabe durch Referenz (Originalobjekt muss geändert werden)
  4. Pass by shared_ptr (die Lebensdauer des Objekts wird von etwas anderem verwaltet, dies zeigt auch deutlich die Absicht, die Referenz zu behalten)
  5. Pass by raw pointer (Sie haben eine Adresse, auf die Sie casten müssen, oder Sie können aus irgendeinem Grund keinen Smart Pointer verwenden)

Wenn Ihre Begründung für die Wahl des nächsten Punktes auf der Liste "Leistung" lautet, sollten Sie sich hinsetzen und den Unterschied messen. Meiner Erfahrung nach neigen die meisten Leute (insbesondere mit Java- oder C#-Hintergrund) dazu, die Kosten der Übergabe eines Objekts als Wert zu überschätzen (und die Kosten der Dereferenzierung zu unterschätzen). Die Übergabe per Wert ist die sicherste Option (sie wird keine Überraschungen außerhalb des Objekts oder der Funktion verursachen, auch nicht in einem anderen Thread), geben Sie diesen großen Vorteil nicht so leicht auf.

1voto

CashCow Punkte 29849

Oft hängt es vom Kontext ab, z.B. wenn es sich um einen Funktor handelt, der in einem for_each oder ähnlichem aufgerufen wird, dann wird man oft eine Referenz oder einen Zeiger innerhalb des Funktors auf ein Objekt speichern, von dem man annimmt, dass es eine Lebensdauer über den Funktor hinaus hat.

Wenn es sich um eine allgemeine Nutzungsklasse handelt, muss man sich überlegen, wie die Menschen sie nutzen werden.

Wenn Sie einen Tokenizer schreiben, müssen Sie bedenken, dass das Kopieren dessen, was Sie tokenisieren, teuer sein könnte, aber Sie müssen auch bedenken, dass Sie, wenn Sie eine Boost-Bibliothek schreiben, diese für die Allgemeinheit schreiben, die sie für verschiedene Zwecke verwenden wird.

Speichern einer const char * wäre besser als eine std::string const& hier. Wenn der Benutzer eine std::string dann die const char * bleiben gültig, solange sie ihre Zeichenfolge nicht ändern, was sie wahrscheinlich nicht tun werden. Wenn sie ein const char * oder etwas anderes haben, das ein Array von Zeichen enthält und es übergeben, wird es trotzdem kopiert, um die std::string const & und es besteht die große Gefahr, dass es Ihren Konstrukteur nicht überlebt.

Natürlich kann man mit einem const char * nicht alle schönen std::basic_string Funktionen in Ihrer Implementierung.

Es besteht die Möglichkeit, als Parameter eine std::string& (nicht const reference), was (mit einem konformen Compiler) garantieren sollte, dass niemand ein temporäres übergeben wird, aber Sie werden in der Lage sein zu dokumentieren, dass Sie es nicht wirklich ändern, und die Gründe für Ihren scheinbar nicht const-korrekten Code. Übrigens habe ich diesen Trick auch einmal in meinem Code verwendet. Und Sie können gerne die Suchfunktionen von string verwenden. (Wenn Sie möchten, können Sie auch basic_string anstelle von string verwenden, um auch breite Zeichenketten zu tokenisieren).

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