Introdução ao C++11

c11 destaque

A linguagem de programação C++ foi inicialmente proposta em 1979 e padronizada em 1998 pelo International Organization for Standardization (ISO), sendo, desde então, uma das linguagens de programação mais populares no desenvolvimento das mais variadas soluções de software. Sua popularidade pode ser estimada com base no TIOBE Index, que, no presente momento, classifica o C++ como sendo a terceira linguagem de programação mais popular do mundo. Condicionada pelos avanços tecnológicos obtidos nas áreas de Microeletrônica e Compiladores, o C++ tem se mostrado cada vez mais aplicado no desenvolvimento de sistemas embarcados também.

Periodicamente, o comitê de padronização da linguagem reúne-se e, tendo em mente a retrocompatibilidade entre versões e a adição de funcionalidades, um novo standard é publicado. Atualmente, o último standard publicado é o C++14 e em breve deve ser lançado o C++17. Entretanto, entre os standards publicados até o momento, o C++11 é um dos que mais apresentaram melhorias significativas, adicionando novas funcionalidades que simplificam a tarefa de programação em C++, tornando nosso código mais expressivo e simples.

Neste contexto, o presente artigo tem como objetivo introduzir algumas das funcionalidades do C++11.

Ambiente e Compilação do C++11

Para escrever os códigos, irei utilizar o ambiente GNU/Linux com GCC 5.4.0. Por padrão, o GCC não compila o código com o standard C++11, sendo necessário habilitar este comportamento com o argumento -std=c++11. Por exemplo, para compilar o arquivo main.cpp com C++11, é necessário invocar o compilador segundo o comando exibido abaixo:

Esta invocação irá gerar o arquivo executável test a partir do código-fonte main.cpp considerando o standard C++11 em sua compilação.

Template Parser

Uma limitação conhecida no C++ era vista no trecho de código exibido a seguir, que faz uso do contêiner std::vector<std::vector<int>>.

Ao compilar, a mensagem de erro é:

Isto acontece pois o >> que fecha a expressão com o template está sendo interpretado como o operador >>. Este comportamento decorre de uma limitação no parser do C++, conhecida como Maximal Munch. A solução para este problema é simples, basta acrescentar um espaço entre cada >, mas tal construção não é a mais elegante e pode, como normalmente o faz, gerar dúvidas quanto a sua presença. O C++11 corrige esta limitação e ao compilar este código com o standard, a compilação é concluída com sucesso.

Type Detection

Em algumas circunstâncias, o tipo de uma variável é implícito a partir de sua atribuição, isto é, o compilador é capaz de inferir seu tipo mediante seu valor atribuído. Por exemplo, a atribuição da constante 10 em uma variável do tipo inteiro (int x = 10), pode ser redundante. Para eliminar esta redundância, o C++11 fornece um poderoso mecanismo de inferência de tipos com a extensão semântica da palavra-chave auto, até então utilizada como especificador de armazenamento, responsável por indicar ao compilador que a variável em questão deve ter seu ciclo de vida automático. Ao sair de seu escopo, a mesma deve ser desalocada, mas como este é o comportamento default de variáveis declaradas dentro de blocos, a palavra-chave auto torna-se opcional e raramente utilizada.

No C++11, auto ganha a semântica de, em alguns contextos, substituir o tipo de uma variável. Por exemplo, na atribuição auto x = 10, a informação do tipo da variável x é automaticamente inferida pelo compilador como sendo um inteiro a partir de sua atribuição.

Nem sempre a regra se aplica, mas entre os cenários aplicáveis, vale destacar a atribuição de iteradores em um laço de repetição. Este exemplo é ilustrado no código abaixo. Note a simplicidade da declaração do iterator, que antes do C++11 seria algo do tipo std::map<std::string, int>::iterator it = driversCounter.begin(). Mais verboso e menos expressivo, certo?

Range Based For

O típico idioma do C++ para percorrer um contêiner é com a combinação: for + iterator, exibida no trecho abaixo, para o caso de um std::vector<char>.

Algumas linguagens, como Java, C#, etc, dispõem de uma sintaxe mais enxuta para a tarefa, conhecida como for-each. Esta sintaxe passa a estar presente no C++11 e é exibida no código a seguir. Note que a combinação com a funcionalidade de type detection torna o laço menos verboso e a intenção mais clara.

Lambda Functions

Funções Lambda representam um importante conceito no paradigma da programação funcional, que preconiza que algoritmos devem ser compostos a partir da chamada de funções puras, isto é, semelhantes às funções matemáticas que não gerem efeitos colaterais após sua chamada, por exemplo, alterando permanentemente um de seus argumentos. Em resumo, na programação funcional não existe o conceito de estado de um programa, algo interessante, principalmente quando se fala em programação concorrente.

Na prática, uma função Lambda é, basicamente, uma função anônima, isto é, sem identificador. Sua implementação lembra o conceito de ponteiro para função, em que trafegamos um comportamento através de nosso código. Por exemplo, passando uma função (na verdade um ponteiro para uma função) para outra função e assim sucessivamente. A esta função que recebe uma outra função, dá-se o nome de função de alta ordem.

Com o C++11, o tráfego de funções ficou mais simples do que o habitual uso de ponteiro para função, ou até mesmo de uma função objeto (“functors”). Por exemplo, o código bloco abaixo é responsável por calcular a temperatura média de um conjunto de leituras.

O código com a mesma finalidade, mas que faz uso da função lambda é apresentado a seguir, em que a sintaxe [&sum] (double measure) { sum += measure; } define uma função lambda que captura uma referência à variável sum para possibilitar sua alteração dentro da função. O parâmetro double measure representa o valor obtido a partir do iterator acessado pela função std::for_each a ser passado para cada iteração do laço à função. Uma função lambda que faz referência, isto é, captura, variáveis externas a sua definição (por exemplo, a variável sum que declarada fora do bloco, escopo, da função lambda) é denominada closure.

A princípio, a sintaxe associada a uma função lambda pode parecer estranha e complicada, mas com o tempo, adequamos-nos a ela e notamos o potencial de se trabalhar com elas. Favorecendo mais o pensamento em termos de funções, que são unidades elementares, independentes e sem efeitos externos, do que em termos de estado do programa que necessita ser mantido com cautela (por exemplo: variáveis globais).

Constant Expression

Entre as premissas do C++11, está condicionar melhorias de performance em códigos escritos na linguagem, mas sem prejudicar a legibilidade e a segurança do código. Neste contexto, um conceito introduzido na especificação é o de constant expression, realizado pela nova palavra-chave constexpr. Mas antes de descrever o significado de constexpr, vamos recordar o uso da palavra-chave const.

O modificador de armazenamento const serve para informar ao compilador que a variável em questão não será modificada após sua atribuição, entre as vantagens de seu uso, destaca-se a passagem de parâmetros entre funções.

Ao passar uma estrutura como argumento de uma função, é feita a cópia de cada um de seus membros para o parâmetro da função invocada, isto gera desperdício de memória e tempo de processamento, para contornar este comportamento, ao invés de passar uma cópia da struct (ou class), passamos um ponteiro (ou uma referência) da mesma. Entretanto, ao fazer isso, a função que executa a chamada não tem garantia alguma de que a função invocada não irá, conscientemente ou não, modificar algum dos campos da estrutura e aqui entra um típico uso de const, em que, ao modificar o parâmetro struct* myStruct (ou struct& myStruct) para const struct* myStruct (ou const struct& myStruct). O mesmo conceito aplica-se para classes, trocando somente struct por class.

No exemplo acima, temos que const representa uma garantia de que, uma vez que o valor é atribuído, ele não será modificado.

Agora que falamos um pouco sobre const, vamos abordar o constexpr, que serve ao propósito de indicar ao compilador que o valor de uma expressão pode ser avaliado em tempo de compilação, ao invés de delegar ao tempo de execução, como tipicamente o é. Mas o que isso quer dizer? Qual é a vantagem disso? Performance! Até o C++11, o resultado atingido por constexpr era emulado pelo uso de macros de pré-processamento, mas seu uso tem certos problemas, principalmente: sacríficio de type-checking, além da sintaxe que pode obusfucar seu propósito.

A ideia básica é a seguinte: se uma determinada computação pode ser realizada em tempo de compilação (e assim, na plataforma de desenvolvimento, uma só vez antes do deploy) ao invés de ser realizada em tempo de execução (e assim na máquina plataforma de produção, eventualmente múltiplas vezes), é preferível optar pela primeira opção.

Um exemplo de código com constexpr é exibido na Listagem 8. Neste caso, a função heavyComputation simula alguma função que realiza cálculos lentos que poderiam poupar a CPU da plataforma alvo se forem realizados em tempo de compilação na plataforma de desenvolvimento. Uma aplicação típica a geração de tabelas de lookup para resultados de cálculos elaborados etc.

O uso de constexpr em funções não é permitido de maneira livre e algumas regras devem ser respeitadas:

  • O corpo da função deve conter somente uma única declaração, que deve ser de retorno
  • Pode invocar transitivamente outras funções também modificadas por constexpr
  • Pode referenciar apenas variáveis globais que sejam também modificadas por constexpr

A limitação imposta pela primeira regra pode ser contornada com a combinação do operador ternário e recursão, por exemplo,  a Listagem 9 calcula a soma de todos os inteiros no intervalo [begin, end] com apenas uma declaração, para o exemplo, [3,7] que resulta em 25.

Scoped Enum

Um problema conhecido para desenvolvedores C/C++ é a poluição de escopo causada por membros de enum, conhecidos como enumerators. Isto pois, os enumeratores de um tipo enum são visíveis ao escopo que circunda a enum. Para ilustrar esta questão, ao tentar compilar o código da Listagem 9, temos o seguinte erro:

main.cpp:10:2: error: redeclaration of ‘SPI’

 SPI

 ^

main.cpp:5:7: note: previous declaration ‘MemoryProtocol SPI

 I2C, SPI

Isto acontece pois os enumerators são visíveis (possuem escopo) fora da declaração do enum. Desta forma, o enumerator spi declarado em MemoryProtocol, é interpretado como sendo duplicado pelo enumerator spi de  SensorProtocol, pois ambos têm o mesmo nome e são visíveis fora de seus respectivos enums.

A primeira abordagem para corrigir o problema seria prefixar o nome de cada enumerator com o nome do enum, por exemplo: MemoryProtocolSPI etc, mas isto tornaria o código consideravelmente mais verboso, menos expressivo e além disso, o problema é apenas mitigado, mas não corrigido, pois nada impede de que no futuro outro enum seja criado e contenha o enumerator SPI (ou algum outro). Outra abordagem seria utilizar namespaces, mas esta solução poderia ser demasiadamente exigente para o problema, pois não necessariamente pode fazer sentido criar um namespace completo para, somente, distinguir entre dois enums.

C++11 corrige este problema de uma forma bem interessante com a introdução de scoped enums, que possibilitam criar enums que limitam o escopo de seus enumerators. Esta construção é realizada com a adição da palavra-chave class entre o tipo enum e seu nome. A Listagem 9 reescreve o código da Listagem 8, mas utilizando scoped enum e o problema original é resolvido.

Note na atribuição que, como agora os enumerators não pertencem mais ao escopo global, é necessário utilizar o operator :: (scope resolution) para informar ao compilador qual é o enumerator que estamos nos referindo.

Outra modificação introduzida pelo uso de scoped enums é a tipagem forte de seus enumerators, que até então eram implicitamente convertidos para int e agora este comportamento não ocorre mais. Sendo assim, atribuições como int protocolValue = MemoryProtocol::I2C ou int protocolValue =I2C, não são mais permitidas.

Conclusões

C++11 traz uma série de novidades que aumentam a expressividade com a linguagem, simplificando o processo de codificação e, com efeito, nossa produtividade. Entretanto, para fazer seu uso, é necessário verificar se o compilador que você utiliza em seu projeto suporta as features do standard. Para tal, vale a pena conferir o manual do compilador. Caso exista o suporte do compilador, recomendo experimentar estas features e explorar as tantas outras que foram incorporadas, por exemplo: base enum, multithreading, move semantics etc . Para tal, na Seção de Referências, indico alguns lugares onde pode começar os estudos.

Bons códigos!

Referências

Website | Veja + conteúdo

Software Engineer interested in C++, Scala, Go, C, Python, Haskell, Linux, declarative style, library design, cloud computing, embedded systems, tooling, etc.

I have a bachelor in Information Engineering and other in Science and Technology, both by UFABC. Therefore my academic background is more closely related to Electrical Engineering and DSP, but I've being professionally working as Software Engineer during my whole career.

Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.

Receba os melhores conteúdos sobre sistemas eletrônicos embarcados, dicas, tutoriais e promoções.

Comentários:
Talvez você goste:

Séries

Menu