Quando devo usar a dedução automática de tipo de retorno do C ++ 14?

144

Com o GCC 4.8.0 lançado, temos um compilador que oferece suporte à dedução automática de tipo de retorno, parte do C ++ 14. Com -std=c++1y, eu posso fazer isso:

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

Minha pergunta é: quando devo usar esse recurso? Quando é necessário e quando torna o código mais limpo?

Cenário 1

O primeiro cenário em que consigo pensar é sempre que possível. Toda função que pode ser escrita dessa maneira deve ser. O problema é que nem sempre o código fica mais legível.

Cenário 2

O próximo cenário é evitar tipos de retorno mais complexos. Como um exemplo muito leve:

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

Eu não acredito que isso seja realmente um problema, embora eu ache que o tipo de retorno dependa explicitamente dos parâmetros possa ser mais claro em alguns casos.

Cenário 3

Em seguida, para evitar redundância:

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

No C ++ 11, às vezes podemos apenas return {5, 6, 7};substituir um vetor, mas isso nem sempre funciona e precisamos especificar o tipo no cabeçalho da função e no corpo da função. Isso é puramente redundante, e a dedução automática do tipo de retorno nos salva dessa redundância.

Cenário 4

Finalmente, ele pode ser usado no lugar de funções muito simples:

auto position() {
    return pos_;
}

auto area() {
    return length_ * width_;
}

Às vezes, porém, podemos olhar para a função, querendo saber o tipo exato, e se ela não for fornecida, precisamos ir para outro ponto do código, como onde pos_é declarado.

Conclusão

Com os cenários apresentados, quais deles realmente provam ser uma situação em que esse recurso é útil para tornar o código mais limpo? E os cenários que deixei de mencionar aqui? Que precauções devo tomar antes de usar este recurso para que ele não me morda mais tarde? Existe algo novo que esse recurso traga para a mesa que não seja possível sem ele?

Observe que as múltiplas perguntas devem ajudar a encontrar perspectivas para responder a isso.

chris
fonte
18
Pergunta maravilhosa! Enquanto você pergunta quais cenários tornam o código "melhor", também me pergunto quais cenários o tornarão pior .
Desenhou Dormann
2
@DrewDormann, é o que eu estou pensando também. Eu gosto de fazer uso de novos recursos, mas saber quando usá-los e quando não é muito importante. Há um período de tempo quando novos recursos surgir que tomamos para descobrir isso, então vamos fazê-lo agora para que nós estamos prontos para quando se trata oficialmente :)
chris
2
@ NicolBolas, talvez, mas o fato de estar em uma versão real de um compilador agora seria suficiente para as pessoas começarem a usá-lo em código pessoal (ele definitivamente deve ser mantido longe dos projetos neste momento). Sou uma dessas pessoas que gosta de usar os novos recursos possíveis no meu próprio código e, embora não saiba até que ponto a proposta está indo com o comitê, acho que é o primeiro incluído nesta nova opção. alguma coisa. Pode ser melhor deixar para mais tarde, ou (não sei como funcionaria) revivido quando sabemos com certeza que está chegando.
Chris1 /
1
@NicolBolas, se ajudar, foi adotado agora: p
chris
1
As respostas atuais não parecem mencionar que a substituição ->decltype(t+u)pela dedução automática mata o SFINAE.
Marc Glisse

Respostas:

62

O C ++ 11 levanta questões semelhantes: quando usar a dedução de tipo de retorno em lambdas e quando usar autovariáveis.

A resposta tradicional para a pergunta em C e C ++ 03 tem sido "através dos limites da declaração, tornamos os tipos explícitos, em expressões que geralmente são implícitas, mas podemos explicitá-las com moldes". C ++ 11 e C ++ 1y introduzem ferramentas de dedução de tipo para que você possa deixar de fora o tipo em novos locais.

Desculpe, mas você não vai resolver isso com antecedência, criando regras gerais. Você precisa examinar um código específico e decidir por si mesmo se ajuda ou não a legibilidade para especificar tipos em todo o lugar: é melhor para o seu código dizer "o tipo dessa coisa é X" ou é melhor para seu código para dizer: "o tipo disso é irrelevante para entender essa parte do código: o compilador precisa saber e provavelmente poderíamos resolver isso, mas não precisamos dizê-lo aqui"?

Como "legibilidade" não é objetivamente definida [*] e, além disso, varia de acordo com o leitor, você tem a responsabilidade como autor / editor de um trecho de código que não pode ser totalmente satisfeito por um guia de estilo. Mesmo na medida em que um guia de estilo especifica normas, pessoas diferentes preferem normas diferentes e tendem a achar que qualquer coisa estranha é "menos legível". Portanto, a legibilidade de uma regra de estilo proposta em particular geralmente pode ser julgada apenas no contexto das outras regras de estilo em vigor.

Todos os seus cenários (até o primeiro) encontrarão uso para o estilo de codificação de alguém. Pessoalmente, acho que o segundo é o caso de uso mais atraente, mas mesmo assim espero que isso dependa de suas ferramentas de documentação. Não é muito útil ver documentado que o tipo de retorno de um modelo de função é auto, enquanto vê-lo documentado como decltype(t+u)cria uma interface publicada na qual você pode (espero) confiar.

[*] Ocasionalmente, alguém tenta fazer algumas medidas objetivas. Na pequena extensão em que alguém obtém resultados estatisticamente significativos e geralmente aplicáveis, eles são completamente ignorados pelos programadores que trabalham, em favor dos instintos do autor quanto ao que é "legível".

Steve Jessop
fonte
1
Bom ponto com as conexões com lambdas (embora isso permita corpos funcionais mais complexos). O principal é que é um recurso mais recente, e ainda estou tentando equilibrar os prós e os contras de cada caso de uso. Para fazer isso, é útil ver as razões por que seria usado para quê, para que eu possa descobrir por que prefiro o que faço. Eu posso estar pensando demais, mas é assim que eu sou.
Chris1
1
@ Chris: como eu disse, acho que tudo se resume à mesma coisa. É melhor para o seu código dizer "o tipo dessa coisa é X" ou é melhor para o seu código dizer "o tipo dessa coisa é irrelevante, o compilador precisa saber e provavelmente poderíamos resolver isso mas não precisamos dizer ". Quando escrevemos 1.0 + 27U, estamos afirmando o último, quando escrevemos (double)1.0 + (double)27U, estamos afirmando o primeiro. A simplicidade da função, o grau de duplicação e a evasão decltypepodem contribuir para isso, mas nada será decisivamente confiável.
9788 Steve Jobs
1
É melhor para o seu código dizer "o tipo dessa coisa é X" ou é melhor para o seu código dizer "o tipo dessa coisa é irrelevante, o compilador precisa saber e provavelmente poderíamos resolver isso mas não precisamos dizer ". - Essa frase é exatamente na mesma linha do que estou procurando. Levarei isso em consideração ao encontrar opções de uso desse recurso e, autoem geral.
Chris1
1
Eu gostaria de acrescentar que os IDEs podem aliviar o "problema de legibilidade". Veja o Visual Studio, por exemplo: se você passar o mouse sobre a autopalavra-chave, ela exibirá o tipo de retorno real.
precisa saber é o seguinte
1
@andreee: isso é verdade dentro de limites. Se um tipo tem muitos aliases, saber o tipo real nem sempre é tão útil quanto você esperaria. Por exemplo, um tipo de iterador pode ser int*, mas o que é realmente significativo, se é que existe alguma coisa, é que o motivo int*é porque é isso que std::vector<int>::iterator_typeestá nas suas opções de compilação atuais!
21718 Steve Stoplin
30

De um modo geral, o tipo de retorno da função é de grande ajuda para documentar uma função. O usuário saberá o que é esperado. No entanto, há um caso em que acho que seria bom descartar esse tipo de retorno para evitar redundância. Aqui está um exemplo:

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

Este exemplo é retirado do documento oficial do comitê N3493 . O objetivo da função applyé encaminhar os elementos de a std::tuplepara uma função e retornar o resultado. O int_seqe make_int_seqsão apenas parte da implementação e provavelmente confundirão qualquer usuário tentando entender o que ele faz.

Como você pode ver, o tipo de retorno nada mais é do que uma decltypeexpressão retornada. Além disso, apply_não sendo feito para ser visto pelos usuários, não tenho certeza da utilidade de documentar seu tipo de retorno quando ele é mais ou menos o mesmo que applyo dele. Eu acho que, nesse caso em particular, largar o tipo de retorno torna a função mais legível. Observe que esse tipo de retorno foi realmente descartado e substituído decltype(auto)na proposta de adicionar applyao padrão N3915 (observe também que minha resposta original é anterior a este artigo):

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

No entanto, na maioria das vezes, é melhor manter esse tipo de retorno. No caso específico que descrevi acima, o tipo de retorno é bastante ilegível e um usuário em potencial não ganha nada ao saber disso. Uma boa documentação com exemplos será muito mais útil.


Outra coisa que ainda não foi mencionada: embora declype(t+u)permita usar a expressão SFINAE , decltype(auto)não (embora exista uma proposta para alterar esse comportamento). Tomemos, por exemplo, uma foobarfunção que chamará a foofunção membro de um tipo, se existir, ou chamará a barfunção membro de um tipo, se existir, e suponha que uma classe sempre tenha exatidão fooou barmas nenhuma das duas ao mesmo tempo:

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

A chamada foobarde uma instância de Xserá exibida fooenquanto a chamada foobarde uma instância de Yserá exibida bar. Se você usar a dedução automática do tipo de retorno (com ou sem decltype(auto)), não obterá a expressão SFINAE e invocará foobaruma instância de qualquer uma delas Xou Yacionará um erro em tempo de compilação.

Morwenn
fonte
8

Isso nunca é necessário. Quanto a quando você deveria, você receberá muitas respostas diferentes sobre isso. Eu diria que não, até que seja realmente uma parte aceita do padrão e bem suportada pela maioria dos principais compiladores da mesma maneira.

Além disso, será um argumento religioso. Pessoalmente, eu diria que nunca colocar o tipo de retorno real torna o código mais claro, é muito mais fácil para manutenção (posso olhar para a assinatura de uma função e saber o que ela retorna versus realmente ter que ler o código) e remove a possibilidade de que você acha que deve retornar um tipo e o compilador pensa que outro está causando problemas (como aconteceu com todas as linguagens de script que eu já usei). Eu acho que o automóvel foi um erro gigante e causará muito mais dor do que ajuda em ordens de magnitude. Outros dirão que você deve usá-lo o tempo todo, pois ele se encaixa na filosofia de programação deles. De qualquer forma, isso está fora do escopo deste site.

Gabe Sechan
fonte
1
Concordo que pessoas com diferentes origens terão opiniões diferentes sobre isso, mas espero que isso chegue a uma conclusão em C ++. Em (quase) todos os casos, é indesculpável abusar dos recursos de linguagem para tentar transformá-lo em outro, como usar #defines para transformar C ++ em VB. Os recursos geralmente têm um bom consenso na mentalidade da linguagem sobre o que é o uso adequado e o que não é, de acordo com o que os programadores da linguagem estão acostumados. O mesmo recurso pode estar presente em vários idiomas, mas cada um tem suas próprias diretrizes sobre o uso dele.
Chris
2
Alguns recursos têm um bom consenso. Muitos não. Conheço muitos programadores que pensam que a maioria do Boost é lixo que não deve ser usado. Também conheço alguns que acham que é a melhor coisa a acontecer com o C ++. Em ambos os casos, acho que há algumas discussões interessantes sobre ele, mas é realmente um exemplo exato da opção fechar como não construtiva neste site.
perfil completo de Gabe Sechan
2
Não, discordo completamente de você e acho que autoé pura bênção. Remove muita redundância. É simplesmente doloroso repetir o tipo de retorno algumas vezes. Se você deseja retornar um lambda, pode ser até impossível sem armazenar o resultado em um std::function, o que pode resultar em alguma sobrecarga.
Yongwei Wu
1
Há pelo menos uma situação em que tudo é totalmente necessário. Suponha que você precise chamar funções e, em seguida, registre os resultados antes de devolvê-los, e as funções nem sempre têm o mesmo tipo de retorno. Se você fizer isso por meio de uma função de log que aceita funções e seus argumentos como parâmetros, precisará declarar o tipo de retorno da função de log autopara que ele sempre corresponda ao tipo de retorno da função passada.
Justin Time - Restabelece Monica
1
[Existe tecnicamente outra maneira de fazer isso, mas ele usa a mágica do modelo para fazer exatamente a mesma coisa e ainda precisa da função de registro autopara o tipo de retorno à direita.]
Justin Time - Restabelece Monica
7

Não tem nada a ver com a simplicidade da função (como uma duplicata agora excluída desta pergunta deveria).

Ou o tipo de retorno é fixo (não use auto), ou dependente de uma forma complexa em um parâmetro do modelo (uso autona maioria dos casos, emparelhado com decltypequando há vários pontos de retorno).

Ben Voigt
fonte
3

Considere um ambiente de produção real: muitas funções e testes de unidade são todos interdependentes no tipo de retorno foo(). Agora, suponha que o tipo de retorno precise ser alterado por qualquer motivo.

Se o tipo de retorno estiver em autoqualquer lugar e os chamadores foo()e as funções relacionadas usarem autoao obter o valor retornado, as alterações que precisam ser feitas são mínimas. Caso contrário, isso pode significar horas de trabalho extremamente tedioso e propenso a erros.

Como exemplo do mundo real, fui convidado a mudar um módulo de usar ponteiros brutos em todos os lugares para ponteiros inteligentes. A correção dos testes de unidade foi mais dolorosa do que o código real.

Embora existam outras maneiras de lidar com isso, o uso de autotipos de retorno parece ser um bom ajuste.

Jim Wood
fonte
1
Nesses casos, não seria melhor escrever decltype(foo())?
precisa
Pessoalmente, sinto que typedefas declarações s e alias (ou seja, usingdeclaração de tipo) são melhores nesses casos, especialmente quando têm escopo de classe.
MAChitgarha
3

Quero fornecer um exemplo em que o tipo de retorno auto seja perfeito:

Imagine que você deseja criar um pequeno alias para uma longa chamada de função subsequente. Com o auto, você não precisa cuidar do tipo de retorno original (talvez isso mude no futuro) e o usuário pode clicar na função original para obter o tipo de retorno real:

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

PS: Depende dessa pergunta.

kaiser
fonte
2

Para o cenário 3, eu mudaria o tipo de retorno da assinatura da função com a variável local a ser retornada. Isso tornaria mais claro para os programadores clientes que a função retorna. Como isso:

Cenário 3 Para evitar redundância:

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

Sim, não possui palavra-chave automática, mas o principal é o mesmo para evitar redundância e facilitar aos programadores que não têm acesso à fonte.

Servir Laurijssen
fonte
2
Na IMO, a melhor solução é fornecer um nome específico de domínio para o conceito do que quer que seja representado como um vector<map<pair<int,double>,int>e usá-lo.
davidbak 11/06/19