Share via


Bem-vindo de volta ao C++ – C++ moderno

Desde sua criação, o C++ se tornou uma das linguagens de programação mais usadas do mundo. os programas bem escritos que a utilizam são rápidos e eficientes. Ele é mais flexível do que outras linguagens: pode funcionar nos níveis mais elevados de abstração e no nível do silício. O C++ fornece bibliotecas padrão altamente otimizadas. Ele permite o acesso a recursos de hardware de baixo nível, a fim de maximizar a velocidade e minimizar os requisitos de memória. O C++ pode criar quase qualquer tipo de programa: aplicativos de jogos, drivers de dispositivo, HPC, nuvem, área de trabalho, inseridos e móveis, entre muito outros. Até mesmo bibliotecas e compiladores para outras linguagens de programação são escritos em C++.

Um dos requisitos originais da C++ é a compatibilidade com as versões anteriores da linguagem C. Como resultado, o C++ sempre permitiu a programação no estilo C, com ponteiros brutos, matrizes, cadeias de caracteres terminadas em nulo e outros recursos. Isso possibilita um ótimo desempenho, mas também pode gerar bugs e complexidade. A evolução do C++ enfatizou recursos que reduzem consideravelmente a necessidade de usar expressões de estilo C. Os antigos recursos de programação em C ainda estão lá quando você precisa deles. No entanto, no código C++ moderno, você deve precisar deles cada vez menos. O código C++ moderno é mais simples, seguro e elegante e continua rápido como sempre.

As seções a seguir fornecem uma visão geral dos principais recursos do C++ moderno. A menos que seja observado o contrário, os recursos listados aqui estão disponíveis no C++11 e posteriores. No compilador de C++ da Microsoft, você pode definir a opção do compilador /std para especificar qual versão do padrão usar para o projeto.

Recursos e ponteiros inteligentes

Uma das principais classes de bugs na programação em estilo C é o vazamento de memória. Muitas vezes, os vazamentos são causados por uma falha ao chamar delete para memória alocada com new. O C++ moderno enfatiza que o princípio de RAII (a aquisição de recursos é a inicialização). A ideia é simples. Os recursos (memória de heap, identificadores de arquivo, soquetes e assim por diante) devem pertencer a um objeto. Esse objeto cria, ou recebe, o recurso recém-alocado no respectivo construtor e o exclui no destruidor. O princípio de RAII garante que todos os recursos serão retornados corretamente ao sistema operacional quando o objeto proprietário ficar fora do escopo.

Para dar suporte à adoção fácil dos princípios de RAII, a Biblioteca Padrão de C++ fornece três tipos de ponteiro inteligente: std::unique_ptr, std::shared_ptr e std::weak_ptr. Um ponteiro inteligente cuida da alocação e da exclusão da memória que possui. O exemplo a seguir mostra uma classe com um membro de matriz alocado no heap na chamada para make_unique(). As chamadas para new e delete são encapsuladas pela classe unique_ptr. Quando um objeto widget sair do escopo, o destruidor unique_ptr será invocado e liberará a memória alocada para a matriz.

#include <memory>
class widget
{
private:
    std::unique_ptr<int[]> data;
public:
    widget(const int size) { data = std::make_unique<int[]>(size); }
    void do_something() {}
};

void functionUsingWidget() {
    widget w(1000000);  // lifetime automatically tied to enclosing scope
                        // constructs w, including the w.data gadget member
    // ...
    w.do_something();
    // ...
} // automatic destruction and deallocation for w and w.data

Sempre que possível, use um ponteiro inteligente para gerenciar a memória do heap. Se você precisar usar os operadores new e delete explicitamente, siga o princípio de RAII. Para obter mais informações, consulte Gerenciamento de tempo de vida e recursos do objeto (RAII).

std::string e std::string_view

Cadeias de caracteres de estilo C são outra fonte considerável de bugs. Usando std::string e std::wstring, você pode eliminar praticamente todos os erros associados a cadeias de caracteres de estilo C. Você também obtém o benefício das funções membro para pesquisar, acrescentar, preceder e assim por diante. Ambos são altamente otimizados em termos de velocidade. Ao passar uma cadeia de caracteres para uma função que exige apenas acesso somente leitura, no C++17 você pode usar std::string_view para ter um benefício de desempenho ainda maior.

std::vector e outros contêineres da Biblioteca Padrão

Todos os contêineres de biblioteca padrão seguem o princípio de RAII. Eles fornecem iteradores para a passagem segura dos elementos. Também são altamente otimizados quanto ao desempenho e foram testados minuciosamente quanto à correção. Usando esses contêineres, você elimina o potencial de bugs ou ineficiências que podem ser introduzidos em estruturas de dados personalizadas. Em vez de matrizes brutas, use vector como um contêiner sequencial em C++.

vector<string> apples;
apples.push_back("Granny Smith");

Use map (não unordered_map) como o contêiner associativo padrão. Use set, multimap e multiset para casos degenerativos e múltiplos.

map<string, string> apple_color;
// ...
apple_color["Granny Smith"] = "Green";

Quando a otimização de desempenho for necessária, considere usar:

  • Contêineres associativos não ordenados, como unordered_map. Eles têm sobrecarga menor por elemento e pesquisa com tempo constante, mas podem ser mais difíceis de usar correta e eficientemente.
  • Classificado como vector. Para obter mais informações, consulte Algoritmos.

Não use matrizes de estilo C. Para APIs mais antigas que precisam de acesso direto aos dados, use métodos de acessador, como f(vec.data(), vec.size());. Para obter mais informações sobre contêineres, consulte Contêineres da Biblioteca Padrão de C++.

Algoritmos da Biblioteca Padrão

Antes de presumir que você precisa escrever um algoritmo personalizado para o programa, examine os algoritmos da Biblioteca Padrão de C++. Ela contém uma variedade crescente de algoritmos para muitas operações comuns, como pesquisa, classificação, filtragem e randomização. A biblioteca matemática é extensa. No C++17 e posteriores, são fornecidas versões paralelas de muitos algoritmos.

Veja alguns exemplos importantes:

  • for_each, o algoritmo de passagem padrão (juntamente com loops for baseados em intervalo).
  • transform, para modificação não in loco de elementos de contêiner
  • find_if, o algoritmo de pesquisa padrão.
  • sort, lower_bound e os outros algoritmos de classificação e pesquisa padrão.

Para escrever um comparador, use < estrito e lambdas nomeados quando puder.

auto comp = [](const widget& w1, const widget& w2)
     { return w1.weight() < w2.weight(); }

sort( v.begin(), v.end(), comp );

auto i = lower_bound( v.begin(), v.end(), widget{0}, comp );

auto em vez de nomes de tipo explícitos

O C++11 introduziu a palavra-chave auto para uso em declarações de variável, função e modelo. auto instrui o compilador a deduzir o tipo do objeto para que você não precise digitá-lo explicitamente. auto é especialmente útil quando o tipo deduzido é um modelo aninhado:

map<int,list<string>>::iterator i = m.begin(); // C-style
auto i = m.begin(); // modern C++

Loops for baseados em intervalo

A iteração de estilo C em matrizes e contêineres é propensa a erros de indexação e também é tediosa de digitar. Para eliminar esses erros e deixar o código mais legível, use loops for baseados em intervalo com contêineres da Biblioteca Padrão e matrizes brutas. Para obter mais informações, consulte Instrução for baseada em intervalo.

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v {1,2,3};

    // C-style
    for(int i = 0; i < v.size(); ++i)
    {
        std::cout << v[i];
    }

    // Modern C++:
    for(auto& num : v)
    {
        std::cout << num;
    }
}

Expressões constexpr em vez de macros

Macros em C e C++ são tokens processados pelo pré-processador antes da compilação. Cada instância de um token de macro é substituída pelo respectivo valor ou expressão definida antes que o arquivo seja compilado. As macros costumam ser usadas na programação de estilo C para definir valores constantes de tempo de compilação. No entanto, elas são propensas a erros e difíceis de depurar. No C++ moderno, dê preferência a variáveis constexpr para constantes de tempo de compilação:

#define SIZE 10 // C-style
constexpr int size = 10; // modern C++

Inicialização uniforme

No C++ moderno, você pode usar a inicialização com chaves para qualquer tipo. Essa forma de inicialização é especialmente conveniente ao inicializar matrizes, vetores ou outros contêineres. No exemplo a seguir, v2 é inicializado com três instâncias de S. v3 é inicializado com três instâncias de S que são inicializadas usando chaves. O compilador infere o tipo de cada elemento com base no tipo declarado de v3.

#include <vector>

struct S
{
    std::string name;
    float num;
    S(std::string s, float f) : name(s), num(f) {}
};

int main()
{
    // C-style initialization
    std::vector<S> v;
    S s1("Norah", 2.7);
    S s2("Frank", 3.5);
    S s3("Jeri", 85.9);

    v.push_back(s1);
    v.push_back(s2);
    v.push_back(s3);

    // Modern C++:
    std::vector<S> v2 {s1, s2, s3};

    // or...
    std::vector<S> v3{ {"Norah", 2.7}, {"Frank", 3.5}, {"Jeri", 85.9} };

}

Para obter mais informações, consulte Inicialização com chaves.

Semântica de transferência de recursos

O C++ moderno fornece semântica de transferência de recursos, que possibilita eliminar cópias de memória desnecessárias. Em versões anteriores da linguagem, as cópias eram inevitáveis em determinadas situações. Uma operação de transferência transfere a propriedade de um recurso de um objeto para outro sem fazer uma cópia. Algumas classes possuem recursos, como memória de heap, identificadores de arquivo e assim por diante. Ao implementar uma classe proprietária de recursos, você pode definir um construtor de transferência e um operador de atribuição de transferência para ele. O compilador escolhe esses membros especiais durante a resolução da sobrecarga em situações em que uma cópia não é necessária. Os tipos de contêiner da Biblioteca Padrão invocam o construtor de transferência em objetos quando um é definido. Para obter mais informações, consulte Construtores de transferência e operadores de atribuição de transferência (C++).

Expressões lambda

Na programação de estilo C, uma função pode ser passada para outra usando um ponteiro de função. Ponteiros de função são inconvenientes de manter e entender. A função à qual eles se referem pode ser definida em outro lugar no código-fonte, longe do ponto em que é invocada. Além disso, eles não são fortemente tipados. O C++ moderno fornece objetos de função, classes que substituem o operador operator(), que permite que sejam chamados como uma função. A maneira mais conveniente de criar objetos de função é com expressões lambda embutidas. O seguinte exemplo mostra como usar uma expressão lambda para passar um objeto de função, que a função find_if invocará em cada elemento no vetor:

    std::vector<int> v {1,2,3,4,5};
    int x = 2;
    int y = 4;
    auto result = find_if(begin(v), end(v), [=](int i) { return i > x && i < y; });

A expressão lambda [=](int i) { return i > x && i < y; } pode ser lida como "função que usa um só argumento do tipo int e retorna um booliano que indica se o argumento é maior que x e menor que y." Observe que as variáveis x e y do contexto ao redor podem ser usadas no lambda. O [=] especifica que essas variáveis são capturadas por valor, ou seja, a expressão lambda tem as próprias cópias desses valores.

Exceções

O C++ moderno enfatiza exceções, não códigos de erro, como a melhor maneira de relatar e lidar com condições de erro. Para obter mais informações, consulte Melhores práticas do C++ moderno para tratamento de erros e exceções.

std::atomic

Use o struct std::atomic da Biblioteca Padrão de C++ e tipos relacionados para mecanismos de comunicação entre threads.

std::variant (C++17)

As uniões costumam ser usadas na programação de estilo C para conservar a memória permitindo que membros de diferentes tipos ocupem o mesmo local de memória. No entanto, ela não são fortemente tipadas e são propensas a erros de programação. O C++17 introduz a classe std::variant como uma alternativa mais robusta e segura às uniões. A função std::visit pode ser usada para acessar os membros de um tipo variant de maneira fortemente tipada.

Confira também

Referência da linguagem C++
Expressões Lambda
Biblioteca Padrão do C++
Conformidade com a linguagem do Microsoft C/C++