No mundo da programação nós podemos classificar as linguagens de duas maneiras: Linguagem de Baixo Nível e Linguagem de Alto Nível. Quando nos referimos à linguagem de Baixo Nível estamos nos referindo a sintaxes próximas ao código de maquina, ou seja, a linguagem que o computador consegue facilmente interpretar. Em contrapartida a definição de linguagem de Baixo Nível nós encontramos as linguagem de Alto Nível, com relação a esse tipo de linguagem podemos afirmar que a mesma possui uma sintaxe próxima à linguagem humana um exemplo desse tipo de linguagem é o próprio C++.
Para ter uma ideia de como é desenvolver em C++, assista à aula do livecoder DenisJose12 sobre estruturas de dados em C++:
O C++ foi inicialmente desenvolvido por Bjarne Stroustrup dos Bell Labs durante a década de 1980 (originalmente com o nome C with Classes, como um adicional à linguagem C) com o objectivo de melhorar a linguagem de programação C ainda que mantendo máxima compatibilidade. Em 1983 o nome da linguagem foi alterado de C with Classes para C++, novas características foram adicionadas, como funções virtuais, sobrecarga de operadores e funções, referências, constantes, gerenciamento manual de memória, melhorias na verificação de tipo de dado e estilo de comentário de código de uma linha (//).
Para aprender mais sobre C++, confira este curso gratuito e com certificado sobre desenvolvimento orientado a objeto em C++ da fundação Bradesco que a CBSI separou.
Após a padronização ISO realizada em 1998 e a posterior revisão realizada em 2003, uma nova versão da especificação da linguagem foi lançada em setembro de 2011, conhecida informalmente como C++11 ou C++0x. Este novo padrão incluirá muitas adições ao núcleo da linguagem (sua implementação principal), e estenderá a biblioteca padrão do C++, incluindo a maior parte da biblioteca do chamado C++ Technical Report 1 — um documento que propõe mudanças ao C++ — com exceção das funções matemáticas específicas.
C++11 introduziu uma sintaxe alternativa para escrever declarações de função. Em vez de colocar o tipo de retorno antes do nome da função (por exemplo, int func () ), a nova sintaxe permite escrever após os parâmetros (por exemplo, auto func () -> int ). Isso leva a duas perguntas: Por que uma sintaxe alternativa foi adicionada? Ela vai substituir a sintaxe original do C++? Para ajudá-lo com essas perguntas, vamos resumir as vantagens e desvantagens desta sintaxe recém-adicionada.
Introdução
Desde o C++11, temos uma nova maneira de declarar funções. Esta sintaxe alternativa nos permite escrever a seguinte função:
1 2 3 4 5 6 7 |
// C or C++98 int f(int x, int y) { // ... } |
Como
1 2 3 4 5 6 7 |
// C++11 auto f(int x, int y) -> int { // ... } |
Basicamente, em vez de escrever o tipo de retorno antes do nome da função, nós colocamos apenas auto e especificamos o tipo de retorno após a lista de parâmetros. Uma vez que o tipo de retorno aparece no final da declaração, diz-se que a função tem um tipo de retorno à direita. Ambas as declarações acima são equivalentes, o que significa que elas significam exatamente o mesmo.
Nota: o uso do auto aqui é apenas parte da sintaxe e não executa dedução de tipo automática neste caso. A dedução automática foi adicionada no C++14, e teria o seguinte efeito se não fornecêssemos o tipo de retorno de retorno:
1 2 3 4 5 6 7 8 9 10 11 |
// C++14 auto f(int x, int y) { // The return type is deduced automatically // based on the function's body. // ... } |
Prós
Simplificação do Código Genérico
Considere a seguinte função, escrita usando a sintaxe alternativa:
1 2 3 4 5 6 7 8 9 |
// C++11 template<typename Lhs, typename Rhs> auto add(const Lhs& lhs, const Rhs& rhs) -> decltype(lhs + rhs) { return lhs + rhs; } |
A função tem dois parâmetros e retorna sua soma. Note que os parâmetros podem ter diferentes tipos, razão pela qual usamos dois parâmetros de modelo diferentes. Contanto que os tipos suportam binário +, eles podem ser usados como argumentos para add(). O especificador decltype nos dá o tipo da expressão lhs + rhs.
Vamos tentar reescrever a função usando a sintaxe padrão:
1 2 3 4 5 6 7 8 9 |
template<typename Lhs, typename Rhs> decltype(lhs + rhs) add(const Lhs& lhs, const Rhs& rhs) { // error: ^^^ 'lhs' and 'rhs' were not declared in this scope return lhs + rhs; } |
Oops! Uma vez que o compilador analisa o código-fonte da esquerda para a direita, o compilador verá lhs e rhs antes de suas definições e rejeita o código. Usando o tipo de retorno à direita, podemos contornar essa limitação.
Nota: a função acima pode ser escrita usando a sintaxe padrão com a ajuda de declval():
1 2 3 4 5 6 7 |
Template <typename Lhs, typename Rhs> Declty (std :: declval <Lhs> () + std :: declval <Rhs> ()) add (const Lhs & lhs, const Rhs & rhs) { Return lhs + rhs; } |
No entanto, como você pode ver, torna o código menos legível.
Eliminação de Repetição
Considere a seguinte classe:
1 2 3 4 5 6 7 |
class LongClassName { using IntVec = std::vector<int>; IntVec f(); }; |
Para definir f() usando a sintaxe padrão, temos que duplicar o nome da classe:
1 2 3 4 5 |
LongClassName::IntVec LongClassName::f() { // ... } |
O motivo é semelhante ao do exemplo anterior: O compilador analisa o código da esquerda para a direita, por isso, se o compilador viu IntVec, não soube onde procurá-lo porque o contexto (LongClassName) é dado após o tipo de retorno. Com a nova sintaxe, não há necessidade de repetir LongClassName:
1 2 3 4 5 |
auto LongClassName::f() -> IntVec { // ... } |
Consistência
Por último, mas certamente não menos importante, o uso uniforme da nova sintaxe pode levar a um código mais consistente. Por exemplo, quando você define uma expressão lambda, seu tipo de retorno pode ser especificado apenas como o tipo de retorno à direita:
1 |
[](int i) -> double { /* ... */ }; |
Não existe uma sintaxe de tipo de retorno “antiga” para expressões lambda, portanto, não é possível escrever o tipo de retorno no lado esquerdo.
De forma mais geral, como apontado por Herb Sutter, o mundo C ++ está indo para um estilo de declaração da esquerda para a direita em todos os lugares, na forma nome da categoria = tipo e/ou inicializador, em que a categoria pode ser auto ou using. Exemplos:
1 2 3 4 5 |
auto hello = "Hello"s; auto f(double) -> int; using dict = std::map<std::string, std::string>; |
Finalmente, uma propriedade um tanto agradável da nova sintaxe é que as declarações de funções agora estão ordenadamente alinhadas por seu nome:
1 2 3 4 5 |
auto vectorize() -> std::vector<int>; auto devour(Value value) -> void; auto get_random_value() -> Value; |
No entanto, o alinhamento de nomes de funções parece mais legível apenas quando as funções levam uma linha cada.
Contras
Omissão pode causar uma cópia a ser devolvida
No C++14, se você esquecer de especificar o tipo de retorno à direita, um tipo de retorno será deduzido automaticamente. Infelizmente, o tipo deduzido pode não ser o que você quer. Por exemplo, considere a seguinte definição padrão de um operador de atribuição:
1 2 3 4 |
auto MyClass::operator=(const MyClass& other) -> MyClass& { value = other.value; return *this; } |
Se você omitir o tipo de retorno à direita, o código será compilado, mas ele retornará um valor em vez de uma referência:
1 2 3 4 |
auto MyClass::operator=(const MyClass& other) { value = other.value; return *this; // Oops, returns a copy of MyClass! } |
Na verdade, a dedução de tipo automático via auto nunca deduz uma referência (se você quiser uma referência, use auto& em vez). Uma omissão descuidada pode assim mudar silenciosamente a semântica do seu código.
Pode produzir declarações mais longas
Às vezes, a nova sintaxe produz declarações mais longas:
1 2 3 |
int func(); // vs auto func() -> int; |
Posição inesperada com Override
Independente da experiência, já vimos pessoas atingidas por isso. Considere o seguinte código:
1 2 3 4 5 6 |
struct A { virtual int foo() const noexcept; }; struct B: A { virtual int foo() const noexcept override |
Algumas pessoas esperam que a declaração de B::foo() com a nova sintaxe seja assim:
1 2 |
virtual auto foo() const noexcept override -> int; // error: virtual function cannot have deduced return type |
Oops! A forma correta é:
1 |
virtual auto foo() const noexcept -> int override; |
Ou seja, override tem de ser especificado após o tipo de retorno à direita.
Consistência
Se você ainda se lembra da lista de prós, você deve se lembrar que a consistência era um dos prós. No entanto, isso só se aplica ao novo código. A maior parte do código existente foi escrito usando a sintaxe padrão. Assim, quando você começar a usar o novo estilo, seu estilo de codificação pode realmente se tornar inconsistente.
Não é um recurso amplamente conhecido
Em geral, os programadores não estão familiarizados com a nova sintaxe. Embora isso não seja uma razão contra a nova sintaxe, é algo deve ser lembrado. Para as pessoas que programam em C e C ++ por muito tempo, a nova sintaxe parece estranha. Então, pense duas vezes antes de escrever
1 |
auto main() -> int {} |
já que isso pode fazer que seus colegas de trabalho queiram bater-lhe com uma vara 🙂
Conclusão
A sintaxe alternativa foi adicionada para auxiliar a escrita de código genérico e para fornecer consistência. No entanto, devido às várias desvantagens listadas acima, a sintaxe original é usada mais amplamente do que a nova sintaxe. Até mesmo as diretrizes do C++ usam a sintaxe original.