O auto torna o código C ++ mais difícil de entender?

119

Eu vi uma conferência por Herb Sutter, onde ele encoraja cada programador C ++ a usar auto .

Eu tive que ler o código C # há algum tempo, onde var foi extensivamente usado e o código era muito difícil de entender - toda vez que var era usado, eu tinha que verificar o tipo de retorno do lado direito. Às vezes mais de uma vez, porque esqueci o tipo da variável depois de um tempo!

Eu sei que o compilador conhece o tipo e não preciso escrevê-lo, mas é amplamente aceito que devemos escrever código para programadores, não para compiladores.

Eu também sei que é mais fácil escrever:

auto x = GetX();

Do que:

someWeirdTemplate<someOtherVeryLongNameType, ...>::someOtherLongType x = GetX();

Mas isso é gravado apenas uma vez e o tipo de retorno GetX() é verificado várias vezes para entender o tipo de x .

Isso me fez pensar - será que auto torna o código C ++ mais difícil de entender?

    
por Felics 20.12.2012 / 15:01
fonte

14 respostas

92

Resposta curta: mais completamente, minha opinião atual sobre auto é que você deve usar auto por padrão, a menos que você queira explicitamente uma conversão. (Um pouco mais precisamente, "... a menos que você queira explicitamente se comprometer com um tipo, o que quase sempre é porque você quer uma conversão.")

Resposta e justificativa mais longas:

Escreva um tipo explícito (em vez de auto ) apenas quando você realmente quiser se comprometer explicitamente com um tipo, o que quase sempre significa que você deseja obter explicitamente uma conversão para esse tipo. No topo da minha cabeça, lembro-me de dois casos principais:

  • (Comum) A surpresa initializer_list que auto x = { 1 }; deduz initializer_list . Se você não quiser initializer_list , diga o tipo, ou seja, solicite explicitamente uma conversão.
  • (Rare) O caso de modelos de expressão, como auto x = matrix1 * matrix 2 + matrix3; , captura um tipo auxiliar ou de proxy não destinado a ser visível para o programador. Em muitos casos, é bom e benigno capturar esse tipo, mas às vezes, se você realmente quiser que ele colapse e faça o cálculo, diga o tipo - ou seja, solicite explicitamente uma conversão.

Use rotineiramente auto por padrão, porque usar auto evita armadilhas e torna seu código mais correto, mais sustentável, robusto e mais eficiente. Aproximadamente na ordem do mais para o menos importante, no espírito de "escrever para clareza e correção primeiro":

  • Exatidão: Usar auto garante que você terá o tipo certo. Como diz o ditado, se você se repetir (diga o tipo de forma redundante), você pode e irá mentir (errar). Aqui está um exemplo usual: void f( const vector<int>& v ) { for( /*…* - neste momento, se você escrever o tipo do iterador explicitamente, você quer se lembrar de escrever const_iterator (não é?), Enquanto auto apenas acerta.
  • Mantenabilidade e robustez: Usar auto torna seu código mais robusto em relação a alterações, porque quando o tipo da expressão é alterado, auto continuará a ser resolvido para o tipo correto. Se você, em vez disso, se comprometer com um tipo explícito, a alteração do tipo da expressão injetará conversões silenciosas quando o novo tipo for convertido para o tipo antigo ou intervalos de compilação desnecessários quando o novo tipo continuar funcionando como o tipo antigo, mas não convertido para o antigo tipo (por exemplo, quando você altera um map para um unordered_map , o que é sempre bom se você não estiver confiando no pedido, usando auto para seus iteradores, você alternará facilmente de map<>::iterator para unordered_map<>::iterator , mas usar map<>::iterator em todos os lugares explicitamente significa que você estará desperdiçando seu valioso tempo em uma ondulação mecânica de código, a menos que um estagiário esteja passando e você possa impingir o trabalho chato sobre eles).
  • Desempenho: Como auto garante que nenhuma conversão implícita ocorrerá, ela garante melhor desempenho por padrão. Se, em vez disso, você disser o tipo e exigir uma conversão, muitas vezes você receberá uma conversão silenciosamente, independentemente do que esperava ou não.
  • Usabilidade: Usar auto é sua única boa opção para tipos difíceis de serem soletrados e inutilizáveis, como lambdas e ajudantes de modelo, além de recorrer a expressões decltype repetitivas ou indiretas menos eficientes, como std::function .
  • Conveniência: E, sim, auto é menos digitação. Eu mencionei isso por completo porque é um motivo comum para gostar, mas não é o maior motivo para usá-lo.

Por isso: Prefiro dizer auto por padrão. Ele oferece tanta simplicidade, desempenho e clareza que você só está se machucando (e os futuros mantenedores do seu código) se você não o fizer. Somente se comprometa com um tipo explícito quando você realmente quiser, o que quase sempre significa que você quer uma conversão explícita.

Sim, existe (agora) um GotW sobre isso.

    
por 25.12.2012 / 21:06
fonte
111

É uma situação caso a caso.

Às vezes, torna o código mais difícil de entender, às vezes não. Tome, por exemplo:

void foo(const std::map<int, std::string>& x)
{
   for ( auto it = x.begin() ; it != x.end() ; it++ )
   { 
       //....
   }
}

é definitivamente fácil de entender e definitivamente mais fácil de escrever do que a declaração real do iterador.

Estou usando o C ++ já há algum tempo, mas posso garantir que recebo um erro do compilador na minha primeira tentativa, porque eu esqueceria o const_iterator e inicialmente iria para o iterator ...:)

Eu o usaria para casos como esse, mas não onde ele realmente ofusca o tipo (como a sua situação), mas isso é puramente subjetivo.

    
por 20.12.2012 / 15:03
fonte
94

Olhe de outra maneira. Você escreve:

std::cout << (foo() + bar()) << "\n";

ou:

// it is important to know the types of these values
int f = foo();
size_t b = bar();
size_t total = f + b;

std::cout << total << "\n";

Às vezes, não ajuda a soletrar explicitamente o tipo.

A decisão sobre a necessidade de mencionar o tipo não é a mesma que decidir se você deseja dividir o código em várias instruções, definindo variáveis intermediárias. Em C ++ 03 os dois estavam ligados, você pode pensar em auto como uma maneira de separá-los.

Às vezes, tornar os tipos explícitos pode ser útil:

// seems legit    
if (foo() < bar()) { ... }

vs.

// ah, there's something tricky going on here, a mixed comparison
if ((unsigned int)foo() < bar()) { ... }

Nos casos em que você declara uma variável, o uso de auto permite que o tipo não seja expresso, como acontece em muitas expressões. Você provavelmente deve tentar decidir por si mesmo quando isso ajuda na legibilidade e quando isso atrapalha.

Você pode argumentar que misturar tipos assinados e não assinados é um erro para começar (na verdade, alguns argumentam que não se deve usar tipos não assinados). A razão que é sem dúvida um erro é que torna os tipos de operandos vitalmente importantes por causa do comportamento diferente. Se é uma coisa ruim precisar saber os tipos de seus valores, provavelmente também não é uma coisa ruim não precisar conhecê-los. Assim, desde que o código já não seja confuso por outras razões, isso torna auto OK, certo? ; -)

Particularmente, quando se escreve código genérico, há casos em que o tipo real de uma variável não deve ser importante, o que importa é que satisfaça a interface necessária. Então auto fornece um nível de abstração onde você ignora o tipo (mas é claro que o compilador não sabe). Trabalhar em um nível adequado de abstração pode ajudar muito a capacidade de leitura. Trabalhar no nível "errado" torna a leitura do código uma tarefa difícil.

    
por 20.12.2012 / 15:08
fonte
26

IMO, você está olhando para isso praticamente ao contrário.

Não é uma questão de auto levar a códigos ilegíveis ou até menos legíveis. É uma questão de (esperar que) ter um tipo explícito para o valor de retorno compensará o fato de que (aparentemente) não está claro qual tipo seria retornado por alguma função específica.

Pelo menos na minha opinião, se você tem uma função cujo tipo de retorno não é imediatamente óbvio, esse é o seu problema. O que a função faz deve ser óbvia a partir de seu nome, e o tipo do valor de retorno deve ser óbvio a partir do que ela faz. Se não, essa é a verdadeira fonte do problema.

Se houver um problema aqui, não será com auto . É com o resto do código, e é bem provável que o tipo explícito seja apenas um band-aid suficiente para evitar que você veja e / ou conserte o problema central. Depois de corrigir esse problema real, a legibilidade do código usando auto geralmente ficará bem.

Suponho que, para ser justo, devo acrescentar: lidei com alguns casos em que essas coisas não eram tão óbvias quanto você gostaria, e corrigir o problema também era bastante insustentável. Apenas por exemplo, fiz algumas consultorias para uma empresa há alguns anos que já havia se fundido a outra empresa. Eles acabaram com uma base de código que foi mais "empurrada juntos" do que realmente mesclada. Os programas constituintes tinham começado usando bibliotecas diferentes (mas bem similares) para propósitos similares, e embora eles estivessem trabalhando para fundir as coisas de maneira mais limpa, eles ainda faziam isso. Em um bom número de casos, a única maneira de adivinhar qual tipo seria retornado por uma determinada função era saber onde essa função havia se originado.

Mesmo nesse caso, você pode ajudar a esclarecer algumas coisas. Nesse caso, todo o código começou no namespace global. Simplesmente mover uma quantia justa para alguns namespaces eliminou os confrontos de nome e facilitou bastante o rastreamento de tipos.

    
por 21.12.2012 / 06:51
fonte
14

Sim, fica mais fácil saber o tipo de sua variável se você não usa auto . A questão é: você precisa para saber o tipo de sua variável para ler o código? Às vezes a resposta será sim, às vezes não. Por exemplo, ao obter um iterador de std::vector<int> , você precisa saber que é std::vector<int>::iterator ou auto iterator = ...; suficiente? Tudo o que qualquer um gostaria de fazer com um iterador é dado pelo fato de ser um iterador - não importa qual seja o tipo especificamente.

Use auto nas situações em que isso não dificulta a leitura do seu código.

    
por 20.12.2012 / 15:05
fonte
14

Existem várias razões pelas quais eu não gosto de auto para uso geral:

  1. Você pode refatorar o código sem modificá-lo. Sim, essa é uma das coisas frequentemente listadas como benefício de usar o modo automático. Basta alterar o tipo de retorno de uma função e, se todo o código que a chama usar auto, nenhum esforço adicional será necessário! Você compila, ele cria - 0 avisos, 0 erros - e você simplesmente prossegue e verifica seu código sem ter que lidar com a confusão de olhar e potencialmente modificar os 80 lugares em que a função é usada.

Mas espere, isso é realmente uma boa ideia? E se o tipo importasse em meia dúzia desses casos de uso, e agora esse código realmente se comporta de maneira diferente? Isso também pode implicitamente quebrar o encapsulamento, modificando não apenas os valores de entrada, mas o próprio comportamento da implementação privada de outras classes que chamam a função.

1a. Acredito no conceito de "código de auto-documentação". O raciocínio por trás do código de autodocumentação é que os comentários tendem a se tornar desatualizados, não refletindo mais o que o código está fazendo, enquanto o código em si - se escrito de maneira explícita - é auto-explicativo, sempre atualizado em sua intenção, e não vai deixar você confuso com comentários obsoletos. Se os tipos podem ser alterados sem a necessidade de modificar o próprio código, o próprio código / variáveis pode ficar obsoleto. Por exemplo:

auto bThreadOK = CheckThreadHealth ();

Exceto o problema é que CheckThreadHealth () em algum ponto foi refatorado para retornar um valor de enum indicando o status de erro, se houver algum, em vez de um bool. Mas a pessoa que fez essa mudança não detectou essa linha de código em particular, e o compilador não ajudou em nada porque compilou sem avisos ou erros.

  1. Você pode nunca saber quais são os tipos reais. Isso também é frequentemente listado como um "benefício" primário de auto. Por que aprender o que uma função está lhe dando, quando você pode simplesmente dizer: "Quem se importa? Ela compila!"

Até funciona, provavelmente. Eu digo tipo de trabalho, porque mesmo que você esteja fazendo uma cópia de uma estrutura de 500 bytes para cada iteração de loop, para que você possa inspecionar um único valor nele, o código ainda estará completamente funcional. Assim, até mesmo os testes de unidade não ajudam você a perceber que o código ruim está se escondendo atrás desse automóvel simples e inocente. A maioria das outras pessoas que examinam o arquivo não o notará à primeira vista.

Isso também pode ser pior se você não souber qual é o tipo, mas você escolhe um nome de variável que faz uma suposição errada sobre o que é, na verdade alcançando o mesmo resultado que em 1a, mas a partir do próprio começando em vez de pós-refatorar.

  1. Digitar o código ao escrever inicialmente não é a parte mais demorada da programação. Sim, o auto faz a gravação de alguns códigos mais rapidamente inicialmente. Como aviso legal, eu digito > 100 WPM, então talvez isso não me incomode tanto quanto os outros. Mas se tudo que eu tivesse que fazer fosse escrever um novo código o dia todo, eu seria um feliz campista. A parte mais demorada da programação é diagnosticar bugs difíceis de reproduzir no código, muitas vezes resultantes de problemas sutis e não óbvios - como é provável que o uso excessivo do auto (referência versus cópia, assinado vs. não assinado, float vs. int, bool vs. ponteiro, etc.).

Parece óbvio para mim que o auto foi introduzido principalmente como uma solução alternativa para uma sintaxe terrível com os tipos de modelo de biblioteca padrão. Em vez de tentar consertar a sintaxe do modelo com a qual as pessoas já estão familiarizadas - o que também pode ser quase impossível de ser feito por causa de todo o código existente que ele poderia quebrar - adicione uma palavra-chave que basicamente oculta o problema. Essencialmente, o que você pode chamar de "hack".

Na verdade, não tenho nenhum desacordo com o uso do auto com contêineres de biblioteca padrão. Obviamente, é para isso que a palavra-chave foi criada e as funções na biblioteca padrão provavelmente não mudarão fundamentalmente de propósito (ou tipo, para essa matéria), tornando o auto relativamente seguro de usar. Mas eu seria muito cauteloso em usá-lo com seu próprio código e interfaces que podem ser muito mais voláteis e potencialmente sujeitos a mudanças mais fundamentais.

Outra aplicação útil de auto que aumenta a capacidade da linguagem é criar temporárias em macros agnósticas. Isso é algo que você realmente não poderia fazer antes, mas você pode fazer isso agora.

    
por 25.07.2015 / 03:09
fonte
12

Pessoalmente, eu uso auto apenas quando é absolutamente óbvio para o programador o que é.

Exemplo 1

std::map <KeyClass, ValueClass> m;
// ...
auto I = m.find (something); // OK, find returns an iterator, everyone knows that

Exemplo 2

MyClass myObj;
auto ret = myObj.FindRecord (something)// NOT OK, everyone needs to go and check what FindRecord returns
    
por 20.12.2012 / 15:09
fonte
10

Esta questão solicita opiniões, que variam de programador para programador, mas eu diria que não. De fato, em muitos casos, exatamente o oposto, auto pode ajudar a tornar o código mais fácil de entender, permitindo que o programador se concentre na lógica em vez das minúcias.

Isso é especialmente verdadeiro em face de tipos complexos de modelos. Aqui está um & simplificado & exemplo inventado. Qual é mais fácil de entender?

for( std::map<std::pair<Foo,Bar>, std::pair<Baz, Bot>, std::less<BazBot>>::const_iterator it = things_.begin(); it != things_.end(); ++it )

.. ou ...

for( auto it = things_.begin(); it != things_.end(); ++it )

Alguns diriam que o segundo é mais fácil de entender, outros podem dizer o primeiro. Ainda outros podem dizer que um uso gratuito de auto pode contribuir para um emburrecimento dos programadores que o usam, mas isso é outra história.

    
por 20.12.2012 / 15:06
fonte
8

Muitas boas respostas até agora, mas para focar na pergunta original, acho que Herb vai longe demais em seu conselho de usar auto liberalmente. Seu exemplo é um caso em que usar auto obviamente prejudica a legibilidade. Algumas pessoas insistem que não é um problema com os IDEs modernos, onde você pode passar o mouse sobre uma variável e ver o tipo, mas eu discordo: mesmo as pessoas que sempre usam uma IDE às vezes precisam ver snippets de código isoladamente (pense em revisões de código , por exemplo) e um IDE não ajudará.

Linha de fundo: use auto quando ajudar: ou seja, iteradores em loops for. Não use quando o leitor tiver dificuldades para descobrir o tipo.

    
por 21.12.2012 / 14:52
fonte
5

Estou bastante surpreso que ninguém tenha apontado ainda que o auto ajuda se não houver um tipo claro. Nesse caso, você pode contornar esse problema usando um #define ou um typedef em um modelo para localizar o tipo utilizável real (e isso às vezes não é trivial) ou você apenas usa o auto.

Suponha que você tenha uma função que retorne algo com um tipo específico de plataforma:

#ifdef PLATFROM1
__int256 getStuff();
#else //PLATFORM2
__int128 getStuff();
#endif

Uso de bruxa você preferiria?

#ifdef PLATFORM1
__int256 stuff = getStuff();
#else
__int128 stuff = getStuff();
#endif

ou simplesmente

auto stuff = getStuff();

Claro, você pode escrever

#define StuffType (...)

bem em algum lugar, mas

StuffType stuff = getStuff();

diz mais alguma coisa sobre o tipo x? Ele diz que é o que é retornado de lá, mas é exatamente o que é auto. Isso é apenas redundante - 'coisas' são escritas 3 vezes aqui - isso, na minha opinião, torna menos legível que a versão 'auto'.

    
por 27.12.2012 / 12:38
fonte
3

A legibilidade é subjetiva; você precisará analisar a situação e decidir o que é melhor.

Como você apontou, sem auto, declarações longas podem produzir muita confusão. Mas, como você também apontou, as declarações curtas podem remover informações de tipo que podem ser valiosas.

Além disso, adicionaria também isso: verifique se você está atento à legibilidade e não à capacidade de gravação. Código que é fácil de escrever geralmente não é fácil de ler e vice-versa. Por exemplo, se eu estivesse escrevendo, preferiria auto. Se eu estivesse lendo, talvez as declarações mais longas.

Depois, há coerência; quão importante é isso para você? Você desejaria auto em algumas partes e declarações explícitas em outros, ou um método consistente em todo o processo?

    
por 20.12.2012 / 15:02
fonte
2

Eu considero o código de menor legibilidade como uma vantagem e encorajarei o programador a usá-lo cada vez mais. Por quê? Claramente, se o código usando auto é difícil de ler, então será difícil escrever também. O programador é forçado a usar o nome da variável significativa , para melhorar o seu trabalho.
Talvez no começo o programador não possa escrever os nomes das variáveis significativas. Mas eventualmente enquanto corrige os bugs, ou na revisão de código, quando ele / ela tem que explicar o código para os outros, ou em um futuro não tão próximo, explicando o código para as pessoas de manutenção, o programador perceberá o erro e usará o nome da variável significativa no futuro.

    
por 21.12.2012 / 07:44
fonte
2

Eu tenho duas diretrizes:

  • Se o tipo da variável for óbvio, tedioso para escrever ou difícil de determine o uso automático.

    auto range = 10.0f; // Obvious
    
    for (auto i = collection.cbegin(); i != cbegin(); ++i) // Tedious if collection type
    // is really long
    
    template <typename T> ... T t; auto result = t.get(); // Hard to determine as get()
    // might return various stuff
    
  • Se você precisar de conversão específica ou o tipo de resultado não for óbvio e causar confusão.

    class B : A {}; A* foo = new B(); // 'Convert'
    
    class Factory { public: int foo(); float bar(); }; int f = foo(); // Not obvious
    
por 15.01.2013 / 10:33
fonte
1

Sim. Diminui a verbosidade, mas o mal-entendido comum é que a verbosidade diminui a legibilidade. Isso só é verdadeiro se você considerar a legibilidade como estética, em vez de sua capacidade real de interpretar o código - o que não é aumentado com o uso automático. No exemplo mais comumente citado, iteradores vetoriais, pode parecer na superfície que o uso automático aumenta a legibilidade do seu código. Por outro lado, nem sempre você sabe o que a palavra-chave automática lhe dará. Você precisa seguir o mesmo caminho lógico que o compilador faz com a reconstrução interna e, na maior parte do tempo, em particular com os iteradores, você fará as suposições erradas.

No final do dia, 'auto' sacrifica a legibilidade do código e clareza, para a 'limpeza' sintática e estética (que é necessária apenas porque os iteradores têm uma sintaxe desnecessariamente contorcida) e a capacidade de talvez digitar menos 10 caracteres em qualquer dado linha. Não vale a pena o risco ou o esforço envolvido a longo prazo.

    
por 30.10.2014 / 02:50
fonte

Tags