Liebe Leserinnen, liebe Leser,

heute habe ich endlich eine definitve Richtlinie (und deswegen eine Lösung) für ein Problem gefunden, das mich seit meine erste Begegnung mit univelsalen Referenzen (Nomenklatur von Scott Meyers) oder “Forwaring”-Referenzen (Nomenklatur von vielen anderen Leute?) gestört hat.

Eine Randbemerkung:
Ich bin darüber nicht ganz sicher, welcher Name jetzt offiziell ist, aber es gab 2014 ein formales Dokument von Sutter, Stroustrup und Dos Reis, das der Name “Forwaring”-Referenz bevorzugt. Troztdessen ergibt jetzt (4 Jahre später) ein Google-Nachschlag 10x mehr Ergebnisse für univelsale Referenzen als für den anderen Begriff, also ich werde den zweiten Begriff verwenden, obwohl mir der Name “Forwaring”-Referenz besser gefällt (meiner Meinung nach ist das nicht so verwirrend).

Also, was für Probleme hatte ich bei universalen Referenzen? Die Antwort ist eine Frage: wo sollte man sie benutzen? Für mihc ist ein bisschen Optimierung des Codes sehr verlockend, besonders wenn das (aus erster Sicht) einfach erreichbar ist. Mit dieser Mentalität ist es dagegen möglich, etwas Gegensätzliches zu bekommen. Lasst uns zu der GumballMachine zurückkehren, die ich in meinem letzten Beitrag diskutiert habe:

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// in foo.h:

class GumballMachine {
public:
    // constructors...

    static auto create(std::string llocation, unsigned int numberGumballs) -> std::unique_ptr<GumballMachine>;
    // other machinery...

private:
    template <typename S>
    GumballMachine(S&& llocation, unsigned int numberGumballs)
        : location(std::forward<S>(llocation)), ballCount(numberGumballs) {}

    std::string location;
    unsigned int ballCount = 0;
    // other states...
};

// in foo.cpp::

auto GumballMachine::create(std::string llocation, unsigned int numberGumballs) -> std::unique_ptr<GumballMachine>
{
    std::unique_ptr<GumballMachine> instance(new GumballMachine(std::move(llocation), numberGumballs));

    // other initializations in instance...
}

Einfach gesagt gibt es ein Static-Factory mit pass-by-value Parameter, der den privaten Konstruktor mit einem universalen Referenz aufruft. Die Vorstellung hier ist, dass die am Anfang erstellte Kopie des Strings dem Klassenkonstruktor, der die Kopie dem String Move-Kunstruktor weitergibt, als ein rvalue ungefählrich weitergegeben werden kann. Diese Methode führt nur zu einer einzigen Speicherzuteilung (ich lasse STO jetzt aus) und einem Umzug: das schneint nicht so schlecht zu sein. Was ist das Problem dann? Bevor darüber diskutiert wird, stelle ich eine Sammlung der weiteren Möglichkeiten dar (diese Beispiele sind in den 2014 CppCon Folien von Herrn Sutter zu finden):

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
 * Beispiele von Herrn Sutter:
 */

class employee {
    std::string name_;
    
public:
    void set_name(/* ?? */);
};

// option #1:
void set_name(const std::string& name) {name_ = name;}

// option #2:
void set_name(const std::string& name) {name_ = name;}
void set_name(std::string&& name) noexcept {name_ = std::move(name);}

// option #3:
void set_name(std::string name) noexcept {name_ = std::move(name);}

// option #4:
template <class String, class = std::enable_if_t<!std::is_same<std::decay_t<String>,std::string>::value>>
void set_name(String&& name) noexcept(std::is_nothrow_assignable<std::string&, String>::value)
    {name_ = std::forward<String>(name);}

Option #4 ist kein Scherz, das ist ein Template völlig für String beschränkt. Das noexcept Stichwort in Option #3 ist gefährlich! Weitere Einzelheiten (und benchmarks!) über die obengenannte Optionen sind in den verlinkten Folien zu finden. Jetzt haben wir mehrere Möglichkeiten, also man könnte die folgende Frage stellen: Welche Option ist empfohlen? Herr Sutter gab eine gute Tabelle ab, um diese Frage zu beantworten. Er ermutigt diese Tabelle auf die Wand des Büros zu kleben (Das Bild ist aus den verlinkten Folien auch):

Function parameters guideSutters Anleitung für die Nutzung von Funktionparametern

Falls man den GumballMachine-Code erneut untersucht, wird man Probleme sehen. Einerseits soll der pass-by-value Ansatz des Strings in der statischen Funktion durch Option #1 ersetzt werden. Einerseits wird es die unvermeidliche Not für Speicherzuteilung für lange Strings entfernen. Andererseits soll der private Konstruktor (und das Template) den pass-by-reference Ansatz für den String verwenden, weil es hier keinen Perfect-Forwarding gibt. Aber braucht es wirklich ersetzt werden? Der Regel hat eine Erweiterung, die man nur in dem video völlig untersuchen kann (das ist sehr empfohlen). Hier gibt’s einen Bildschirmausdruck:

Function parameters guide extraSutters erwetierte Anleitung für die Nutzung von Funktionparametern

Es würde so erscheinen, dass in besonderen Fällen (z.B. Bibliothekklassen) Perfect-Forwarding auch benutzt werden kann. Ich denke, dass mein Fall mit dem privaten Konstruktor eine dieser Situationen ist. Aber was passiert mit dem zusätzlichen Code im Bezug auf Beschränkungen, wie es Option #4 zeigt? Im Falle der GumballMachine-Klasse ist es OK zu vermeiden, weil der Konstruktor ist privat, und nur der Static-Factory ruft das auf. Das ist ein Factory, der in allen Fällen einen String ergibt. Auf diese Weise verschafft der Factory alle nötige Beschränkungen, und deshalb ist alles OK.

War das Teil mit dem Template absichtlich? Ich möchte es sagen, dass es so ist, aber ehrlich gesagt war das zufällig. Ich habe das nur später herausgefunden, dass diesee Situation etwas Besonderes ist. Also, was habe ich gelernt?

Verwende die Standards, weil sie normalerweise fein funktionieren.

Vielen Dank fürs Lesen.