Quando devo utilizzare la detrazione automatica del tipo di ritorno C ++ 14?


144

Con GCC 4.8.0 rilasciato, abbiamo un compilatore che supporta la deduzione automatica del tipo di ritorno, parte di C ++ 14. Con -std=c++1y, posso fare questo:

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

La mia domanda è: quando dovrei usare questa funzione? Quando è necessario e quando rende il codice più pulito?

scenario 1

Il primo scenario che mi viene in mente è quando possibile. Ogni funzione che può essere scritta in questo modo dovrebbe essere. Il problema è che potrebbe non rendere sempre più leggibile il codice.

Scenario 2

Il prossimo scenario è quello di evitare tipi di ritorno più complessi. A titolo di esempio molto chiaro:

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

Non credo che sarebbe mai un problema, anche se immagino che il tipo di ritorno dipenda esplicitamente dai parametri potrebbe essere più chiaro in alcuni casi.

Scenario 3

Successivamente, per prevenire la ridondanza:

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

In C ++ 11, a volte possiamo solo return {5, 6, 7}; al posto di un vettore, ma ciò non sempre funziona e dobbiamo specificare il tipo sia nell'intestazione della funzione che nel corpo della funzione. Questo è puramente ridondante e la deduzione automatica del tipo di ritorno ci salva da quella ridondanza.

Scenario 4

Infine, può essere utilizzato al posto di funzioni molto semplici:

auto position() {
    return pos_;
}

auto area() {
    return length_ * width_;
}

A volte, tuttavia, potremmo guardare la funzione, volendo conoscere il tipo esatto, e se non viene fornito lì, dobbiamo andare in un altro punto del codice, come dove pos_è dichiarato.

Conclusione

Con questi scenari definiti, quale di essi si rivela effettivamente una situazione in cui questa funzionalità è utile per rendere il codice più pulito? Che dire degli scenari che ho trascurato di menzionare qui? Quali precauzioni dovrei prendere prima di utilizzare questa funzione in modo che non mi morda in seguito? C'è qualcosa di nuovo che questa funzionalità porta sul tavolo che non è possibile senza di essa?

Si noti che le domande multiple sono pensate per essere di aiuto nel trovare prospettive da cui rispondere.


18
Domanda meravigliosa! Mentre chiedi quali scenari rendono il codice "migliore", mi chiedo anche quali scenari lo peggioreranno .
Drew Dormann

2
@DrewDormann, è anche quello che mi chiedo. Mi piace fare uso di nuove funzionalità, ma sapere quando usarle e quando non farlo è molto importante. C'è un periodo di tempo in cui emergono nuove funzionalità che prendiamo per capirlo, quindi facciamolo ora in modo che siamo pronti per quando arriverà ufficialmente :)
chris

2
@NicolBolas, forse, ma il fatto che sia in una versione effettiva di un compilatore ora sarebbe sufficiente per le persone a iniziare a usarlo nel codice personale (a questo punto deve assolutamente essere tenuto lontano dai progetti). Sono una di quelle persone a cui piace usare le più recenti funzionalità possibili nel mio codice e, anche se non so quanto bene la proposta stia andando in commissione, immagino che sia la prima inclusa in questa nuova opzione qualcosa. Potrebbe essere meglio lasciarlo per dopo, o (non so quanto funzionerebbe) rianimato quando sappiamo per certo che sta arrivando.
chris,

1
@NicolBolas, se aiuta, è stato adottato ora: p
chris,

1
Le risposte attuali non sembrano menzionare il fatto che la sostituzione ->decltype(t+u)con la deduzione automatica uccide SFINAE.
Marc Glisse,

Risposte:


62

C ++ 11 solleva domande simili: quando usare la deduzione del tipo restituito in lambdas e quando usare le autovariabili.

La risposta tradizionale alla domanda in C e C ++ 03 è stata "oltre i confini delle istruzioni che rendiamo espliciti i tipi, all'interno delle espressioni sono generalmente impliciti ma possiamo renderli espliciti con i cast". C ++ 11 e C ++ 1 introducono strumenti di detrazione del tipo in modo da poter tralasciare il tipo in nuovi posti.

Ci dispiace, ma non hai intenzione di risolverlo in anticipo creando regole generali. Devi guardare un particolare codice e decidere tu stesso se aiutare la leggibilità a specificare i tipi in tutto il luogo: è meglio per il tuo codice dire "il tipo di questa cosa è X", oppure è meglio per il tuo codice per dire "il tipo di questa cosa è irrilevante per comprendere questa parte del codice: il compilatore deve sapere e probabilmente potremmo risolverlo ma non è necessario dirlo qui"?

Poiché la "leggibilità" non è definita oggettivamente [*] e inoltre varia in base al lettore, l'utente ha la responsabilità di autore / editore di un pezzo di codice che non può essere completamente soddisfatto da una guida di stile. Anche nella misura in cui una guida di stile specifica norme, persone diverse preferiranno norme diverse e tenderanno a trovare qualcosa di non familiare da "meno leggibile". Quindi la leggibilità di una particolare regola di stile proposta può spesso essere giudicata solo nel contesto delle altre regole di stile in atto.

Tutti i tuoi scenari (anche il primo) troveranno utilizzo per lo stile di programmazione di qualcuno. Personalmente trovo che il secondo sia il caso d'uso più convincente, ma anche così prevedo che dipenderà dai vostri strumenti di documentazione. Non è molto utile vedere documentato che il tipo di ritorno di un modello di funzione è auto, mentre vederlo documentato come decltype(t+u)crea un'interfaccia pubblicata su cui si può (si spera) fare affidamento.

[*] Occasionalmente qualcuno cerca di fare alcune misurazioni oggettive. Nella misura in cui qualcuno ottiene mai risultati statisticamente significativi e generalmente applicabili, vengono completamente ignorati dai programmatori che lavorano, a favore dell'istinto dell'autore di ciò che è "leggibile".


1
Un buon punto con le connessioni ad lambdas (anche se questo consente corpi funzionali più complessi). La cosa principale è che si tratta di una funzionalità più recente e sto ancora cercando di bilanciare i pro ei contro di ogni caso d'uso. Per fare ciò, è utile vedere i motivi alla base del perché sarebbe usato per quello in modo che io stesso possa scoprire perché preferisco quello che faccio. Potrei pensarci troppo, ma è così che sono.
chris,

1
@chris: come ho detto, penso che tutto si riduce alla stessa cosa. È meglio per il tuo codice dire "il tipo di questa cosa è X", o è meglio per il tuo codice dire "il tipo di questa cosa è irrilevante, il compilatore deve sapere e probabilmente potremmo risolverlo ma non dobbiamo dirlo ". Quando scriviamo 1.0 + 27Uaffermiamo il secondo, quando scriviamo (double)1.0 + (double)27Uaffermiamo il primo. La semplicità della funzione, il grado di duplicazione, l'evitamento decltypepotrebbero tutti contribuire a ciò, ma nessuno sarà affidabile in modo decisivo.
Steve Jessop,

1
È meglio per il tuo codice dire "il tipo di questa cosa è X", o è meglio per il tuo codice dire "il tipo di questa cosa è irrilevante, il compilatore deve sapere e probabilmente potremmo risolverlo ma non dobbiamo dirlo ". - Quella frase è esattamente sulla falsariga di ciò che sto cercando. Lo prenderò in considerazione quando mi imbatterò in opzioni di utilizzo di questa funzione, e autoin generale.
chris

1
Vorrei aggiungere che gli IDE potrebbero alleviare il "problema di leggibilità". Ad esempio, Visual Studio: se si passa con il mouse sulla autoparola chiave, verrà visualizzato il tipo di ritorno effettivo.
andreee

1
@andreee: è vero entro certi limiti. Se un tipo ha molti alias, conoscere il tipo effettivo non è sempre utile come speravi. Ad esempio un tipo di iteratore potrebbe essere int*, ma ciò che è effettivamente significativo, semmai, è che il motivo int*è perché è quello che std::vector<int>::iterator_typeè con le tue attuali opzioni di costruzione!
Steve Jessop,

30

In generale, il tipo restituito dalla funzione è di grande aiuto per documentare una funzione. L'utente saprà cosa ci si aspetta. Tuttavia, c'è un caso in cui penso che potrebbe essere bello eliminare quel tipo di ritorno per evitare la ridondanza. Ecco un esempio:

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

Questo esempio è tratto dal documento ufficiale del comitato N3493 . Lo scopo della funzione applyè di inoltrare gli elementi di a std::tuplea una funzione e restituire il risultato. I int_seqe make_int_seqfanno solo parte dell'implementazione e probabilmente confonderanno solo gli utenti che provano a capire cosa fa.

Come puoi vedere, il tipo restituito non è altro che una decltypedelle espressioni restituite. Inoltre, apply_non essendo pensato per essere visto dagli utenti, non sono sicuro dell'utilità di documentare il suo tipo di ritorno quando è più o meno lo stesso di applyquello. Penso che, in questo caso particolare, l'eliminazione del tipo restituito renda la funzione più leggibile. Si noti che questo tipo di restituzione è stato effettivamente eliminato e sostituito dalla decltype(auto)proposta per aggiungere applyallo standard, N3915 (si noti inoltre che la mia risposta originale precede questo documento):

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

Tuttavia, il più delle volte, è meglio mantenere quel tipo di ritorno. Nel caso particolare che ho descritto sopra, il tipo restituito è piuttosto illeggibile e un potenziale utente non otterrà nulla dalla sua conoscenza. Una buona documentazione con esempi sarà molto più utile.


Un'altra cosa che non è stata ancora menzionata: mentre declype(t+u)consente di utilizzare l' espressione SFINAE , decltype(auto)non (anche se esiste una proposta per modificare questo comportamento). Prendiamo ad esempio una foobarfunzione che chiamerà la foofunzione membro di un tipo se esiste o chiamerà la barfunzione membro del tipo se esiste, e supponiamo che una classe abbia sempre esattezza fooo barnessuna delle due contemporaneamente:

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

La chiamata foobarsu un'istanza di Xverrà visualizzata foomentre la chiamata foobarsu un'istanza di Yverrà visualizzata bar. Se si utilizza invece la detrazione automatica del tipo restituito (con o senza decltype(auto)), non si otterrà l'espressione SFINAE e la chiamata foobara un'istanza di uno Xo Ygenererà un errore di compilazione.


8

Non è mai necessario Quanto a quando dovresti ... avrai molte risposte diverse a riguardo. Direi di no fino a quando non è effettivamente una parte accettata dello standard e ben supportata dalla maggior parte dei principali compilatori allo stesso modo.

Oltre a ciò, sarà una discussione religiosa. Personalmente direi che non inserire mai il tipo di reso effettivo rende il codice più chiaro, è molto più facile per la manutenzione (posso guardare la firma di una funzione e sapere cosa restituisce rispetto a dover effettivamente leggere il codice) e rimuove la possibilità che pensi che dovrebbe restituire un tipo e il compilatore pensa che un altro causi problemi (come è successo con ogni linguaggio di scripting che abbia mai usato). Penso che l'auto sia stato un errore gigantesco e causerà ordini di grandezza più dolore che aiuto. Altri diranno che dovresti usarlo tutto il tempo, poiché si adatta alla loro filosofia di programmazione. Ad ogni modo, questo è fuori dal campo di applicazione di questo sito.


1
Sono d'accordo sul fatto che le persone con background diversi avranno opinioni diverse su questo, ma spero che questo giunga a una conclusione del C ++. In (quasi) ogni caso, è ingiustificabile abusare delle funzionalità del linguaggio per provare a trasformarlo in un altro, come usare #defines per trasformare C ++ in VB. Le caratteristiche generalmente hanno un buon consenso all'interno della mentalità della lingua su cosa sia un uso corretto e cosa no, secondo ciò a cui sono abituati i programmatori di quella lingua. La stessa funzionalità può essere presente in più lingue, ma ognuna ha le proprie linee guida sull'uso di essa.
chris

2
Alcune funzionalità hanno un buon consenso. Molti non lo fanno. Conosco molti programmatori che pensano che la maggior parte di Boost sia spazzatura che non dovrebbe essere usata. Conosco anche alcuni che pensano che sia la cosa più grande che accada al C ++. In entrambi i casi penso che ci siano alcune interessanti discussioni da fare, ma è davvero un esempio esatto dell'opzione stretta come non costruttiva su questo sito.
Gabe Sechan,

2
No, non sono completamente d'accordo con te e penso che autosia pura benedizione. Rimuove molta ridondanza. A volte è semplicemente un dolore ripetere il tipo di ritorno. Se si desidera restituire un lambda, può anche essere impossibile senza memorizzare il risultato in un std::function, che può comportare un certo sovraccarico.
Yongwei Wu,

1
C'è almeno una situazione in cui è quasi del tutto necessaria. Supponiamo di dover chiamare le funzioni, quindi registrare i risultati prima di restituirli e che le funzioni non hanno necessariamente lo stesso tipo restituito. Se lo fai tramite una funzione di registrazione che accetta le funzioni e i loro argomenti come parametri, dovrai dichiarare il tipo di ritorno della funzione di registrazione in automodo che corrisponda sempre al tipo di ritorno della funzione passata.
Justin Time - Ripristina Monica

1
[Esiste tecnicamente un altro modo per farlo, ma usa la magia del modello per fare sostanzialmente la stessa cosa, e ha ancora bisogno della funzione di registrazione per essere autoil tipo di ritorno finale.]
Justin Time - Ripristina Monica

7

Non ha nulla a che fare con la semplicità della funzione (come suppone un duplicato ora eliminato di questa domanda).

Il tipo restituito è fisso (non utilizzare auto) o dipendente in modo complesso da un parametro modello (utilizzare autonella maggior parte dei casi, associato a decltypequando ci sono più punti di restituzione).


3

Considera un ambiente di produzione reale: molte funzioni e unit test sono tutti interdipendenti dal tipo di ritorno foo() . Supponiamo ora che il tipo restituito debba cambiare per qualsiasi motivo.

Se il tipo di restituzione è autoovunque e vengono utilizzati i chiamanti foo()e le relative funzioniauto quando ottengono il valore restituito, le modifiche che devono essere apportate sono minime. In caso contrario, ciò potrebbe significare ore di lavoro estremamente noioso e soggetto a errori.

Come esempio nel mondo reale, mi è stato chiesto di cambiare un modulo dall'uso di puntatori non elaborati ovunque a puntatori intelligenti. Riparare i test unitari era più doloroso del codice reale.

Mentre ci sono altri modi in cui questo può essere gestito, l'uso dei autotipi restituiti sembra una buona scelta.


1
In quei casi, non sarebbe meglio scrivere decltype(foo())?
Oren S,

Personalmente, sento typedefche le dichiarazioni alias (vale a dire la usingdichiarazione di tipo) sono migliori in questi casi, specialmente quando sono di classe.
MAChitgarha,

3

Voglio fornire un esempio in cui il tipo di ritorno auto è perfetto:

Immagina di voler creare un breve alias per una lunga chiamata di funzione successiva. Con auto non è necessario occuparsi del tipo di restituzione originale (forse cambierà in futuro) e l'utente può fare clic sulla funzione originale per ottenere il tipo di restituzione reale:

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

PS: dipende da questa domanda.


2

Per lo scenario 3 cambierei il tipo restituito della firma della funzione con la variabile locale da restituire. Ciò renderebbe più chiaro per i programmatori client che la funzione restituisce. Come questo:

Scenario 3 Per prevenire la ridondanza:

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

Sì, non ha parole chiave automatiche, ma il principale è lo stesso per prevenire la ridondanza e offrire ai programmatori che non hanno accesso alla fonte un momento più semplice.


2
IMO la soluzione migliore per questo è fornire un nome specifico del dominio per il concetto di qualunque cosa si intenda rappresentare come un vector<map<pair<int,double>,int>e quindi usarlo.
davidbak,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.