Partilhar: WhatsApp
aulify · Sebenta
UC · Unidade de Competência · UC02839

Sebenta · C/C++ avançado (UC02839)

Templates, STL, smart pointers, concorrência
50h · 4.5 pontos crédito Curso: T. Desenv. Software ↗ Referencial oficial SNQ
Índice

Introdução

Esta UC continua a UC00669 (C/C++ básico) e foca-se no que torna C++ uma linguagem dominante em sistemas de alta performance: templates (generics), STL (Standard Template Library), smart pointers (RAII), lambdas, exceptions, concorrência com threads, move semantics e ferramentas modernas de build (CMake) e testes (Catch2, GoogleTest).

A C++ moderna (a partir de C++11, 2011) é uma linguagem essencialmente diferente da C++ tradicional. É expressiva, segura, e quase tão rápida como C. Aprender C++ moderno abre portas em jogos AAA (Unreal Engine), motores 3D, sistemas embedded sérios, finanças de alta frequência, drivers, navegadores e muito mais.

Esta sebenta assume base sólida em C (UC00669).

1. C++ moderno

Versões e standards

Compilar com versão moderna:

g++ -std=c++20 -Wall -Wextra programa.cpp -o programa

Features essenciais

auto — inferência de tipo:

auto x = 42;              // int
auto pi = 3.14;           // double
auto v = vector<int>{1,2,3};

Range-based for:

for (auto& n : v) {
    n *= 2;
}

Initializer lists {}:

vector<int> v{1, 2, 3};
Pessoa p{"Ana", 25};
int x{42};

Structured bindings (C++17):

auto [chave, valor] = par;
for (auto& [k, v] : mapa) { ... }

nullptr em vez de NULL:

int* p = nullptr;

constexpr — calculado em compile-time:

constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}
constexpr int x = factorial(5);  // 120 em compile-time

2. Templates

Templates permitem código genérico — funciona com qualquer tipo, sem perda de performance.

Function templates

template <typename T>
T maximo(T a, T b) {
    return (a > b) ? a : b;
}

cout << maximo(3, 7);                 // int
cout << maximo(2.5, 1.8);             // double
cout << maximo<string>("ana", "bia"); // string

O compilador gera versões específicas (instanciação) para cada tipo usado.

Class templates

template <typename T>
class Caixa {
private:
    T conteudo;

public:
    Caixa(T val) : conteudo(val) {}
    T obter() const { return conteudo; }
    void definir(T val) { conteudo = val; }
};

Caixa<int> c1(42);
Caixa<string> c2("Olá");
Caixa<Pessoa> c3(Pessoa("Ana", 25));

Multiple template parameters

template <typename K, typename V>
class Par {
public:
    K chave;
    V valor;
    Par(K k, V v) : chave(k), valor(v) {}
};

Par<string, int> p("Ana", 25);

Variadic templates

Aceita número variável de argumentos:

template <typename T>
void imprimir(T arg) {
    cout << arg << endl;
}

template <typename T, typename... Args>
void imprimir(T primeiro, Args... resto) {
    cout << primeiro << " ";
    imprimir(resto...);
}

imprimir(1, "Ana", 3.14, true);

Concepts (C++20)

Restringe templates a tipos que satisfazem certos requisitos:

#include <concepts>

template <integral T>   // só inteiros
T dobro(T x) { return x * 2; }

dobro(5);     // OK
dobro(2.5);   // ERRO de compilação

3. STL — Standard Template Library

A STL tem três pilares: containers, iterators, algorithms.

Containers

Sequência (mantêm ordem de inserção): - vector<T> — array dinâmico (padrão). - list<T> — lista duplamente ligada. - deque<T> — double-ended queue. - array<T, N> — array fixo no stack. - forward_list<T> — lista simples ligada.

Associativos ordenados (baseados em árvore balanceada): - map<K, V> — chave → valor único, ordenado. - set<T> — únicos, ordenado. - multimap<K, V> — pode haver chaves duplicadas. - multiset<T>.

Associativos unordered (hash table): - unordered_map<K, V> — O(1) amortizado. - unordered_set<T>. - E versões multi.

Adaptadores: - stack<T> — LIFO. - queue<T> — FIFO. - priority_queue<T> — heap (max-heap por padrão).

vector — o trabalho-cavalo

#include <vector>

vector<int> v;                    // vazio
vector<int> v2(5);                // 5 zeros
vector<int> v3(5, 100);           // 5 cópias de 100
vector<int> v4{1, 2, 3, 4, 5};    // initializer list

// Adicionar
v.push_back(10);
v.emplace_back(20);   // constrói in-place (eficiente para tipos complexos)

// Tamanho
v.size();
v.empty();
v.capacity();          // memória alocada (≥ size)
v.reserve(1000);       // reserva sem allocar elementos

// Acesso
v[0];                  // sem bounds check
v.at(0);               // com bounds check (lança out_of_range)
v.front(), v.back();

// Remover
v.pop_back();
v.erase(v.begin() + 2);    // remove índice 2
v.clear();

// Iterar
for (auto& n : v) { ... }
for (auto it = v.begin(); it != v.end(); ++it) { ... }

map / unordered_map

#include <map>
#include <unordered_map>

map<string, int> idades;   // ordenado por chave
idades["Ana"] = 25;
idades["Bruno"] = 30;

// Acesso
int n = idades["Ana"];     // 25

// Verificar (sem criar)
if (auto it = idades.find("Carla"); it != idades.end()) {
    cout << it->second;
}

// C++20: contains
if (idades.contains("Ana")) { ... }

// Iterar (ordenado por chave)
for (const auto& [nome, idade] : idades) {
    cout << nome << ": " << idade << endl;
}

// Remover
idades.erase("Ana");

unordered_map tem a mesma API mas é hash-based — muito mais rápido para acessos pontuais (O(1) vs O(log n)), mas iteração sem ordem garantida.

Iterators

Iterators são "pointers generalizados" — abstraem percurso de containers.

vector<int> v{1, 2, 3, 4, 5};

auto it = v.begin();    // primeiro
auto end = v.end();     // depois do último

while (it != end) {
    cout << *it << " ";
    ++it;
}

Categorias: - Input/Output: leitura/escrita uma passagem. - Forward: andar para a frente. - Bidirectional: para a frente e para trás (list, map). - Random access: salto a posição (vector, deque, array).

Algorithms

<algorithm> tem ~100 funções genéricas que operam sobre iterators:

#include <algorithm>
#include <numeric>

vector<int> v{3, 1, 4, 1, 5, 9, 2, 6};

// Ordenar
sort(v.begin(), v.end());
sort(v.begin(), v.end(), greater<int>());

// Reverse
reverse(v.begin(), v.end());

// Pesquisar
auto it = find(v.begin(), v.end(), 5);
bool tem = (it != v.end());

binary_search(v.begin(), v.end(), 5);  // requer ordenado

// Contar
int n = count(v.begin(), v.end(), 1);

int n2 = count_if(v.begin(), v.end(),
    [](int x) { return x > 3; });

// Transformar (in-place)
transform(v.begin(), v.end(), v.begin(),
    [](int x) { return x * 2; });

// Acumular
int soma = accumulate(v.begin(), v.end(), 0);
int prod = accumulate(v.begin(), v.end(), 1, multiplies<int>());

// Min/max
int mn = *min_element(v.begin(), v.end());
auto [mn2, mx] = minmax_element(v.begin(), v.end());

// Verificar predicados
bool todos = all_of(v.begin(), v.end(), [](int x) { return x > 0; });
bool algum = any_of(v.begin(), v.end(), [](int x) { return x > 100; });
bool nenhum = none_of(v.begin(), v.end(), [](int x) { return x < 0; });

// Copy / move
vector<int> dest;
copy_if(v.begin(), v.end(), back_inserter(dest),
    [](int x) { return x % 2 == 0; });

// Unique (em sequência ordenada)
sort(v.begin(), v.end());
auto last = unique(v.begin(), v.end());
v.erase(last, v.end());

Ranges (C++20)

Sintaxe mais limpa para chains de operações:

#include <ranges>

vector<int> v{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

auto resultado = v
    | views::filter([](int n) { return n % 2 == 0; })
    | views::transform([](int n) { return n * n; })
    | views::take(3);

for (int n : resultado) cout << n << " ";
// 4 16 36

4. Smart pointers e RAII

RAII — Resource Acquisition Is Initialization

Princípio fundamental de C++: recursos são adquiridos em construtores e libertados em destrutores. Garante limpeza mesmo com exceptions.

class Ficheiro {
    FILE* f;
public:
    Ficheiro(const char* nome) : f(fopen(nome, "r")) {
        if (!f) throw runtime_error("Não abriu");
    }

    ~Ficheiro() {
        if (f) fclose(f);
    }
};

void usar() {
    Ficheiro f("dados.txt");
    // ... 
    // mesmo com throw aqui, ~Ficheiro é chamado
}

unique_ptr

Ownership único, sem cópia, com auto-delete:

#include <memory>

void exemplo() {
    auto p = make_unique<Pessoa>("Ana", 25);
    p->apresentar();
    cout << p->getNome();
}  // delete automático aqui

// Move (não cópia)
auto p2 = move(p);  // p agora é nullptr

// Não funciona:
auto p3 = p;        // ERRO de compilação

Para arrays:

auto arr = make_unique<int[]>(10);
arr[0] = 42;

shared_ptr

Ownership partilhado com reference counting:

auto p1 = make_shared<Pessoa>("Ana", 25);
auto p2 = p1;       // OK — incrementa refcount
auto p3 = p1;

cout << p1.use_count();   // 3

p2.reset();
cout << p1.use_count();   // 2

// Pessoa é deletada quando o último shared_ptr morre

Custo: mais memória (control block), atomic refcount (lock-free).

weak_ptr

Referência não-owning para shared_ptr — evita ciclos:

class Pai;

class Filho {
    weak_ptr<Pai> pai;   // weak para evitar ciclo
};

class Pai {
    vector<shared_ptr<Filho>> filhos;
};

// Para usar weak_ptr:
weak_ptr<Pessoa> wp;
if (auto sp = wp.lock()) {  // tenta promover para shared_ptr
    sp->apresentar();
} else {
    cout << "Já não existe";
}

Regra geral

5. Lambdas

Sintaxe básica

auto somar = [](int a, int b) {
    return a + b;
};

cout << somar(2, 3);   // 5

// Tipo deduzido (auto), pode ser passado a funções

Captures

int x = 10;

auto a = [x]() { return x; };        // captura por valor
auto b = [&x]() { x++; };            // captura por referência
auto c = [=]() { return x; };        // tudo por valor
auto d = [&]() { x++; };             // tudo por referência
auto e = [x, &y]() { return x + y; }; // mistura

// Capturar this (em método de classe)
class Foo {
    int membro;
    void bar() {
        auto f = [this]() { return membro; };
    }
};

Lambdas com STL

vector<Pessoa> pessoas{ ... };

// Sort por idade
sort(pessoas.begin(), pessoas.end(),
    [](const Pessoa& a, const Pessoa& b) {
        return a.idade < b.idade;
    });

// Filter
vector<Pessoa> adultos;
copy_if(pessoas.begin(), pessoas.end(), back_inserter(adultos),
    [](const Pessoa& p) { return p.idade >= 18; });

// for_each
for_each(pessoas.begin(), pessoas.end(),
    [](const Pessoa& p) { cout << p.nome << endl; });

Lambdas como callbacks

void executar(function<void()> f) {
    f();
}

executar([]() { cout << "Olá!" << endl; });

int x = 5;
executar([x]() { cout << x << endl; });

6. Exceptions

Hierarquia padrão

exception
├── logic_error
│   ├── invalid_argument
│   ├── out_of_range
│   ├── length_error
│   └── domain_error
└── runtime_error
    ├── overflow_error
    ├── underflow_error
    └── range_error

Throw / catch

#include <stdexcept>

void validar(int idade) {
    if (idade < 0)
        throw invalid_argument("Idade negativa");
    if (idade > 150)
        throw out_of_range("Idade demasiado alta");
}

try {
    validar(-5);
}
catch (const invalid_argument& e) {
    cerr << "Argumento: " << e.what() << endl;
}
catch (const exception& e) {
    cerr << "Geral: " << e.what() << endl;
}
catch (...) {
    cerr << "Desconhecido" << endl;
}

Custom exceptions

class SaldoInsuficiente : public runtime_error {
public:
    double saldoActual;

    SaldoInsuficiente(double saldo, const string& msg)
        : runtime_error(msg), saldoActual(saldo) {}
};

void sacar(double valor) {
    if (valor > saldo)
        throw SaldoInsuficiente(saldo, "Não chega");
}

try {
    sacar(1000);
} catch (const SaldoInsuficiente& e) {
    cerr << e.what() << " (saldo: " << e.saldoActual << ")";
}

noexcept

Indica que função não lança:

int somar(int a, int b) noexcept { return a + b; }

Move constructors devem ser noexcept (STL optimiza).

7. Concorrência

std::thread

#include <thread>

void trabalho(int id) {
    cout << "Thread " << id << endl;
}

int main() {
    thread t1(trabalho, 1);
    thread t2(trabalho, 2);

    t1.join();   // espera t1
    t2.join();
}

Mutex

Para proteger dados partilhados:

#include <mutex>

mutex m;
int contador = 0;

void incrementar() {
    for (int i = 0; i < 1000; i++) {
        lock_guard<mutex> lock(m);   // RAII
        contador++;
    }
}

// Threads acabam, contador = N * 1000 (garantido)

unique_lock é mais flexível (pode unlock manualmente):

unique_lock<mutex> lock(m);
// ...
lock.unlock();
// ...
lock.lock();

Atomic

Operações thread-safe sem mutex:

#include <atomic>

atomic<int> contador{0};

void incrementar() {
    for (int i = 0; i < 1000; i++) {
        contador++;   // atómico, sem mutex
    }
}

async / future

#include <future>

int trabalho_pesado() {
    this_thread::sleep_for(chrono::seconds(2));
    return 42;
}

auto futuro = async(launch::async, trabalho_pesado);

// Fazer outras coisas...

int resultado = futuro.get();  // bloqueia até estar pronto

Condition variables

Para sinalização entre threads:

#include <condition_variable>

mutex m;
condition_variable cv;
bool pronto = false;

void produtor() {
    {
        lock_guard<mutex> lock(m);
        pronto = true;
    }
    cv.notify_one();
}

void consumidor() {
    unique_lock<mutex> lock(m);
    cv.wait(lock, []{ return pronto; });
    // ...
}

8. Move semantics

Problema da cópia

vector<int> v{1, 2, 3, ... 1'000'000};

vector<int> v2 = v;   // copia TUDO — lento

Solução: move

vector<int> v3 = move(v);   // move — só transfere pointer interno
// v fica vazio (estado "moved-from")

Move constructor / assignment

class MeuVec {
    int* dados;
    size_t n;

public:
    // Copy constructor (caro)
    MeuVec(const MeuVec& outro) : n(outro.n) {
        dados = new int[n];
        copy(outro.dados, outro.dados + n, dados);
    }

    // Move constructor (barato)
    MeuVec(MeuVec&& outro) noexcept
        : dados(outro.dados), n(outro.n)
    {
        outro.dados = nullptr;
        outro.n = 0;
    }

    // Copy assignment
    MeuVec& operator=(const MeuVec& outro) {
        if (this != &outro) {
            delete[] dados;
            n = outro.n;
            dados = new int[n];
            copy(outro.dados, outro.dados + n, dados);
        }
        return *this;
    }

    // Move assignment
    MeuVec& operator=(MeuVec&& outro) noexcept {
        if (this != &outro) {
            delete[] dados;
            dados = outro.dados;
            n = outro.n;
            outro.dados = nullptr;
            outro.n = 0;
        }
        return *this;
    }

    ~MeuVec() { delete[] dados; }
};

Rule of Zero / Five

9. CMake — build moderno

CMakeLists.txt mínimo

cmake_minimum_required(VERSION 3.20)
project(MeuProjecto LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

add_executable(programa
    src/main.cpp
    src/utils.cpp
)

target_include_directories(programa PRIVATE include)

target_compile_options(programa PRIVATE -Wall -Wextra)

Build

cmake -B build              # configurar
cmake --build build         # compilar
./build/programa            # correr

Out-of-source build (build/ separado do código) é boa prática.

Dependências externas

find_package(Threads REQUIRED)
target_link_libraries(programa PRIVATE Threads::Threads)

# Bibliotecas modernas com FetchContent
include(FetchContent)
FetchContent_Declare(
  fmt
  GIT_REPOSITORY https://github.com/fmtlib/fmt.git
  GIT_TAG 10.1.1
)
FetchContent_MakeAvailable(fmt)

target_link_libraries(programa PRIVATE fmt::fmt)

10. Testes com Catch2

#include <catch2/catch_test_macros.hpp>
#include "calculadora.h"

TEST_CASE("Somar funciona", "[calc]") {
    Calculadora c;
    REQUIRE(c.somar(2, 3) == 5);
    REQUIRE(c.somar(-1, 1) == 0);
}

TEST_CASE("Dividir por zero lança", "[calc]") {
    Calculadora c;
    REQUIRE_THROWS_AS(c.dividir(10, 0), invalid_argument);
}

TEST_CASE("Sections", "[calc]") {
    Calculadora c;

    SECTION("positivos") {
        REQUIRE(c.somar(2, 3) == 5);
    }

    SECTION("negativos") {
        REQUIRE(c.somar(-2, -3) == -5);
    }
}

Adicionar a CMakeLists:

enable_testing()
add_executable(tests tests/test_main.cpp)
target_link_libraries(tests PRIVATE Catch2::Catch2WithMain)
add_test(NAME tests COMMAND tests)

Correr:

cd build
ctest --output-on-failure

11. Boas práticas

Modern C++ guidelines

Princípios

Anti-patterns a evitar

12. Próximos passos

Recursos

C++ moderno é uma das linguagens mais poderosas e expressivas que existem. A curva inicial é íngreme — templates, move semantics, RAII demoram a interiorizar. Mas o domínio compensa: tens controlo absoluto sobre o hardware com abstrações elegantes. Para jogos, sistemas críticos, finanças, robótica — C++ continua a ser rei.