Breve história do C++ e as novidades da versão 17

C++

C++ foi projetado por Bjarne Stroustrup enquanto trabalhava para a AT&T Bell Labs, que o empacotou e comercializou. Versões iniciais da linguagem foram disponibilizadas internamente na AT&T a partir de 1981. O C++ evoluiu de forma constante em resposta ao feedback do usuário.

A primeira edição do livro de Stroustrup, The C++ Programming Language, foi publicada no início de 1986. Após o lançamento da versão 2.0 em 1989, o C++ foi rapidamente reconhecido como uma linguagem séria e útil. O trabalho começou naquele ano para estabelecer um padrão de linguagem internacionalmente reconhecido para isso. Em 1997, um comitê do American National Standards Institute (ANSI) completou e publicou internamente o Draft Standard – A Linguagem C++, X3J16/97-14882, Conselho de Tecnologia da Informação (NSITC), Washington, DC.

Em junho de 1998, o projeto de norma foi aceito por unanimidade pelos representantes das 20 principais nações que participaram do esforço de nove anos da ANSI/ISO (International Standards Organization). A terceira edição do livro de Stroustrup, [Stroustrup97], foi publicada em 1997. Hohe é amplamente considerada como a referência definitiva em C++.

O trabalho contínuo para refinar o padrão está sendo feito pela ISO com a Comissão Eletrotécnica Internacional (IEC), um órgão internacional de avaliação de padrões e conformidade para todos os campos da eletrotecnologia. Em 2005, um relatório técnico, também conhecido como “tr1” foi publicado, contendo muitas extensões para a linguagem C++ e biblioteca padrão. Em 2010, o grupo de trabalho de padronização internacional em C++ foi denominado ISO/IEC JTC1/SC22/WG21. Em 2011 foi lançada a versão C++ 11 também chamada de C++0x. Nesta versão um grande número de alterações foi introduzido para padronizar as práticas existentes e melhorar as abstrações disponíveis para os programadores de C++.

Em 2012 foi fundada a The Standard C++ Foundation, organização sem fins lucrativos cuja finalidade é apoiar a comunidade de desenvolvedores de software C++ e promover a compreensão e o uso do Standard C++ moderno em todos os compiladores e plataformas.

Em 2014 é lançada a versão C++ 14 com pequenas melhorias. Em 2015 foi lançada a nova biblioteca do sistema de arquivos TS (ISO/IEC TS 18822:2015). A lib TS é uma extensão de biblioteca C++ experimental que especifica uma biblioteca de sistema de arquivos baseada em boost.filesystem V3 (com algumas modificações e extensões). Esta lib acabou sendo fundida na última versão C++ 17.

Desde o C++ 11, o WG21 (a designação ISO para o Comitê de Padrões C++) se concentrou no envio de um novo padrão a cada três anos. O padrão é composto de duas partes principais: o idioma principal e a Biblioteca de modelos padrão. Além de enviar o Padrão Internacional, o comitê produz Especificações Técnicas para recursos de linguagem principal ou biblioteca que ainda são experimentais para que a comunidade possa ganhar experiência de implementação e feedback do usuário antes de lançar os recursos no Padrão Internacional. Dentro deste cronograma de três anos, foi lançado o novo padrão C++ versão 17. Falaremos agora alguns dos novos recursos da linguagem.

Especificações de exceção

Até agora, o C++ teve duas maneiras diferentes de especificar se uma exceção escaparia ou não de uma função: especificações de exceção dinâmica e noexcept especificifiers.

void dynamic_exception_specifier_throwing() throw(std::exception);
void dynamic_exception_specifier_nonthrowing() throw()
void noexcept_specifier_throwing() noexcept(false);
void noexcept_specifier_nonthrowing() noexcept;

O uso de especificações de exceções dinâmicas foi preterido em C++ 11 quando nenhum especificador foi introduzido e as especificações de exceção dinâmica foram removidas em C++ 17. No entanto, a especificação de exceção dinâmica throw () ainda é permitida, apesar de ser preterida, como um alias para noexcept (true).

Uma mudança interessante nas especificações de exceção é que elas agora fazem parte do próprio tipo de função, o que significa que você pode sobrecarregar funções ou produzir especializações de modelos com base na especificação de exceção. Por exemplo:

void nonthrowing_func() noexcept {}
void throwing_func() noexcept(false) {}

void call_func_ptr(void (*fp)() noexcept) noexcept {
	fp();
}

template <typename Ty>
void call_func_ptr2(Ty) {}

template <>
void call_func_ptr2(void (*fp)() noexcept) {}

void f() {
	call_func_ptr(nonthrowing_func); // OK
	call_func_ptr(throwing_func); // Erro

	call_func_ptr2(nonthrowing_func); // chama template de especializacao
	call_func_ptr2(throwing_func); // chama template primario
}

Expressões Dobra

Às vezes, o código genérico faz uso de pacotes de parâmetros de modelo como substituto seguro de tipo para funções “variadic”. As Expressões Dobra permitem que se aplique o mesmo operador unário ou binário a todos os elementos de um pacote de parâmetros sem se dar ao trabalho de expandir manualmente o pacote. Por exemplo, se você estivesse escrevendo uma função genérica para somar todos os parâmetros (com a exigência de que os tipos envolvidos suportam o operador binário “+” e a função sum, portanto requerem um ou mais argumentos), você escreveria esse código manualmente em C++ 14 como:qp

template <typename T>
T sum(T v) {return v;}
template <typename T, typename... Args>
auto sum(T v, Args... args) {
	return v + sum(args...);
}

No entanto, com Expressões Dobra em C++ 17, esse código pode ser reduzido para:

template <typename... T>
auto sum(T... args) {
	return (args + ...);
}

As expressões de dobra vêm em dois tipos: dobras unárias (como mostrado acima) e dobras binárias, com a distinção sendo o número de argumentos para a expressão de dobra. Dobras binárias podem ser úteis em circunstâncias em que você deseja controlar o elemento inicial na dobra, como neste exemplo em que queremos transmitir todos os argumentos para o fluxo de saída padrão:

#include <iostream>
template <typename... T>
void f(T... args) {
	(std::cout << ... << args) << std::endl;
}

Guias de dedução explícita

Falando de modelos, é familiar a idéia de que uma função pode ter seus argumentos de modelo deduzidos de uma chamada de função sem requerer que a chamada de função especifique explicitamente os argumentos do modelo. Por exemplo, veja este código:

template <typename T>
void f(T);

int main() {
	f(12);
}

No entanto, o mesmo não é tradicionalmente verdadeiro de modelos de classe porque você não pode usar o mesmo truque de dedução de tipo com construtores de classe porque não há nenhuma maneira de distinguir os tipos em um construtor de modelo dos tipos na classe. Considere este construtor aceitando um par de iteradores, como pode ser visto em uma classe de contêiner:

template <typename T>
class C {
	public:
	template <typename I>
	C(I begin, I end) {}
};

int main() {
	int i[] = { 1, 2, 3, 4, 5 };
	C c(i, i + 5); // Erro
	C<int> c(i, i + 5); // OK
}

Em C++ 17, agora você pode adicionar guias de dedução explícitos a um construtor de classe para permitir a dedução dos tipos de modelo de classe.

#include <iterator>

template <typename T>
class C {
  public:
    template <typename I>
    C(I begin, I end) {}
};

template <typename I>
C(I begin, 
I end) -> C<typenamestd::iterator_traits<I>::value_type>;

int main() {
    int i[] = { 1, 2, 3, 4, 5 };
    C c(i, i + 5);
}

O guia de dedução tem a mesma forma sintática de um tipo de retorno no construtor, que especifica o tipo de classe concreta a deduzir. O guia de dedução não pode ser escrito em uma definição de construtor nem pode ser embutido na classe na declaração do construtor. O guia de dedução fora da linha informa ao compilador que uma chamada para esse construtor resulta em um tipo conforme especificado pelo tipo de retorno à direita, que deve ser uma especialização do modelo de classe principal.

A biblioteca de modelos padrão agora inclui guias de dedução de classes para muitos contêineres comuns. Por exemplo, agora você pode escrever std::pair p(42, ‘a’) para definir uma variável do tipo std::pair <int, char>.

Ligações estruturadas

Um desejo comum no código é retornar vários valores de uma função usando uma sintaxe elegante. Freqüentemente, isso significa usar um std::tuple<> ou estrutura para retornar as informações da função. No entanto, a maneira atual de dissociar os dados retornados de seu agregado nem sempre é elegante, pois exige que os tipos envolvidos suportem a construção padrão. Além disso, não há maneira automática de obter partes constituintes de um valor de estrutura. Considerar:

#include <tuple>

struct S {
	explicit S(int) {}  
};

struct T {
	int i;
	S s;
	double d; 
};

std::tuple<int, S, double> f() {
	return { 42, S(10), 1.2 };
}

T g() {
	return { 42, S(10), 1.2 };
}

int main() {
	int i1, i2;
	S s1, s2;
	double d1, d2;
	std::tie(i1, s1, d1) = f();
	std::tie(i2, s2, d2) = g(); 
}

Ligações estruturadas permitem que você decomponha dados agregados usando uma única declaração com dedução automática de tipo. A declaração única, anteriormente referida no padrão como a declaração de decomposição, usa colchetes para cercar as variáveis que se ligam aos membros agregados. Você pode usar ligações estruturadas para desacoplar estruturas, arrays ou classes semelhantes a tuplas com uma função get<>().

#include <tuple>

struct S {
	explicit S(int) {}
};

struct T {
	int i;
	S s;
	double d; 
};

std::tuple<int, S, double> f() {
	return { 42, S(10), 1.2 };
}

T g() {
	return { 42, S(10), 1.2 };
}

int main() {
	auto [i1, s1, d1] = f();
	auto [i2, s2, d2] = g();
}

Inicializadores if/switch

Há muito tempo o C++ suporta a capacidade de declarar uma variável na instrução init de uma instrução if, em que a variável declarada participa do teste de ramificação e pode ser usada pelo corpo da instrução if. No entanto, esse padrão quebra com usos mais complexos além das conversões booleanas implícitas. Isso leva à introdução de variáveis com um escopo mais amplo do que o necessário ou contorções para limitar o escopo da variável:

#include <mutex>
#include <vector>

void f(std::vector<int> &v, std::mutex &m) {
	{ // lock de escopo
	  std::lock_guard<std::mutex> lock(m);
	  if (v.empty())
		v.push_back(42);
	}
	// ...
}

O C++ 17 introduz a capacidade de if e switch declararem e inicializarem uma variável com escopo local, da mesma maneira que os loops sempre permitiram.

#include <mutex>
#include <vector>

void f(std::vector<int> &v, std::mutex &m) { 
	if (std::lock_guard<std::mutex> lock(m); v.empty())
		v.push_back(42);

	// ...
}

A variável declarada no inicializador da instrução de seleção é definida como escopo para a instrução de seleção, o que significa que esse trecho de código é funcionalmente equivalente ao anterior, sendo menor o código a ser gravado.

O C++ 17 é um ótimo passo evolucionário para o C++ e isso é apenas um conhecimento da nova funcionalidade presente com a última versão do padrão C++. Além dos recursos discutidos aqui, várias Especificações Técnicas foram publicadas, incluindo: Conceitos como um recurso de linguagem principal para permitir que os programadores restrinjam os parâmetros de modelo para metaprogramação de modelos claros, Coroutines como um recurso de linguagem principal para multithreading não preemptiva, Networking como um recurso de biblioteca para permitir comunicações entre aplicativos e Intervalos como uma re-imagem do STL onde os iteradores são emparelhados para formar um intervalo de valores para algoritmos e contêineres. À medida que o comitê ganha experiência com a adoção desses recursos, espera-se que sejam lançados no rascunho de trabalho para o próximo lançamento do C++, que é denominado C++ 2a.

Conclusão

A introdução de novos recursos pode simplificar a linguagem C++, e constexpr é um bom exemplo. Antes do constexpr, os valores constantes de computação envolviam a metaprogramação de gabaritos arcanos que desconcertava muitos usuários. O constexpr tornou esses cálculos drasticamente mais simples, estendendo os recursos de funções comuns em formas intuitivas, abrindo a computação em tempo de compilação para um público muito maior. Da mesma forma, os lambdas abriram linguagens de programação funcionais para um público muito maior, tornando os objetos funcionais mais convenientes e onipresentes.

O conceito TS não simplifica a programação genérica. A experiência com o GCC e a implementação de referência do Ranges TS levanta sérias preocupações sobre se eles podem melhorar a experiência de usar bibliotecas de modelos. The Ranges TS ilustra como a complexidade da linguagem vaza através dos conceitos e na experiência do usuário, e demonstra que os conceitos do mundo real não são simples ou fáceis de escrever ou usar.

Nos 14 anos desde que essa abordagem do conceito de suporte foi proposta pela primeira vez, o C++ evoluiu significativamente, com a introdução de constexpr, templates variáveis, constexpr-if, a expansão do SFINAE e a invenção de numerosas novas técnicas de template, que juntas fornecem o ferramentas que os desenvolvedores já estão usando para expressar as idéias de programação genérica. Qualquer recurso de idioma adicionado para dar suporte a esses idiomas deve melhorar significativamente os resultados que podemos obter hoje.

*** A OctalMind é uma empresa especializada no desenvolvimento de sistemas de alta tecnologia.