1.5 ABCg

Para facilitar o desenvolvimento das atividades práticas utilizaremos a biblioteca ABCg desenvolvida especialmente para esta disciplina.

ABCg permite a prototipagem rápida de aplicações gráficas interativas 3D em C++ capazes de rodar tanto no desktop com binário nativo, quanto no navegador com binário WebAssembly.

Internamente a ABCg utiliza a biblioteca SDL para gerenciar o acesso a dispositivos de entrada (mouse/teclado/gamepad) e saída (vídeo e áudio) de forma independente de plataforma, e a biblioteca GLEW para acesso às funções da API gráfica OpenGL. Além disso, a API do Emscripten é utilizada sempre que a aplicação é compilada para WebAssembly.

ABCg é mais propriamente um framework do que uma biblioteca, pois ela gerencia o fluxo de trabalho da aplicação. Nosso código deverá ser construído em torno da estrutura fornecida pela ABCg. Do ponto de vista do desenvolvedor, essa estrutura é apenas uma leve camada de abstração das APIs utilizadas. Por exemplo, é possível acessar diretamente as funções da API gráfica OpenGL. De fato, faremos isso na maior parte das vezes. Outras bibliotecas também utilizadas e que podem ser acessadas diretamente são:

  • CPPIterTools: para o suporte a laços range-based em C++ usando funções do tipo range, enumerate e zip similares às da linguagem Python;
  • Dear ImGui: para gerenciamento de widgets de interface gráfica do usuário, tais como janelas, botões e caixas de edição;
  • {fmt}: como alternativa mais eficiente ao stdio da linguagem C (printf, scanf, etc) e iostreams do C++ (std::cout, std::cin, etc), e para formatação de strings com uma sintaxe similar às f-strings do Python;
  • Guidelines Support Library (GSL): para uso de funções e tipos de dados recomendados pelo C++ Core Guidelines;
  • OpenGL Mathematics (GLM): para suporte a operações de transformação geométrica com vetores e matrizes;
  • tinyobjloader: para a leitura de modelos 3D no formato Wavefront OBJ.

A seguir veremos como instalar e compilar a ABCg junto com um exemplo de uso.

Instalação

Em um terminal, clone o repositório do GitHub:

git clone https://github.com/hbatagelo/abcg.git
Observação

A release mais recente da ABCg também pode ser baixada como um arquivo compactado de https://github.com/hbatagelo/abcg/releases/latest.

Atenção

No Windows, certifique-se de clonar/descompactar o repositório em um diretório cujo nome não contenha espaços ou caracteres especiais. Por exemplo, clone/descompacte em C:\cg em vez de C:\computação gráfica.

O repositório tem a estrutura mostrada a seguir. Para simplificar, os arquivos e subdiretórios .git* foram omitidos:

abcg
│   .clang-format
│   .clang-tidy
│   build.bat
│   build.sh
|   build-vs.bat
│   build-wasm.bat
│   build-wasm.sh
|   CHANGELOG.md
│   CMakeLists.txt
|   DockerFile
│   LICENSE
│   README.md
│   runweb.bat
│   runweb.sh
│
└───abcg
│   │   ...
│
└───cmake
│   │   ...
│   
└───examples
│   │   ...
│
└───public
    │   ...

Os arquivos .clang-format e .clang-tidy são arquivos de configuração utilizados pelas ferramentas ClangFormat e Clang-Tidy caso estejam instaladas.

Os arquivos build.* são scripts de compilação em linha de comando. Note que há scripts correspondentes com extensão .bat para usar no Prompt de Comando do Windows 2:

  • build.sh/build.bat: para compilar a ABCg e os exemplos em binários nativos usando o compilador padrão (no nosso caso, o GCC);
  • build-wasm.sh/build-wasm.bat: similar ao build.sh, mas para gerar binário em WebAssembly dentro do subdiretório public;
  • build-vs.bat: similar ao build.bat, mas usando o compilador do Visual Studio 2022 ao invés do GCC do MSYS2.

O arquivo CMakeLists.txt é o script de compilação do CMake.

Os arquivos runweb.sh e runweb.bat podem ser usados para criar um servidor web local para servir o conteúdo de public.

Os subdiretórios são os seguintes:

  • abcg contém o código-fonte da ABCg e suas dependências.
  • cmake contém scripts auxiliares de configuração do CMake.
  • examples contém um exemplo de uso da ABCg: o “Hello, World!”;
  • public contém os códigos HTML para exibir o “Hello, World!” no navegador.
Observação

O “Hello, World!” pode usar tanto a API gráfica OpenGL (código-fonte em examples/opengl) quanto a API gráfica Vulkan (código-fonte em examples/vulkan).

Nesta disciplina usaremos o OpenGL em todas as atividades. Em particular, usaremos o OpenGL 3.3. Assim poderemos possível construir aplicações para desktop e web usando o mesmo código-fonte. Por este motivo, os scripts CMake da ABCg estão configurados para usar o OpenGL por padrão.

Compilando na linha de comando

Execute o script build.sh (Linux/macOS) ou build.bat (Windows) para iniciar o processo de configuração e construção da versão OpenGL do “Hello, World!”. A saída será similar a esta (o exemplo a seguir mostra a saída no Ubuntu):

-- The C compiler identification is GNU 11.2.0
-- The CXX compiler identification is GNU 11.2.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/lib/ccache/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/lib/ccache/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
Using ccache
-- Found OpenGL: /usr/lib/x86_64-linux-gnu/libOpenGL.so   
-- Found GLEW: /usr/include (found version "2.2.0") 
-- Looking for pthread.h
-- Looking for pthread.h - found
-- Performing Test CMAKE_HAVE_LIBC_PTHREAD
-- Performing Test CMAKE_HAVE_LIBC_PTHREAD - Success
-- Found Threads: TRUE  
-- Found SDL2: /usr/lib/x86_64-linux-gnu/libSDL2main.a;/usr/lib/x86_64-linux-gnu/libSDL2.so  
-- Found SDL2_image: /usr/lib/x86_64-linux-gnu/libSDL2_image.so  
-- Configuring done
-- Generating done
-- Build files have been written to: /home/ufabc/abcg/build
...
[100%] Linking CXX executable ../../bin/helloworld
[100%] Built target helloworld

Ao final, os binários estarão disponíveis no subdiretório build. A biblioteca estática estará em build/abcg/libabcg.a e o executável do exemplo “Hello, World!” estará em build/bin/helloworld.

Para testar, execute o helloworld. No Linux/macOS:

./build/bin/helloworld/helloworld

No Windows:

.\build\bin\helloworld\helloworld.exe | cat
Importante

No Windows, a saída deve sempre ser redirecionada para cat ou tee. Se isso não for feito, nenhuma saída de texto será exibida no terminal. Isso se deve a um bug do MSYS2.

Observação

Observe o conteúdo de build.sh (build.bat contém instruções equivalentes):

#!/bin/bash
set -euo pipefail

BUILD_TYPE=${1:-Release}
CMAKE_EXTRA_ARGS=${2:-""}

# Reset build directory
rm -rf build
mkdir -p build && cd build

# Configure
cmake -DCMAKE_BUILD_TYPE="$BUILD_TYPE" "$CMAKE_EXTRA_ARGS" ..

# Build
if [[ "$OSTYPE" == "darwin"* ]]; then
  # macOS
  NUM_PROCESSORS="$(sysctl -n hw.ncpu)"
else
  NUM_PROCESSORS="$(nproc)"
fi
cmake --build . --config "$BUILD_TYPE" -- -j "$NUM_PROCESSORS"
  • A variável BUILD_TYPE é Release por padrão, mas pode ser modificada passando a string Debug, MinSizeRel ou RelWithDebInfo como primeiro argumento de build.sh. A opção Debug ou RelWithDebInfo é utilizada quando queremos gerar símbolos de depuração. O modo Release (padrão) ou MinSizeRel devem ser usados quando queremos gerar um binário otimizado e sem arquivos de símbolos de depuração. Em particular, Release otimiza em favor do código mais rápido, enquanto MinSizeRel otimiza em favor do binário de menor tamanho.

  • build.sh/build.bat também aceita um segundo argumento que corresponde a uma string a ser passada como argumento do CMake durante a configuração do projeto. Por exemplo, para compilar no modo Release e com a API gráfica OpenGL podemos executar build.sh Release -DGRAPHICS_API=OpenGL (neste caso basta executar build.sh, pois essas já são as configurações padrão).

  • Observe que o script apaga o subdiretório build antes de criá-lo novamente. Portanto, não salve arquivos dentro de build pois eles serão removidos na próxima compilação!

  • A geração dos binários usando o CMake é composta de duas etapas: a configuração (cmake -DCMAKE_BUILD_TYPE=$BUILD_TYPE ..) e a construção (cmake --build . --config $BUILD_TYPE). A configuração gera os scripts do sistema de compilação nativo (por exemplo, arquivos Makefile ou Ninja). A construção dispara a compilação e ligação usando tais scripts. Todos os arquivos gerados na configuração e construção ficam armazenados no subdiretório build.

Compilando no VS Code

Primeiramente, apague o subdiretório build caso você já tenha compilado a ABCg via linha de comando na seção anterior.

No VS Code, selecione o menu “File > Open Folder…” e abra a pasta abcg. Na seguinte janela, selecione “Yes, I trust the authors”:

No canto inferior direito da janela aparecerá uma notificação perguntando se você quer configurar o projeto. Selecione “Yes”.

Ao fazer isso, será feita uma varredura no sistema para identificar os compiladores e toolchains disponíveis. Uma lista dos “kits” de compilação encontrados aparecerá na parte superior da janela. O exemplo a seguir é o resultado exibido no Ubuntu:

Selecione um kit compatível, como o GCC 11.2 ou mais recente. Ao fazer isso, o CMake iniciará o processo de configuração do projeto. Esse processo gera, dentro de um subdiretório build, os arquivos que serão utilizados pelo sistema de construção nativo. Caso queira invocar manualmente a configuração do CMake, acesse a paleta de comandos (Ctrl+Shift+P) e digite a opção “CMake: Configure”.

Se aparecer uma notificação pedindo para configurar o projeto sempre que ele for aberto, selecione “Yes”:

Após o término da configuração, é possível que apareça uma outra notificação solicitando permissão para configurar o Intellisense. Selecione “Allow”.

Além disso, pode aparecer também uma notificação sobre o uso do arquivo compile_commands.json, como mostrado a seguir. Selecione “Yes” novamente:

O arquivo compile_commands.json é gerado automaticamente pelo CMake. Ele contém os comandos de compilação e o caminho de cada unidade de tradução utilizada no projeto. O IntelliSense (ou clangd) utiliza as informações desse arquivo para habilitar as referências cruzadas.

Importante

A construção dos projetos usando o CMake é feita em duas etapas:

  1. Configuração: consiste na geração dos scripts do sistema de compilação nativo (por exemplo, arquivos Makefile ou Ninja);
  2. Construção: consiste no disparo da compilação e ligação usando os scripts gerados na configuração, além da execução de etapas de pré e pós-construção definidas nos scripts dos arquivos CMakeLists.txt.

Tanto os arquivos da configuração quanto os da construção (binários) são gravados no subdiretório build.

Geralmente a configuração só precisa ser feita uma vez e depois refeita caso o subdiretório build tenha sido apagado, ou após a alteração do kit de compilação, ou ainda após a alteração do build type (por exemplo, de Debug para Release). As informações de configuração ficam armazenadas em um arquivo CMakeCache.txt dentro de build.

Como indicado na figura abaixo, na barra de status há botões para selecionar o build type/configurar, selecionar o kit de compilação, e construir a aplicação. A opção de construir já se encarrega de configurar o projeto caso os arquivos de configuração ainda não tenham sido gerados.

Essas opções também estão disponíveis na paleta de comandos. Os comandos são:

  • “CMake: Select Variant”: para selecionar um build type;
  • “CMake: Select a Kit”: para selecionar um kit de compilação;
  • “CMake: Configure”: para configurar o projeto usando o kit e o build type atual;
  • “CMake: Build”: para construir o projeto.
  • “CMake: Clean Rebuild”: para apagar as configurações anteriores do CMake, reconfigurar e reconstruir o projeto.
Observação

Os build types permitidos no CMake são:

  • Debug para gerar binários não otimizados e com arquivos de símbolos de depuração. Esse é o build type padrão (ao contrário de build.sh e build.bat, que usam o modo Release por padrão);
  • RelWithDebInfo para gerar arquivos de símbolos de depuração com binários otimizados;
  • Release para gerar binários otimizados e favorecer código mais rápido. Essa opção não gera os arquivos de símbolos de depuração;
  • MinSizeRel, semelhante ao Release, mas a otimização tenta gerar binário de menor tamanho.

Para compilar e gerar os binários, tecle F7 ou clique em “Build” na barra de status. O progresso será exibido na janela “Output” do CMake/Build. Se a construção terminar com sucesso, a última linha de texto da janela Output será:

[build] Build finished with exit code 0

Os arquivos gerados na construção ficam armazenados no subdiretório build, da mesma forma como ocorre na compilação via linha de comando.

Para testar, abra um terminal e execute ./build/bin/helloworld/helloworld (Linux/macOS) ou .\build\bin\helloworld\helloworld.exe (Windows).

Atenção

A configuração do CMake gerada a partir do VS Code não é necessariamente a mesma gerada usando os scripts de linha de comando: o compilador pode ser diferente, ou o build type pode ser diferente.

Se em algum momento você construir o projeto via linha de comando usando os scripts .sh ou .bat e depois quiser construir pelo editor, apague o subdiretório build antes de retornar ao VS Code. Isso forçará uma nova configuração do CMake e evitará erros de incompatibilidade entre as configurações.

Na primeira vez que um arquivo com extensão .cpp for aberto no editor, algumas notificações poderão aparecer. Experimente abrir, por exemplo, examples\helloworld\main.cpp. Se a extensão clangd foi instalada como sugerido na seção 1.4, a caixa de mensagem a seguir será exibida. Selecione “Install”.

A seguinte mensagem também poderá aparecer. Neste caso, selecione “Disable IntelliSense”:

Ao final dessas configurações, reinicie o editor. Isso pode ser feito rapidamente selecionando a opção “Developer: Reload Window” da paleta de comandos.

Depurando no VS Code

Podemos depurar facilmente nossas aplicações com GDB ou LLDB usando a interface do VS Code.

Após construir um projeto com build type Debug ou RelWithDebInfo, devemos abrir um de seus arquivos com extensão .cpp. Isso é necessário para fazer com que o VS Code identifique que queremos configurar a depuração de um projeto em linguagem C++. Por exemplo, para configurar a depuração do projeto “Hello, World!”, abra o arquivo examples\helloworld\main.cpp.

Selecione a opção “Run and Debug” na barra de atividades (Ctrl+Shift+D). Em seguida, clique na opção “create a launch.json file”:

Se aparecer um drop-down list de seleção do ambiente de depuração, selecione “C++ (GDB/LLDB)”. Isso criará o arquivo launch.json. O arquivo também será aberto no editor.

Copie e cole em launch.json o conteúdo exibido a seguir. Este é uma exemplo para depurar o “Hello, World!” no Linux ou macOS usando o GDB. Um exemplo de configuração para o Windows é mostrado mais adiante:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "(gdb) Launch",
      "type": "cppdbg",
      "request": "launch",
      "program": "${workspaceFolder}/build/bin/helloworld/helloworld",
      "args": [],
      "stopAtEntry": false,
      "cwd": "${fileDirname}",
      "environment": [],
      "externalConsole": false,
      "MIMode": "gdb",
      "setupCommands": [
        {
          "description": "Enable pretty-printing for gdb",
          "text": "-enable-pretty-printing",
          "ignoreFailures": true
        },
        {
          "description":  "Set Disassembly Flavor to Intel",
          "text": "-gdb-set disassembly-flavor intel",
          "ignoreFailures": true
        }
      ]
    }
  ]
}

Observe que o valor da chave program aponta para o executável do projeto: ${workspaceFolder}/build/bin/helloworld/helloworld.

Observação

${workspaceFolder} é uma variável pré-definida do VS Code que contém o caminho da pasta do projeto. Consulte a documentação para informações sobre outras variáveis disponíveis.

A listagem a seguir mostra um exemplo de launch.json para depurar o “Hello, World!” no Windows usando o GDB do MSYS2:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "(gdb) Launch",
      "type": "cppdbg",
      "request": "launch",
      "program": "${workspaceFolder}/build/bin/helloworld/helloworld.exe",
      "args": [],
      "stopAtEntry": false,
      "cwd": "${workspaceFolder}",
      "environment": [],
      "externalConsole": false,
      "MIMode": "gdb",
      "miDebuggerPath": "C:\\msys64\\mingw64\\bin\\gdb.exe",
      "setupCommands": [
        {
          "description": "Enable pretty-printing for gdb",
          "text": "-enable-pretty-printing",
          "ignoreFailures": true
        },
        {
          "description":  "Set Disassembly Flavor to Intel",
          "text": "-gdb-set disassembly-flavor intel",
          "ignoreFailures": true
        }        
      ]
    }
  ]
}

Veja que o valor da chave miDebuggerPath contém o caminho completo do GDB, que é C:\msys64\mingw64\bin\gdb.exe supondo que o MSYS2 tenha sido instalado em C:\msys64.

O valor da chave externalConsole pode ser modificado para true caso você prefira que um novo terminal seja aberto durante a depuração. Consulte a documentação sobre depuração para informações sobre outras opções e informações gerais sobre como depurar código no editor.

Após modificar o arquivo launch.json, selecione novamente a opção “Run” na barra de atividades ou aperte F5 para iniciar o programa no modo de depuração.

Reedite o arquivo launch.json sempre que mudar o nome do executável que você queira depurar.

Observação

No VS Code para Windows, configure o terminal padrão para “Command Prompt” no lugar de “PowerShell”, uma vez que nossos scripts são compatíveis apenas com o Prompt de Comando. Para fazer isso, abra a paleta de comandos (Ctrl+Shift+P), acesse o comando “Terminal: Select Default Profile” e então selecione “Command Prompt”.

Compilando para WASM

Podemos compilar as aplicações ABCg para WebAssembly (WASM) de modo a executá-las diretamente no navegador. A construção é feita via linha de comando usando o toolchain Emscripten. Acompanhe a seguir como construir o “Hello, World!” para WASM e como testá-lo no navegador:

  1. Em um terminal (shell ou Prompt de Comando), ative as variáveis de ambiente do Emscripten (script emsdk_env.sh/emsdk_env.bat do SDK). Após isso, o compilador emcc deverá estar visível no PATH;
  2. No diretório abcg, execute build-wasm.sh (Linux/macOS) ou build-wasm.bat (Windows). Isso fará com que o CMake inicie a configuração do projeto e a construção dos binários. Os arquivos resultantes serão gravados em abcg/public. Em nosso caso, esses arquivos são helloworld.data (arquivo de dados/assets), helloworld.js (arquivo JavaScript) e helloworld.wasm (binário WebAssembly);
  3. Execute o script runweb.sh (Linux/macOS) ou runweb.bat (Windows) para rodar um servidor web local. O conteúdo de public ficará disponível em http://localhost:8080/;
  4. Abra a página http://localhost:8080/helloworld.html que chama o script helloworld.js recém-criado. A página HTML não faz parte do processo de construção e foi criada previamente.

O resultado será semelhante ao exibido a seguir: uma aplicação mostrando um triângulo colorido e uma caixa de diálogo com alguns controles de interface. A pequena janela de texto abaixo da janela da aplicação é uma área de texto em HTML que mostra o conteúdo do terminal. Aqui, são exibidas algumas informações sobre o OpenGL (versão utilizada, fornecedor do driver, etc).

Observação

O subdiretório public contém, além do helloworld.html:

  • full_window.html: para exibir o “Hello, World!” ocupando a janela inteira do navegador;
  • full_window_console.html: idêntico ao anterior, mas com a sobreposição das mensagens do console na tela.

Nos próximos capítulos veremos como construir novas aplicações usando a ABCg.

Revisão de C++

Se você está mais habituado com programação em C ou C++ anterior ao C++11, o código das atividades da disciplina usando a ABCg poderá parecer pouco familiar. Revisaremos nesta seção alguns dos conceitos de C++ que podem gerar dúvidas em programadores que vieram da linguagem C, como a conversão explícita de tipos usando named casts, inicialização uniforme com {} e uso da palavra-chave auto. A aplicação desses conceitos segue as boas práticas de programação indicadas no C++ Core Guidelines e são seguidas em todos os códigos de atividades da disciplina.

Dica

Aproveite as primeiras semanas de aula para se familiarizar com os conceitos do chamado “C++ moderno” (C++11 em diante). Isso facilitará o entendimento do código da ABCg nos próximos capítulos.

Named casts

Observe os seguintes exemplos de conversões explícitas de tipos (casts) usando a sintaxe tradicional oriunda da linguagem C:

int a = (int)7.9;              // double para int
float b = (float)1 / 3;        // int para float
unsigned *pa = (unsigned *)&a; // int* para unsigned*

void foo(Base *b) {
  Derived *pd = (Derived *)b;  // Base* para Derived*
  // ...
}

Estes casts “estilo C” têm uma sintaxe concisa, mas nem sempre traduzem de forma clara a intenção do programador. Além disso, a conversão é feita por conta e risco do programador. Não há verificação da validade da conversão, por exemplo, na conversão de Base* (ponteiro de uma classe base) para Derived* (ponteiro para uma classe derivada de Base).

C++ procura tornar as conversões explícitas mais expressivas e seguras através de quatro tipos de casts chamados de named casts:

  • static_cast para conversões entre tipos básicos cuja validade pode ser verificada em tempo de compilação (por exemplo, float para int e vice-versa);
  • const_cast para conversões entre tipos de diferentes qualificações (por exemplo, de int const para int);
  • reinterpret_cast para conversões entre tipos representados por diferentes padrões de bits na memória (por exemplo, int* para char*);
  • dynamic_cast para conversões de ponteiros e referências a objetos com polimorfismo dinâmico.

Os exemplos anteriores podem ser escritos assim com named casts:

int a = static_cast<int>(7.9);                   // double para int
float b = static_cast<float>(1) / 3;             // int para float
unsigned *pa = reinterpret_cast<unsigned *>(&a); // int* para unsigned*

void foo(Base *b) {
  // Não compila se Base não for uma classe polimórfica
  Derived *pd = dynamic_cast<Derived *>(b);      // Base* para Derived*
  // ...
}

A sintaxe é mais verbosa, mas a intenção do programador fica mais clara e o código torna-se mais seguro. Por exemplo, dynamic_cast retorna nulo se b (ponteiro para Base) não puder ser convertido para Derived* (por exemplo, se o objeto apontado por b é de uma classe incompatível com Derived). Esse erro passaria despercebido com o cast estilo C.

O ideal é usarmos a menor quantidade possível de casts. Em geral só precisaremos usar static_cast, e reinterpret_cast nas atividades do curso. Entretanto, alguns trechos dos códigos das atividades vão um pouco mais além e seguem a diretriz Pro.safety do C++ Core Guidelines. Essa diretriz recomenda o uso de gsl::narrow_cast e gsl::narrow no lugar de static_cast para conversões com estreitamento:

  • gsl::narrow_cast é o mesmo que static_cast.
  • gsl::narrow é o mesmo que static_cast quando não há perda de informação na conversão. Se houver perda de informação, ocorre um erro de compilação.

Conversões com estreitamento são aquelas em que existe a possiblidade de perda de informação (por exemplo, float para int, ou double para float). Considere o seguinte exemplo de substituição de static_cast por gsl::narrow em uma conversão com estreitamento:

// float x = static_cast<float>(7.9); // Antes: OK
float x = gsl::narrow<float>(7.9);    // Depois: intenção mais clara

Poderíamos usar static_cast, mas o uso de gsl::narrow deixa nossa intenção mais clara: esperamos que a conversão de 7.9 (um double) para float não perca informação. Afinal, float tem precisão suficiente para representar uma casa decimal. Se por algum motivo houver perda de informação, gsl::narrow gerará um erro de compilação e então saberemos que algo está errado3.

Agora considere o seguinte exemplo de substituição de static_cast por gsl::narrow_cast:

// int x = static_cast<int>(7.9f);   // Antes: OK
int x = gsl::narrow_cast<int>(7.9f); // Depois: intenção mais clara

Nesse caso também poderíamos usar static_cast, mas gsl::narrow_cast deixa nossa intenção mais clara: esperamos que a conversão de 7.9f (um float) para um int perca informação. Afinal, a parte fracionária será perdida. gsl::narrow_cast indica que estamos ciente dessa perda e que isso não é um problema para nós.

Em resumo, em conversões com estreitamento, isto é, com possível perda de informação, o uso de gsl::narrow e gsl::narrow_cast mostra melhor a intenção do programador. Ademais, de nossa parte, faz com que tenhamos que pensar melhor se estamos usando o static_cast corretamente.

Inicialização uniforme

O código das atividades do curso usam inicialização uniforme, que consiste no uso de {} no lugar de = ou () para inicializar objetos. Segundo a diretriz ES.23 do C++ Core Guidelines, “as regras para inicialização com {} são mais simples, mais gerais, menos ambíguas, e mais seguras do que outras formas de inicialização”.

Observe os exemplos abaixo sem inicialização com {}, isto é, usando apenas = ou ():

int a = 0;  // Inicializa a com 0
int b(42);  // Inicializa b com 42
double c(); // Declaração de uma função (ambíguo)

int d = 7.9;         // Inicializa implicitamente com 7 (inseguro)
double pi = 3.1415f; // Inicializa implicitamente com 3.1415

std::vector<int> v1(3);    // Inicializa v1 com 3 elementos de valor 0 (ambíguo)
std::vector<int> v2(3, 2); // Inicializa v2 com 3 elementos de valor 2 (ambíguo)

Compare com os exemplos abaixo com inicialização com {}:

int a{};    // Inicializa a com valor default (0)
int b{42};  // Inicializa b com 42
double c{}; // Inicializa c com valor default (0.0)

// int d{7.9};                     // ERRO: conversão com estreitamento
int d{gsl::narrow_cast<int>(7.9)}; // OK: conversão explícita de double para int
double pi{3.1415f};                // OK: conversão implícita sem estreitamento

std::vector v1{3};    // Inicializa v1 com um único elemento 3
std::vector v2{3, 2}; // Inicializa v2 com os elementos 3 e 2

Observe que a inicialização com {} proíbe conversões implícitas com estreitamento. A inicialização uniforme nos obriga a usar conversões explícitas sempre que houver a possibilidade de perda de informação. Por um lado, isso deixa o código mais verboso, como no uso do gsl::narrow_cast no exemplo anterior. Por outro lado, deixa evidente a intenção do programador e evita bugs decorrentes de conversões implícitas.

Palavra-chave auto

A palavra-chave auto permite a dedução automática do tipo de uma variável a partir de sua inicialização. O código das atividades prioriza o uso de auto para evitar repetições redundantes de tipos de dados. Essa é uma recomendação da diretiva ES.11 do C++ Core Guidelines.

Alguns exemplos de uso de auto são mostrados a seguir:

auto a{42};      // int
auto b{7.9f};    // float
auto c{1.0 / 3}; // double

// Os exemplos a seguir funcionam com qualquer tipo de dado em v
std::vector v{3, 2};
auto e{v[0]};        // Cópia de v[0]
auto &f{v[0]};       // Referência a v[0])
auto const &g{v[0]}; // Referência apenas de leitura
auto *h{&v[0]};      // Ponteiro para v[0]
auto it{v.begin()};  // std::vector<int>::iterator

// Ponteiro inteligente
auto p{std::make_unique<int[]>(1024)}; // std::unique_ptr<int[]>

// Expressão lambda
auto addTen{[](auto x) { return x + 10; }};

auto x{0U};        // unsigned
auto y{addTen(x)}; // unsigned

  1. o PowerShell não é suportado! Use apenas o Prompt de Comando.↩︎

  2. Por exemplo, gsl::narrow<T>(3.1415926535) deve funcionar em uma plataforma em que T é um ponto de flutuante de 48 bits (exótico, mas possível), mas falha em um plataforma em que T tem 32 bits.↩︎