wake-up-neo.com

Wann sollte ich den automatischen Abzugstyp C++ 14 verwenden?

Mit der Veröffentlichung von GCC 4.8.0 haben wir einen Compiler, der die automatische Ableitung des Rückgabetyps unterstützt, Teil von C++ 14. Mit -std=c++1y kann ich folgendes machen: 

auto foo() { //deduced to be int
    return 5;
}

Meine Frage ist: Wann sollte ich diese Funktion verwenden? Wann ist es notwendig und wann macht es Code sauberer?

Szenario 1

Das erste Szenario, das mir einfällt, ist wann immer möglich. Jede Funktion, die so geschrieben werden kann, sollte sein. Das Problem dabei ist, dass der Code möglicherweise nicht immer lesbarer wird.

Szenario 2

Das nächste Szenario besteht darin, komplexere Rückgabetypen zu vermeiden. Als sehr leichtes Beispiel:

template<typename T, typename U>
auto add(T t, U u) { //almost deduced as decltype(t + u): decltype(auto) would
    return t + u;
}

Ich glaube nicht, dass dies jemals wirklich ein Problem sein würde, obwohl ich denke, dass der Rückgabetyp explizit von den Parametern abhängt, in manchen Fällen klarer sein könnte.

Szenario 3

Weiter, um Redundanz zu vermeiden:

auto foo() {
    std::vector<std::map<std::pair<int, double>, int>> ret;
    //fill ret in with stuff
    return ret;
}

In C++ 11 können wir manchmal einfach anstelle eines Vektors return {5, 6, 7};, aber das funktioniert nicht immer und wir müssen den Typ sowohl im Funktionsheader als auch im Funktionshauptteil angeben. Dies ist rein redundant, und die automatische Ableitung des Rückgabetyps erspart uns diese Redundanz.

Szenario 4

Schließlich kann es anstelle sehr einfacher Funktionen verwendet werden:

auto position() {
    return pos_;
}

auto area() {
    return length_ * width_;
}

Manchmal schauen wir uns jedoch die Funktion an und möchten den genauen Typ wissen. Wenn sie dort nicht angegeben ist, müssen wir zu einem anderen Punkt im Code gehen, beispielsweise wo pos_ deklariert wird.

Fazit

Welche Szenarien erweisen sich bei diesen Szenarien tatsächlich als eine Situation, in der diese Funktion hilfreich ist, um den Code sauberer zu machen? Was ist mit Szenarien, die ich hier nicht erwähnt habe? Welche Vorsichtsmaßnahmen sollte ich treffen, bevor Sie diese Funktion verwenden, damit sie mich später nicht beißt? Gibt es etwas Neues, das dieses Feature auf den Tisch bringt, das ohne es nicht möglich ist?

Beachten Sie, dass die mehrfachen Fragen als Hilfe bei der Suche nach Perspektiven dienen sollen, aus denen sich dies beantworten lässt.

122
chris

C++ 11 wirft ähnliche Fragen auf: Wann wird in Lambdas der Rückgabetyp verwendet, und wann auto-Variablen verwendet werden.

Die traditionelle Antwort auf die Frage in C und C++ 03 lautete: "Über Anweisungsgrenzen hinweg machen wir Typen explizit, innerhalb von Ausdrücken sind sie normalerweise implizit, aber wir können sie mit Casts explizit machen". C++ 11 und C++ 1y führen Typenabzugstools ein, mit denen Sie den Typ an neuen Stellen auslassen können.

Tut mir leid, aber Sie werden dieses Problem nicht lösen, indem Sie allgemeine Regeln festlegen. Sie müssen sich den jeweiligen Code ansehen und selbst entscheiden, ob es die Lesbarkeit von Typen überall zulässt. Ist es besser, wenn Ihr Code sagt, "der Typ dieses Dings ist X" oder ist es besser Ihr Code, um zu sagen, "der Typ dieses Dings ist für das Verstehen dieses Teils des Codes irrelevant: Der Compiler muss es wissen und wir könnten es wahrscheinlich herausfinden, aber wir müssen es hier nicht sagen"?

Da "Lesbarkeit" nicht objektiv definiert ist [*] und außerdem je nach Leser unterschiedlich ist, haben Sie als Autor/Herausgeber eines Codes die Verantwortung, die von einem Style Guide nicht vollständig erfüllt werden kann. Sogar in dem Umfang, in dem ein Styleguide Normen festlegt, bevorzugen verschiedene Personen andere Normen und neigen dazu, etwas Unbekanntes als "weniger lesbar" zu finden. Daher kann die Lesbarkeit einer bestimmten vorgeschlagenen Stilregel oft nur im Zusammenhang mit den anderen vorhandenen Stilregeln beurteilt werden.

Alle Ihre Szenarien (auch die ersten) werden für den Codierstil von jemandem verwendet. Persönlich finde ich, dass der zweite der überzeugendste Anwendungsfall ist, aber ich gehe davon aus, dass dies von Ihren Dokumentationswerkzeugen abhängen wird. Es ist nicht sehr hilfreich, dokumentiert zu sehen, dass der Rückgabetyp einer Funktionsvorlage auto ist, während die Dokumentation als decltype(t+u) eine veröffentlichte Schnittstelle erstellt, auf die Sie sich (hoffentlich) verlassen können.

[*] Manchmal versucht jemand, objektive Messungen vorzunehmen. In dem geringen Maße, dass jemand statistisch signifikante und allgemein anwendbare Ergebnisse erzielt, werden sie von den Programmierern völlig ignoriert, und zwar zu Gunsten des Instinkts des Autors, was "lesbar" ist.

54
Steve Jessop

Im Allgemeinen ist der Rückgabetyp der Funktion eine große Hilfe, um eine Funktion zu dokumentieren. Der Benutzer wird wissen, was erwartet wird. Es gibt jedoch einen Fall, in dem es meiner Meinung nach schön wäre, diesen Rückgabetyp zu löschen, um Redundanzen zu vermeiden. Hier ist ein Beispiel:

template<typename F, typename Tuple, int... I>
  auto
  apply_(F&& f, Tuple&& args, int_seq<I...>) ->
  decltype(std::forward<F>(f)(std::get<I>(std::forward<Tuple>(args))...))
  {
    return std::forward<F>(f)(std::get<I>(std::forward<Tuple>(args))...);
  }

template<typename F, typename Tuple,
         typename Indices = make_int_seq<std::Tuple_size<Tuple>::value>>
  auto
  apply(F&& f, Tuple&& args) ->
  decltype(apply_(std::forward<F>(f), std::forward<Tuple>(args), Indices()))
  {
    return apply_(std::forward<F>(f), std::forward<Tuple>(args), Indices());
  }

Dieses Beispiel stammt aus der offiziellen Ausschußarbeit N3493 . Die Funktion apply dient dazu, die Elemente eines std::Tuple an eine Funktion weiterzuleiten und das Ergebnis zurückzugeben. int_seq und make_int_seq sind nur ein Teil der Implementierung und werden wahrscheinlich nur jeden Benutzer verwirren, der versucht zu verstehen, was er tut.

Wie Sie sehen, ist der Rückgabetyp nichts anderes als ein decltype des zurückgegebenen Ausdrucks. Da apply_ nicht für die Benutzer gedacht ist, bin ich nicht sicher, ob es sinnvoll ist, den Rückgabetyp zu dokumentieren, wenn er mehr oder weniger der von apply entspricht. Ich denke, dass in diesem speziellen Fall das Zurücklassen des Rückgabetyps die Funktion lesbarer macht. Beachten Sie, dass dieser Rückgabetyp tatsächlich gelöscht und durch decltype(auto) ersetzt wurde, im Vorschlag, apply zum Standard N3915 hinzuzufügen.

template <typename F, typename Tuple, size_t... I>
decltype(auto) apply_impl(F&& f, Tuple&& t, index_sequence<I...>) {
    return forward<F>(f)(get<I>(forward<Tuple>(t))...);
}

template <typename F, typename Tuple>
decltype(auto) apply(F&& f, Tuple&& t) {
    using Indices = make_index_sequence<Tuple_size<decay_t<Tuple>>::value>;
    return apply_impl(forward<F>(f), forward<Tuple>(t), Indices{});
}

In den meisten Fällen ist es jedoch besser, diesen Rückgabetyp beizubehalten. In dem speziellen Fall, den ich oben beschrieben habe, ist der Rückgabetyp ziemlich unlesbar und ein potenzieller Benutzer kann nichts davon wissen. Eine gute Dokumentation mit Beispielen wird viel nützlicher sein.


Eine andere Sache, die noch nicht erwähnt wurde: Während declype(t+u) die Verwendung von expression SFINAE erlaubt, ist decltype(auto) nicht möglich (obwohl es einen Vorschlag zur Änderung dieses Verhaltens gibt). Nehmen Sie zum Beispiel eine foobar-Funktion, die die Member-Funktion foo eines Typs anruft, falls vorhanden, oder die bar-Member-Funktion des Typs, falls vorhanden, und nehmen Sie an, dass eine Klasse immer genau foo oder bar hat, aber beides nicht gleichzeitig:

struct X
{
    void foo() const { std::cout << "foo\n"; }
};

struct Y
{
    void bar() const { std::cout << "bar\n"; }
};

template<typename C> 
auto foobar(const C& c) -> decltype(c.foo())
{
    return c.foo();
}

template<typename C> 
auto foobar(const C& c) -> decltype(c.bar())
{
    return c.bar();
}

Wenn Sie foobar für eine Instanz von X aufrufen, wird foo angezeigt, während Sie foobar für eine Instanz von Y aufrufen. bar. Wenn Sie stattdessen den automatischen Rückgabetyp verwenden (mit oder ohne decltype(auto)), erhalten Sie nicht den Ausdruck SFINAE und der Aufruf von foobar für eine Instanz von X oder Y löst einen Kompilierungsfehler aus.

22
Morwenn

Es ist nie nötig. Wann Sie sollten- Sie erhalten dazu viele verschiedene Antworten. Ich würde nicht sagen, bis es tatsächlich ein akzeptierter Teil des Standards ist und von der Mehrheit der großen Compiler auf dieselbe Weise gut unterstützt wird.

Darüber hinaus wird es ein religiöses Argument sein. Ich persönlich würde sagen, dass das Einfügen des tatsächlichen Rückgabetyps den Code klarer macht, für die Wartung viel einfacher ist (ich kann mir die Signatur einer Funktion ansehen und wissen, was sie zurückgibt, anstatt den Code tatsächlich lesen zu müssen) Sie meinen, es sollte einen Typ zurückgeben, und der Compiler denkt, dass ein anderer Probleme verursacht (wie bei jeder Skriptsprache, die ich jemals verwendet habe). Ich denke, Auto war ein riesiger Fehler und es wird mehr Schmerzen verursachen als Hilfe. Andere werden sagen, Sie sollten es ständig verwenden, da es zu ihrer Programmierphilosophie passt. Jedenfalls ist dies für diese Site weit aus dem Rahmen.

7
Gabe Sechan

Es hat nichts mit der Einfachheit der Funktion zu tun (als jetzt gelöschtes Duplikat dieser Frage vermutlich).

Entweder ist der Rückgabetyp fest (verwenden Sie nicht auto) oder abhängig von einem Vorlagenparameter (verwenden Sie in den meisten Fällen auto, gekoppelt mit decltype, wenn mehrere Rückgabepunkte vorhanden sind).

7
Ben Voigt

Stellen Sie sich eine reale Produktionsumgebung vor: Viele Funktionen und Komponententests hängen alle vom Rückgabetyp von foo() ab. Angenommen, der Rückgabetyp muss aus irgendeinem Grund geändert werden.

Wenn der Rückgabetyp überall auto ist und Aufrufer von foo() und verwandten Funktionen beim Abrufen des zurückgegebenen Wertes auto verwenden, sind die erforderlichen Änderungen minimal. Wenn dies nicht der Fall ist, kann dies stundenlange und fehleranfällige Arbeit bedeuten.

Als reales Beispiel wurde ich gebeten, ein Modul von der Verwendung von Rohzeiger überall hin zu intelligenten Zeigern zu ändern. Das Fixieren der Unit-Tests war schmerzhafter als der eigentliche Code.

Zwar gibt es andere Möglichkeiten, wie dies gehandhabt werden könnte, die Verwendung von auto-Rückgabetypen scheint jedoch eine gute Passform zu sein.

3
Jim Wood

Ich möchte ein Beispiel geben, bei dem der Rückgabetyp auto perfekt ist:

Stellen Sie sich vor, Sie möchten einen kurzen Alias ​​für einen langen nachfolgenden Funktionsaufruf erstellen. Mit auto brauchen Sie sich nicht um den ursprünglichen Rückgabetyp zu kümmern (möglicherweise ändert sich dieser in der Zukunft) und der Benutzer kann auf die ursprüngliche Funktion klicken, um den echten Rückgabetyp zu erhalten:

inline auto CreateEntity() { return GetContext()->GetEntityManager()->CreateEntity(); }

PS: Hängt von dieser Frage ab.

2
kaiser

Für Szenario 3 würde ich den Rückgabetyp der Funktionssignatur mit der lokalen Variablen umkehren, die zurückgegeben werden soll. Dies würde es für Client-Programmierer klarer machen, die die Funktion zurückgibt.

Szenario 3 Um Redundanz zu vermeiden:

std::vector<std::map<std::pair<int, double>, int>> foo() {
    decltype(foo()) ret;
    return ret;
}

Ja, es hat kein automatisches Schlüsselwort, aber der Prinzipal ist derselbe, um Redundanz zu verhindern und Programmierern, die keinen Zugriff auf die Quelle haben, eine einfachere Zeit zu geben.

0