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

144

Mit GCC 4.8.0 haben wir einen Compiler, der den automatischen Rückzugstypabzug unterstützt und Teil von C ++ 14 ist. Mit -std=c++1ykann ich das machen:

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

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

Szenario 1

Das erste Szenario, an das ich denken kann, ist wann immer möglich. Jede Funktion, die auf diese Weise geschrieben werden kann, sollte sein. Das Problem dabei ist, dass der Code möglicherweise nicht immer besser lesbar ist.

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 es in einigen Fällen klarer sein könnte, wenn der Rückgabetyp explizit von den Parametern abhängt.

Szenario 3

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 nur return {5, 6, 7};einen Vektor ersetzen, aber das funktioniert nicht immer und wir müssen den Typ sowohl im Funktionsheader als auch im Funktionskörper angeben. Dies ist rein redundant, und der automatische Abzug 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, um den genauen Typ zu erfahren, und wenn sie dort nicht bereitgestellt wird, müssen wir zu einem anderen Punkt im Code gehen, z. B. wo pos_deklariert wird.

Fazit

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

Beachten Sie, dass die mehreren Fragen dazu dienen sollen, Perspektiven zu finden, aus denen diese beantwortet werden können.

chris
quelle
18
Wunderbare Frage! Während Sie fragen, welche Szenarien den Code "besser" machen, frage ich mich auch, welche Szenarien ihn schlechter machen werden .
Drew Dormann
2
@DrewDormann, das frage ich mich auch. Ich nutze gerne neue Funktionen, aber es ist sehr wichtig zu wissen, wann ich sie verwenden soll und wann nicht. Es gibt eine Zeitspanne, in der neue Funktionen auftauchen, um dies herauszufinden. Machen wir es jetzt, damit wir bereit sind, wenn es offiziell kommt :)
Chris
2
@NicolBolas, vielleicht, aber die Tatsache, dass es sich um eine aktuelle Version eines Compilers handelt, würde ausreichen, damit die Leute damit beginnen können, ihn in persönlichem Code zu verwenden (er muss an dieser Stelle definitiv von Projekten ferngehalten werden). Ich gehöre zu den Leuten, die gerne die neuesten Funktionen in meinem eigenen Code verwenden, und obwohl ich nicht weiß, wie gut der Vorschlag beim Ausschuss ankommt, denke ich, dass es die erste ist, die in dieser neuen Option enthalten ist etwas. Es könnte besser für später bleiben oder (ich weiß nicht, wie gut es funktionieren würde) wiederbelebt werden, wenn wir sicher sind, dass es kommt.
Chris
1
@NicolBolas, Wenn es hilft, ist es jetzt angenommen worden: p
chris
1
Die aktuellen Antworten scheinen nicht zu erwähnen, dass das Ersetzen ->decltype(t+u)durch automatischen Abzug SFINAE tötet.
Marc Glisse

Antworten:

62

C ++ 11 wirft ähnliche Fragen auf: Wann wird der Rückgabetypabzug in Lambdas verwendet und wann werden autoVariablen verwendet ?

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 Typabzugswerkzeuge ein, damit Sie den Typ an neuen Stellen weglassen können.

Entschuldigung, aber Sie werden dies nicht im Voraus lösen, indem Sie allgemeine Regeln festlegen. Sie müssen sich einen bestimmten Code ansehen und selbst entscheiden, ob er die Lesbarkeit verbessert, um überall Typen anzugeben: Ist es besser, wenn Ihr Code sagt: "Der Typ dieser Sache ist X", oder ist es besser für Ihr Code sagt: "Der Typ dieser Sache ist für das Verständnis 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 darüber hinaus je nach Leser unterschiedlich ist, haben Sie als Autor / Herausgeber eines Codes die Verantwortung, die von einem Styleguide nicht vollständig erfüllt werden kann. Selbst in dem Maße, in dem ein Styleguide Normen festlegt, bevorzugen unterschiedliche Personen unterschiedliche Normen und neigen dazu, etwas Unbekanntes als "weniger lesbar" zu empfinden. Daher kann die Lesbarkeit einer bestimmten vorgeschlagenen Stilregel häufig nur im Kontext der anderen vorhandenen Stilregeln beurteilt werden.

Alle Ihre Szenarien (auch die ersten) werden für den Codierungsstil einer Person Verwendung finden. Persönlich finde ich, dass der zweite der überzeugendste Anwendungsfall ist, aber ich gehe trotzdem davon aus, dass er von Ihren Dokumentationstools abhängt. Es ist nicht sehr hilfreich zu sehen, dass der Rückgabetyp einer Funktionsvorlage autodokumentiert ist , während die Dokumentation decltype(t+u)eine veröffentlichte Schnittstelle erstellt, auf die Sie sich (hoffentlich) verlassen können.

[*] Gelegentlich versucht jemand, objektive Messungen durchzuführen. In dem geringen Maße, dass jemand jemals statistisch signifikante und allgemein gültige Ergebnisse erzielt, werden sie von arbeitenden Programmierern zugunsten des Instinkts des Autors, was "lesbar" ist, völlig ignoriert.

Steve Jessop
quelle
1
Guter Punkt bei den Verbindungen zu Lambdas (obwohl dies komplexere Funktionskörper ermöglicht). Die Hauptsache ist, dass es sich um eine neuere Funktion handelt, und ich versuche immer noch, die Vor- und Nachteile jedes Anwendungsfalls auszugleichen. Um dies zu tun, ist es hilfreich zu sehen, warum es für was verwendet wird, damit ich selbst herausfinden kann, warum ich das, was ich tue, bevorzuge. Ich könnte es überdenken, aber so bin ich.
Chris
1
@chris: Wie ich schon sagte, ich denke, es kommt alles auf dasselbe an. Ist es besser für Ihren Code zu sagen, "der Typ dieser Sache ist X", oder ist es besser für Ihren Code zu sagen, "der Typ für diese Sache ist irrelevant, der Compiler muss es wissen und wir könnten es wahrscheinlich herausfinden." aber wir müssen es nicht sagen ". Wenn wir schreiben 1.0 + 27U, behaupten wir das letztere, wenn wir schreiben (double)1.0 + (double)27U, behaupten wir das erstere. Die Einfachheit der Funktion, der Grad der Vervielfältigung und die Vermeidung decltypekönnten alle dazu beitragen, aber keine wird zuverlässig entscheidend sein.
Steve Jessop
1
Ist es besser, wenn Ihr Code sagt: "Der Typ dieser Sache ist X", oder ist es besser, wenn Ihr Code sagt: "Der Typ dieser Sache ist irrelevant, der Compiler muss es wissen und wir könnten es wahrscheinlich herausfinden." aber wir müssen es nicht sagen ". - Dieser Satz entspricht genau dem, wonach ich suche. Ich werde dies berücksichtigen, wenn ich auf Optionen zur Verwendung dieser Funktion stoße, und zwar autoim Allgemeinen.
Chris
1
Ich möchte hinzufügen, dass IDEs das "Lesbarkeitsproblem" lindern könnten. Nehmen Sie zum Beispiel Visual Studio: Wenn Sie mit der Maus über das autoSchlüsselwort fahren, wird der tatsächliche Rückgabetyp angezeigt.
andreee
1
@andreee: das stimmt in grenzen. Wenn ein Typ viele Aliase hat, ist es nicht immer so nützlich, den tatsächlichen Typ zu kennen, wie Sie es sich erhoffen. Zum Beispiel könnte ein Iteratortyp sein int*, aber was tatsächlich von Bedeutung ist, wenn überhaupt, ist der Grund dafür, int*dass dies std::vector<int>::iterator_typebei Ihren aktuellen Build-Optionen der Fall ist!
Steve Jessop
30

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 hilfreich sein könnte, diesen Rückgabetyp zu löschen, um Redundanz 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 dem offiziellen Ausschusspapier N3493 . Der Zweck der Funktion applybesteht darin, die Elemente von a std::tuplean eine Funktion weiterzuleiten und das Ergebnis zurückzugeben. Die int_seqund make_int_seqsind nur ein Teil der Implementierung und werden wahrscheinlich nur Benutzer verwirren, die versuchen zu verstehen, was sie tun.

Wie Sie sehen können, ist der Rückgabetyp nichts anderes als ein decltypezurückgegebener Ausdruck. Da apply_ich nicht dafür gedacht bin, von den Benutzern gesehen zu werden, bin ich mir nicht sicher, ob es nützlich ist, den Rückgabetyp zu dokumentieren, wenn er mehr oder weniger dem von ihm entspricht apply. Ich denke, dass in diesem speziellen Fall das Löschen des Rückgabetyps die Funktion lesbarer macht. Beachten Sie, dass genau dieser Rückgabetyp decltype(auto)im Vorschlag zur Ergänzung applydes Standards N3915 tatsächlich gelöscht und durch ersetzt wurde (beachten Sie auch, dass meine ursprüngliche Antwort älter ist als dieses Papier):

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 oben beschriebenen speziellen Fall ist der Rückgabetyp ziemlich unlesbar und ein potenzieller Benutzer wird nichts davon haben, ihn zu kennen. Eine gute Dokumentation mit Beispielen ist weitaus nützlicher.


Eine andere Sache, die noch nicht erwähnt wurde: Während declype(t+u)die Verwendung des Ausdrucks SFINAE erlaubt ist , ist decltype(auto)dies nicht der Fall (obwohl es einen Vorschlag gibt , dieses Verhalten zu ändern). Nehmen Sie zum Beispiel eine foobarFunktion, die die fooMitgliedsfunktion eines Typs baraufruft, wenn sie existiert, oder die Mitgliedsfunktion des Typs, wenn sie existiert, und nehmen Sie an, dass eine Klasse immer genau foooder baraber nicht beide gleichzeitig hat:

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();
}

Das Aufrufen foobareiner Instanz von Xwird angezeigt, foowährend das Aufrufen foobareiner Instanz von Yangezeigt wird bar. Wenn Sie stattdessen den automatischen Rückgabetypabzug verwenden (mit oder ohne decltype(auto)), erhalten Sie keinen Ausdruck SFINAE und rufen foobareine Instanz von entweder auf Xoder Ylösen einen Fehler bei der Kompilierung aus.

Morwenn
quelle
8

Es ist niemals notwendig. Wann Sie sollten - Sie werden viele verschiedene Antworten darauf erhalten. Ich würde überhaupt nicht sagen, bis es tatsächlich ein akzeptierter Teil des Standards ist und von der Mehrheit der großen Compiler auf die gleiche Weise gut unterstützt wird.

Darüber hinaus wird es ein religiöses Argument sein. Ich persönlich würde sagen, dass das Nicht-Eingeben des tatsächlichen Rückgabetyps den Code klarer macht, für die Wartung viel einfacher ist (ich kann die Signatur einer Funktion überprüfen und wissen, was sie zurückgibt, anstatt den Code tatsächlich lesen zu müssen), und dies beseitigt die Möglichkeit, dass Sie denken, es sollte einen Typ zurückgeben, und der Compiler glaubt, dass ein anderer Probleme verursacht (wie es bei jeder Skriptsprache passiert ist, die ich jemals verwendet habe). Ich denke, Auto war ein riesiger Fehler und es wird um Größenordnungen mehr Schmerz als Hilfe verursachen. Andere werden sagen, Sie sollten es die ganze Zeit verwenden, da es zu ihrer Programmierphilosophie passt. In jedem Fall liegt dies außerhalb des Geltungsbereichs dieser Website.

Gabe Sechan
quelle
1
Ich bin damit einverstanden, dass Menschen mit unterschiedlichem Hintergrund unterschiedliche Meinungen dazu haben, aber ich hoffe, dass dies zu einem C ++ - Ergebnis kommt. In (fast) jedem Fall ist es unentschuldbar, Sprachfunktionen zu missbrauchen, um zu versuchen, sie in eine andere #definezu verwandeln , z. B. die Verwendung von s, um C ++ in VB umzuwandeln. Features haben im Allgemeinen einen guten Konsens innerhalb der Denkweise der Sprache darüber, was richtig verwendet wird und was nicht, je nachdem, was die Programmierer dieser Sprache gewohnt sind. Dieselbe Funktion kann in mehreren Sprachen vorhanden sein, aber jede hat ihre eigenen Richtlinien für die Verwendung.
Chris
2
Einige Funktionen haben einen guten Konsens. Viele tun es nicht. Ich kenne viele Programmierer, die denken, dass der Großteil von Boost Müll ist, der nicht verwendet werden sollte. Ich kenne auch einige, die denken, dass es das Beste ist, was C ++ passieren kann. In beiden Fällen denke ich, dass es einige interessante Diskussionen gibt, aber es ist wirklich ein genaues Beispiel für die enge als nicht konstruktive Option auf dieser Seite.
Gabe Sechan
2
Nein, ich stimme Ihnen überhaupt nicht zu, und ich denke, das autoist reiner Segen. Es beseitigt viel Redundanz. Es ist einfach ein Schmerz, den Rückgabetyp manchmal zu wiederholen. Wenn Sie ein Lambda zurückgeben möchten, kann dies sogar unmöglich sein, ohne das Ergebnis in einem zu speichern std::function, was zu einem gewissen Overhead führen kann.
Yongwei Wu
1
Es gibt mindestens eine Situation, in der dies alles andere als notwendig ist. Angenommen, Sie müssen Funktionen aufrufen und dann die Ergebnisse protokollieren, bevor Sie sie zurückgeben, und die Funktionen müssen nicht unbedingt alle denselben Rückgabetyp haben. Wenn Sie dies über eine Protokollierungsfunktion tun, die Funktionen und ihre Argumente als Parameter verwendet, müssen Sie den Rückgabetyp der Protokollierungsfunktion deklarieren, autodamit er immer mit dem Rückgabetyp der übergebenen Funktion übereinstimmt.
Justin Time - Stellen Sie Monica am
1
[Es gibt technisch gesehen einen anderen Weg, dies zu tun, aber es verwendet Template-Magie, um im Grunde genau das Gleiche zu tun, und benötigt weiterhin die Protokollierungsfunktion autofür den nachfolgenden Rückgabetyp.]
Justin Time - Reinstate Monica
7

Es hat nichts mit der Einfachheit der Funktion zu tun (wie ein jetzt gelöschtes Duplikat dieser Frage angenommen wird).

Entweder ist der Rückgabetyp fest (nicht verwenden auto) oder in komplexer Weise von einem Vorlagenparameter abhängig ( autoin den meisten Fällen verwendet, gepaart mit, decltypewenn mehrere Rückgabepunkte vorhanden sind).

Ben Voigt
quelle
3

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

Wenn der Rückgabetyp autoüberall ist und Aufrufer foo()und verwandte Funktionen autobeim Abrufen des zurückgegebenen Werts verwenden, sind die erforderlichen Änderungen minimal. Wenn nicht, könnte dies Stunden extrem mühsamer und fehleranfälliger Arbeit bedeuten.

Als Beispiel aus der Praxis wurde ich gebeten, ein Modul von der Verwendung von Rohzeigern überall auf intelligente Zeiger umzustellen. Das Reparieren der Unit-Tests war schmerzhafter als der eigentliche Code.

Es gibt zwar andere Möglichkeiten, wie dies gehandhabt werden könnte, aber die Verwendung von autoRückgabetypen scheint gut zu passen.

Jim Wood
quelle
1
Wäre es in diesen Fällen nicht besser zu schreiben decltype(foo())?
Oren S
Persönlich bin ich der Meinung, dass typedefs- und alias-Deklarationen (dh usingTypdeklarationen) in diesen Fällen besser sind, insbesondere wenn sie klassenbezogen sind.
MAChitgarha
3

Ich möchte ein Beispiel geben, in 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 müssen Sie sich nicht um den ursprünglichen Rückgabetyp kümmern (möglicherweise ändert er sich in Zukunft), und der Benutzer kann auf die ursprüngliche Funktion klicken, um den tatsächlichen Rückgabetyp zu erhalten:

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

PS: Kommt auf diese Frage an.

Kaiser
quelle
2

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

Szenario 3 So verhindern Sie Redundanz:

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

Ja, es gibt kein Auto-Schlüsselwort, aber der Principal ist derselbe, um Redundanz zu vermeiden und Programmierern, die keinen Zugriff auf die Quelle haben, die Arbeit zu erleichtern.

Servieren Sie Laurijssen
quelle
2
IMO ist die bessere Lösung dafür, einen domänenspezifischen Namen für das Konzept dessen bereitzustellen, was als dargestellt werden soll, vector<map<pair<int,double>,int>und diesen dann zu verwenden.
Davidbak