Kiedy należy używać automatycznego potrącenia typu zwracanego w języku C ++ 14?

144

Wraz z wydaniem GCC 4.8.0 mamy kompilator obsługujący automatyczne odejmowanie zwracanych typów, część C ++ 14. Dzięki -std=c++1y, mogę to zrobić:

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

Moje pytanie brzmi: kiedy powinienem używać tej funkcji? Kiedy jest to konieczne i kiedy powoduje, że kod staje się czystszy?

Scenariusz 1

Pierwszy scenariusz, jaki przychodzi mi do głowy, jest możliwy, kiedy tylko jest to możliwe. Każda funkcja, którą można w ten sposób zapisać, powinna być. Problem polega na tym, że nie zawsze może to uczynić kod bardziej czytelnym.

Scenariusz 2

Następnym scenariuszem jest uniknięcie bardziej złożonych typów zwrotów. Jako bardzo lekki przykład:

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

Nie wierzę, że kiedykolwiek byłby to naprawdę problem, chociaż wydaje mi się, że posiadanie typu zwracanego jawnie zależnego od parametrów może być w niektórych przypadkach wyraźniejsze.

Scenariusz 3

Następnie, aby zapobiec redundancji:

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

W C ++ 11 możemy czasami po prostu return {5, 6, 7};zastąpić wektor, ale to nie zawsze działa i musimy określić typ zarówno w nagłówku funkcji, jak i jej treści. Jest to całkowicie nadmiarowe, a automatyczne odliczanie typu zwracanego chroni nas przed tą nadmiarowością.

Scenariusz 4

Wreszcie może być używany zamiast bardzo prostych funkcji:

auto position() {
    return pos_;
}

auto area() {
    return length_ * width_;
}

Czasami jednak możemy spojrzeć na funkcję, chcąc poznać dokładny typ, a jeśli nie jest tam podany, musimy przejść do innego punktu w kodzie, np. Gdzie pos_jest zadeklarowane.

Wniosek

Po przedstawieniu tych scenariuszy, który z nich faktycznie okazuje się sytuacją, w której ta funkcja jest przydatna w oczyszczaniu kodu? A co ze scenariuszami, o których tu zapomniałem? Jakie środki ostrożności należy podjąć przed użyciem tej funkcji, aby później mnie nie ugryzła? Czy jest coś nowego, co ta funkcja wnosi do stołu, a bez niej nie jest możliwe?

Zwróć uwagę, że wiele pytań ma być pomocą w znalezieniu perspektyw, z których można na to odpowiedzieć.

chris
źródło
18
Cudowne pytanie! Pytając, które scenariusze sprawiają, że kod jest „lepszy”, zastanawiam się również, które scenariusze go pogorszą .
Drew Dormann
2
@DrewDormann, Ja też się nad tym zastanawiam. Lubię korzystać z nowych funkcji, ale wiedza, kiedy ich używać, a kiedy nie, jest bardzo ważna. Jest taki okres, w którym pojawiają się nowe funkcje, które wykorzystujemy, aby to rozgryźć, więc zróbmy to teraz, abyśmy byli gotowi, gdy nadejdzie oficjalnie :)
chris
2
@NicolBolas, być może, ale fakt, że znajduje się on teraz w rzeczywistym wydaniu kompilatora, wystarczyłby, aby ludzie zaczęli go używać w kodzie osobistym (na tym etapie zdecydowanie należy trzymać go z dala od projektów). Jestem jedną z tych osób, które lubią używać najnowszych możliwych funkcji w moim własnym kodzie i chociaż nie wiem, jak dobrze wygląda propozycja z komitetem, myślę, że jest to pierwsza zawarta w tej nowej opcji coś. Lepiej byłoby zostawić to na później lub (nie wiem, jak dobrze by to zadziałało) wskrzeszone, kiedy wiemy na pewno, że nadejdzie.
chris
1
@NicolBolas, Jeśli to pomaga, zostało przyjęte teraz: p
chris
1
Obecne odpowiedzi nie wydają się wspominać, że zastąpienie ->decltype(t+u)autodliczeniem zabija SFINAE.
Marc Glisse

Odpowiedzi:

62

C ++ 11 rodzi podobne pytania: kiedy używać dedukcji typu zwracanego w lambdach, a kiedy autozmiennych.

Tradycyjna odpowiedź na pytanie w C i C ++ 03 brzmiała: „ponad granicami instrukcji uczynimy typy jawnymi, w wyrażeniach zazwyczaj są one niejawne, ale możemy uczynić je jawnymi za pomocą rzutowania”. C ++ 11 i C ++ 1y wprowadzają narzędzia do odliczania typów, dzięki czemu można pominąć typ w nowych miejscach.

Przepraszamy, ale nie rozwiążesz tego z góry, ustalając ogólne zasady. Musisz przyjrzeć się konkretnemu kodowi i samemu zdecydować, czy poprawi on czytelność określania typów w każdym miejscu: czy lepiej jest, aby twój kod mówił „typem tego jest X”, czy lepiej dla Twój kod mówiący: „rodzaj tego nie ma znaczenia dla zrozumienia tej części kodu: kompilator musi wiedzieć i prawdopodobnie moglibyśmy to rozwiązać, ale nie musimy tego tutaj mówić”?

Ponieważ „czytelność” nie jest obiektywnie zdefiniowana [*], a ponadto różni się w zależności od czytelnika, jako autor / redaktor fragmentu kodu nie można w pełni zadowolić się przewodnikiem po stylu. Nawet jeśli przewodnik po stylu określa normy, różni ludzie będą preferować inne normy i będą mieli tendencję do stwierdzania, że ​​coś nieznanego jest „mniej czytelne”. Tak więc czytelność określonej proponowanej reguły stylu można często ocenić tylko w kontekście innych obowiązujących reguł stylu.

Wszystkie Twoje scenariusze (nawet pierwszy) znajdą zastosowanie w czyimś stylu kodowania. Osobiście uważam, że drugi przypadek jest najbardziej przekonujący, ale mimo to spodziewam się, że będzie to zależało od narzędzi do dokumentacji. Nie jest zbyt pomocne, aby zobaczyć udokumentowany typ zwracanego szablonu funkcji auto, podczas gdy zobaczenie, że jest on udokumentowany jako decltype(t+u)tworzy opublikowany interfejs, na którym możesz (miejmy nadzieję) polegać.

[*] Czasami ktoś próbuje dokonać obiektywnych pomiarów. W niewielkim stopniu, w jakim ktokolwiek wymyśli jakiekolwiek statystycznie istotne i ogólnie nadające się do zastosowania wyniki, są one całkowicie ignorowane przez pracujących programistów na rzecz instynktu autora co do tego, co jest „czytelne”.

Steve Jessop
źródło
1
Dobra uwaga, jeśli chodzi o połączenia z lambdami (chociaż pozwala to na bardziej złożone ciała funkcyjne). Najważniejsze jest to, że jest to nowsza funkcja i wciąż staram się wyważyć zalety i wady każdego przypadku użycia. Aby to zrobić, dobrze jest zobaczyć powody, dla których miałoby to być używane, aby samemu odkryć, dlaczego wolę to, co robię. Może za bardzo się nad tym zastanawiam, ale taki właśnie jestem.
chris
1
@chris: jak mówię, myślę, że wszystko sprowadza się do tego samego. Czy lepiej dla twojego kodu powiedzieć „typ tego elementu to X”, czy też lepiej, jeśli twój kod mówi, „typ tego jest nieistotny, kompilator musi wiedzieć i prawdopodobnie moglibyśmy to rozwiązać ale nie musimy tego mówić ”. Kiedy piszemy 1.0 + 27U, potwierdzamy to drugie, kiedy piszemy (double)1.0 + (double)27U, potwierdzamy to pierwsze. Prostota funkcji, stopień powielania, unikanie decltypemogą się do tego przyczynić, ale żadna z nich nie będzie decydująca.
Steve Jessop
1
Czy lepiej dla twojego kodu powiedzieć „typ tego elementu to X”, czy też lepiej, jeśli twój kod mówi, „typ tego jest nieistotny, kompilator musi wiedzieć i prawdopodobnie moglibyśmy to rozwiązać ale nie musimy tego mówić ”. - To zdanie jest dokładnie zgodne z tym, czego szukam. Biorę to pod uwagę, gdy napotykam opcje korzystania z tej funkcji i autoogólnie.
chris
1
Chciałbym dodać, że IDE mogą złagodzić „problem z czytelnością”. Weźmy na przykład program Visual Studio: jeśli umieścisz kursor nad autosłowem kluczowym, wyświetli rzeczywisty zwracany typ.
andreee
1
@andreee: to prawda w pewnych granicach. Jeśli typ ma wiele aliasów, znajomość rzeczywistego typu nie zawsze jest tak przydatna, jak byś miał nadzieję. Na przykład typ iteratora może być int*, ale to, co jest naprawdę istotne, jeśli cokolwiek, to to, że powodem jest int*to, że tak std::vector<int>::iterator_typejest z twoimi obecnymi opcjami kompilacji!
Steve Jessop
30

Ogólnie rzecz biorąc, zwracany typ funkcji jest bardzo pomocny w dokumentowaniu funkcji. Użytkownik będzie wiedział, czego się oczekuje. Jest jednak jeden przypadek, w którym myślę, że fajnie byłoby porzucić ten typ powrotu, aby uniknąć nadmiarowości. Oto przykład:

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

Ten przykład pochodzi z oficjalnego dokumentu komisji N3493 . Celem funkcji applyjest przekazanie elementów a std::tupledo funkcji i zwrócenie wyniku. int_seqA make_int_seqsą jedynie częścią realizacji i będzie prawdopodobnie tylko mylić każdy użytkownik próbuje zrozumieć, co robi.

Jak widać, zwracany typ to nic innego jak decltypezwrócone wyrażenie. Co więcej, apply_nie będąc przeznaczonym dla użytkowników, nie jestem pewien przydatności dokumentowania jego typu zwracanego, gdy jest mniej więcej taki sam jak applyten. Myślę, że w tym konkretnym przypadku porzucenie zwracanego typu sprawia, że ​​funkcja jest bardziej czytelna. Zwróć uwagę, że ten właśnie zwracany typ został faktycznie usunięty i zastąpiony przez decltype(auto)propozycję dodania applydo standardu N3915 (zwróć również uwagę, że moja oryginalna odpowiedź pochodzi sprzed tego artykułu):

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

Jednak w większości przypadków lepiej jest zachować ten typ zwracania. W konkretnym przypadku, który opisałem powyżej, typ zwracany jest raczej nieczytelny i potencjalny użytkownik nic nie zyska na jego znajomości. O wiele bardziej przydatna będzie dobra dokumentacja z przykładami.


Kolejna rzecz, o której jeszcze nie wspomniano: podczas declype(t+u) pozwala na użycie wyrażenia SFINAE , decltype(auto)nie (mimo że jest propozycja zmiany tego zachowania). Weźmy na przykład foobarfunkcję, która wywoła foofunkcję barskładową typu, jeśli istnieje, foolub wywoła funkcję składową typu, jeśli istnieje, i załóż, że klasa zawsze ma dokładność lub barnie ma obu jednocześnie:

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

Wywołanie foobarwystąpienia Xwill displayfoo podczas wywołania foobarwystąpienia Ywill display bar. Jeśli zamiast tego użyjesz automatycznego odliczenia typu zwracanego (z lub bez decltype(auto)), nie otrzymasz wyrażenia SFINAE i wywołanie foobarwystąpienia któregokolwiek z nich Xlub Ywyzwoli błąd w czasie kompilacji.

Morwenn
źródło
8

To nigdy nie jest konieczne. Jeśli chodzi o to, kiedy powinieneś - otrzymasz wiele różnych odpowiedzi na ten temat. Powiedziałbym, że nie, dopóki nie będzie to faktycznie akceptowana część standardu i dobrze obsługiwana przez większość głównych kompilatorów w ten sam sposób.

Poza tym będzie to argument religijny. Osobiście powiedziałbym nigdy - wprowadzenie rzeczywistego typu zwracanego kodu sprawia, że ​​kod jest jaśniejszy, jest znacznie łatwiejszy w utrzymaniu (mogę spojrzeć na podpis funkcji i wiedzieć, co zwraca, zamiast czytać kod) i eliminuje możliwość, że myślisz, że powinien zwrócić jeden typ, a kompilator uważa, że ​​inny powoduje problemy (tak jak stało się z każdym językiem skryptowym, którego kiedykolwiek używałem). Myślę, że auto było gigantycznym błędem i spowoduje o rząd wielkości więcej bólu niż pomocy. Inni powiedzą, że powinieneś go używać przez cały czas, ponieważ pasuje to do ich filozofii programowania. W każdym razie jest to poza zakresem tej witryny.

Gabe Sechan
źródło
1
Zgadzam się, że ludzie z różnych środowisk będą mieli różne opinie na ten temat, ale mam nadzieję, że dojdzie do wniosku w C ++. W (prawie) każdym przypadku niewybaczalne jest nadużywanie funkcji języka, aby spróbować przekształcić go w inny, na przykład użycie #defines do przekształcenia C ++ w VB. Funkcje generalnie są zgodne z mentalnością języka co do tego, co jest właściwe, a co nie, zgodnie z tym, do czego są przyzwyczajeni programiści tego języka. Ta sama funkcja może być dostępna w wielu językach, ale każdy ma własne wytyczne dotyczące jej używania.
chris
2
Niektóre funkcje są zgodne. Wielu tego nie robi. Znam wielu programistów, którzy uważają, że większość Boost to śmieci, których nie należy używać. Znam też niektórych, którzy uważają, że to najlepsza rzecz, jaka przydarzyła się C ++. W każdym razie myślę, że jest kilka interesujących dyskusji na ten temat, ale jest to naprawdę dokładny przykład zamknięcia jako nie konstruktywnej opcji na tej stronie.
Gabe Sechan
2
Nie, zupełnie się z tobą nie zgadzam i myślę, że autoto czyste błogosławieństwo. Usuwa wiele nadmiarowości. Czasami powtarzanie typu powrotu jest po prostu uciążliwe. Jeśli chcesz zwrócić lambdę, może to być nawet niemożliwe bez zapisania wyniku w a std::function, co może wiązać się z pewnym narzutem.
Yongwei Wu
1
Jest przynajmniej jedna sytuacja, w której jest to wszystko, ale całkowicie konieczne. Załóżmy, że musisz wywołać funkcje, a następnie zarejestrować wyniki przed ich zwróceniem, a funkcje niekoniecznie mają ten sam typ zwracania. Jeśli zrobisz to za pomocą funkcji rejestrującej, która przyjmuje funkcje i ich argumenty jako parametry, musisz zadeklarować typ zwracany przez funkcję rejestrowania, autoaby zawsze był zgodny z typem zwracanym przekazanej funkcji.
Justin Time - Przywróć Monikę
1
[Jest technicznie inny sposób, aby to zrobić, ale używa magii szablonów, aby zrobić dokładnie to samo i nadal potrzebuje funkcji rejestrowania, aby była auto dla końcowego typu powrotu.]
Justin Time - Przywróć Monikę
7

Nie ma to nic wspólnego z prostotą funkcji (jak przypuszcza się usunięty teraz duplikat tego pytania).

Typ zwracany jest stały (nie używaj auto) lub zależy w złożony sposób od parametru szablonu (używaj autow większości przypadków w połączeniu z decltypewieloma punktami powrotu).

Ben Voigt
źródło
3

Rozważmy rzeczywiste środowisko produkcyjne: wiele funkcji i testów jednostkowych jest współzależnych od zwracanego typu foo() . Załóżmy teraz, że typ zwracany musi się zmienić z jakiegokolwiek powodu.

Jeśli typ zwracany jest autowszędzie, a osoby wywołujące foo()i powiązane funkcje używająauto zwracanej wartości, zmiany, które należy wprowadzić, są minimalne. Jeśli nie, może to oznaczać godziny wyjątkowo żmudnej i podatnej na błędy pracy.

Jako przykład ze świata rzeczywistego poproszono mnie o zmianę modułu z używania wszędzie wskaźników surowych na inteligentne wskaźniki. Naprawianie testów jednostkowych było bardziej bolesne niż sam kod.

Chociaż istnieją inne sposoby rozwiązania tego problemu, użycie autotypów zwracanych wydaje się być dobrym rozwiązaniem.

Jim Wood
źródło
1
Czy w takich przypadkach nie byłoby lepiej pisać decltype(foo())?
Oren S
Osobiście uważam, że typedefdeklaracje s i aliasów (tj. usingDeklaracja typu) są lepsze w takich przypadkach, szczególnie gdy mają zakres klas.
MAChitgarha
3

Chcę podać przykład, w którym auto zwracanego typu jest idealne:

Wyobraź sobie, że chcesz utworzyć krótki alias dla długiego kolejnego wywołania funkcji. Dzięki auto nie musisz dbać o oryginalny typ zwrotu (być może zmieni się w przyszłości), a użytkownik może kliknąć oryginalną funkcję, aby uzyskać prawdziwy typ zwrotu:

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

PS: Zależy od tego pytania.

kajzer
źródło
2

W scenariuszu 3 zamieniłbym zwracany typ podpisu funkcji na zwracaną zmienną lokalną. Uczyniłoby to bardziej zrozumiałym dla programistów-klientów, kiedy funkcja zwraca. Lubię to:

Scenariusz 3 Aby zapobiec redundancji:

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

Tak, nie ma słowa kluczowego auto, ale zasada jest taka sama, aby zapobiec redundancji i dać łatwiejszy czas programistom, którzy nie mają dostępu do źródła.

Służ Laurijssen
źródło
2
IMO lepszym rozwiązaniem jest podanie nazwy domeny dla koncepcji tego, co ma być reprezentowane jako a, vector<map<pair<int,double>,int>a następnie użycie tego.
davidbak