Ir al contenido

C++ Core Guidelines y Patrones Profesionales

Aprende las C++ Core Guidelines, convenciones de nombres, correccion de const, principios de diseno de APIs, practicas de documentacion, perfilado de rendimiento y tecnicas de depuracion en produccion.

Escribiendo Codigo C++ Profesional

Escribir C++ correcto y eficiente es necesario pero no suficiente para el desarrollo profesional de software. El C++ profesional tambien significa codigo que es legible (comprensible por otros y por tu yo futuro), mantenible (facil de modificar sin introducir bugs) y robusto (maneja errores con gracia y falla de forma predecible). Las C++ Core Guidelines, mantenidas por Bjarne Stroustrup y Herb Sutter, codifican decadas de sabiduria duramente ganada en reglas accionables. Esta leccion cubre las directrices y practicas profesionales mas impactantes.

Descripcion General de las C++ Core Guidelines

Las Core Guidelines cubren gestion de recursos, interfaces, manejo de errores y nomenclatura. Aqui estan algunas de las reglas mas importantes en la practica:

core_guidelines.cpp
#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include <span>
#include <stdexcept>

// R.1: Manage resources automatically using RAII
class DatabaseConnection {
public:
    explicit DatabaseConnection(const std::string& connection_string) {
        // Acquire resource in constructor
        std::cout << "Connected to: " << connection_string << "\n";
    }
    ~DatabaseConnection() {
        // Release resource in destructor — always runs
        std::cout << "Disconnected\n";
    }

    // Rule of 5: if you define a destructor, define or delete all 5
    DatabaseConnection(const DatabaseConnection&) = delete;
    DatabaseConnection& operator=(const DatabaseConnection&) = delete;
    DatabaseConnection(DatabaseConnection&&) noexcept = default;
    DatabaseConnection& operator=(DatabaseConnection&&) noexcept = default;

    void query(std::string_view sql) const {
        std::cout << "Executing: " << sql << "\n";
    }
};

// I.4: Make interfaces precisely and strongly typed
// BAD: void process(int type, void* data, int size);
// GOOD:
enum class DataType { json, xml, csv };

void process(DataType type, std::span<const std::byte> data) {
    // type-safe, no raw pointers, size is carried with the data
}

// F.3: Keep functions short and simple
[[nodiscard]] bool is_valid_email(std::string_view email) {
    auto at_pos = email.find('@');
    if (at_pos == std::string_view::npos) return false;
    auto dot_pos = email.find('.', at_pos);
    return dot_pos != std::string_view::npos && dot_pos > at_pos + 1;
}

int main() {
    // R.5: Prefer scoped objects — no unnecessary heap allocation
    DatabaseConnection db("host=localhost dbname=mydb");
    db.query("SELECT * FROM users");
    // db is automatically closed when it goes out of scope

    // R.11: Avoid raw new/delete — use smart pointers
    auto widget = std::make_unique<std::vector<int>>(100);

    std::cout << is_valid_email("user@example.com") << "\n";   // 1
    std::cout << is_valid_email("invalid-email") << "\n";       // 0

    return 0;
}

Correccion de Const como Disciplina

La correccion de const no se trata solo de prevenir modificaciones accidentales — documenta la intencion, habilita optimizaciones del compilador y hace el codigo seguro para hilos por defecto. Haz todo const a menos que necesite ser mutable:

const_correctness.cpp
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

class StudentRegistry {
public:
    // const method — promises not to modify the object
    [[nodiscard]] size_t count() const { return students_.size(); }

    // const reference return — caller can read but not modify
    [[nodiscard]] const std::string& get_name(size_t index) const {
        if (index >= students_.size())
            throw std::out_of_range("Invalid student index");
        return students_[index];
    }

    // Non-const method — modifies the object
    void add_student(std::string name) {
        students_.push_back(std::move(name));
    }

    // const-correct algorithm usage
    [[nodiscard]] bool has_student(std::string_view name) const {
        return std::any_of(students_.cbegin(), students_.cend(),
            [name](const std::string& s) { return s == name; });
    }

private:
    std::vector<std::string> students_;
};

void print_registry(const StudentRegistry& reg) {
    // Can only call const methods on a const reference
    for (size_t i = 0; i < reg.count(); ++i) {
        std::cout << reg.get_name(i) << "\n";
    }
    // reg.add_student("Eve");  // ERROR: cannot call non-const method
}

int main() {
    StudentRegistry reg;
    reg.add_student("Alice");
    reg.add_student("Bob");

    print_registry(reg);

    // const local variables
    const auto count = reg.count();  // immutable
    const auto& name = reg.get_name(0);  // immutable reference

    std::cout << "Count: " << count << ", First: " << name << "\n";

    return 0;
}

Principios de Diseno de APIs

La regla de oro del diseno de APIs en C++, atribuida a Scott Meyers: Haz las interfaces faciles de usar correctamente y dificiles de usar incorrectamente.

Usa tipos fuertes: En lugar de void set_timeout(int ms), usa void set_timeout(std::chrono::milliseconds timeout). El llamador no puede pasar accidentalmente segundos cuando esperas milisegundos.

Usa [[nodiscard]]: Cualquier funcion que retorne un codigo de error, un recurso o un valor calculado deberia ser [[nodiscard]]. Ignorar el valor de retorno es casi siempre un bug.

Prefiere semantica de valor: Pasa tipos pequenos por valor, tipos mas grandes por const&. Retorna por valor (el compilador aplica elisión de copia). Evita parametros de salida.

Minimiza la superficie publica: Cada miembro publico es un compromiso que debes mantener. Usa private por defecto y solo expone lo necesario.

Sigue el principio de menor sorpresa: Las funciones deben hacer lo que su nombre implica, nada mas. Los efectos secundarios deben ser obvios desde el nombre (save_to_file, no process).

Convenciones de Nombres y Documentacion

La nomenclatura consistente hace el codigo legible en todo el equipo. Convenciones comunes de C++:

Tipos: PascalCase — class HttpClient, struct Point3D, enum class Color
Funciones/Metodos: snake_case o camelCase — calculate_total(), getSize()
Variables: snake_case — int item_count, double total_price
Variables miembro: guion bajo al final — int count_, std::string name_
Constantes: prefijo k o UPPER_SNAKE — constexpr int kMaxRetries = 3
Namespaces: lowercase — namespace network, namespace detail

Elige una convencion y aplicala en todo el proyecto (clang-format + clang-tidy pueden automatizar esto).

Documentacion: Usa comentarios estilo Doxygen para APIs publicas:

/// @brief Conecta a la base de datos.
/// @param connection_string La URI de conexion a la base de datos.
/// @return true si la conexion fue exitosa.
/// @throws DatabaseError si la conexion falla.
[[nodiscard]] bool connect(std::string_view connection_string);

Perfilado de Rendimiento

Nunca optimices sin perfilar primero. Estas herramientas te dicen donde realmente se gasta el tiempo, reemplazando suposiciones con datos:

benchmark.cpp
# Linux: perf — hardware performance counters
perf record -g ./my_program         # record call graph
perf report                           # interactive flamegraph viewer

# Linux: perf stat — high-level statistics
perf stat ./my_program
# Output: cycles, instructions, cache misses, branch misses, IPC

# macOS: Instruments (via Xcode)
xcrun xctrace record --template 'Time Profiler' --launch ./my_program

# Cross-platform: Google Benchmark (micro-benchmarking)
# In your code:
#include <benchmark/benchmark.h>

static void BM_VectorPushBack(benchmark::State& state) {
    for (auto _ : state) {
        std::vector<int> v;
        for (int i = 0; i < state.range(0); ++i) {
            v.push_back(i);
        }
        benchmark::DoNotOptimize(v.data());
    }
}
BENCHMARK(BM_VectorPushBack)->Range(8, 1 << 16);

static void BM_VectorReserved(benchmark::State& state) {
    for (auto _ : state) {
        std::vector<int> v;
        v.reserve(state.range(0));
        for (int i = 0; i < state.range(0); ++i) {
            v.push_back(i);
        }
        benchmark::DoNotOptimize(v.data());
    }
}
BENCHMARK(BM_VectorReserved)->Range(8, 1 << 16);

BENCHMARK_MAIN();
Buena práctica

Al revisar codigo C++, verifica estos problemas comunes:

1. Gestion de recursos: Esta cada recurso (memoria, archivos, locks) gestionado por RAII? Hay algun new/delete sin proteccion?
2. Correccion de const: Son los parametros, funciones miembro y variables locales const donde sea posible?
3. Manejo de errores: Se verifican los valores de retorno? Se capturan las excepciones en los niveles apropiados? Se usa std::expected o codigos de error de forma consistente?
4. Seguridad de hilos: Estan protegidos los recursos compartidos? Son posibles las carreras de datos? Un sanitizador detectaria problemas?
5. Semantica de movimiento: Se mueven los objetos en lugar de copiarlos donde es apropiado? Hay llamadas a std::move sobre objetos const (que silenciosamente copian)?
6. Claridad de la API: Son descriptivos los nombres de funciones? Estan los parametros fuertemente tipados? Se usa [[nodiscard]] en valores de retorno importantes?
7. Advertencias de compilacion: El codigo compila limpiamente con -Wall -Wextra -Wpedantic? Se abordan todas las advertencias, no se suprimen?
8. Testing: Se prueban los casos limite? Estan las nuevas rutas de codigo cubiertas por pruebas? Pasan los sanitizadores?

Error común

Depurar C++ en produccion es mas dificil que en desarrollo. Preparate para ello:

Siempre envia con simbolos de depuracion: Compila RelWithDebInfo para binarios de produccion. Los simbolos de depuracion en un archivo .debug separado no agregan ninguna sobrecarga en tiempo de ejecucion pero hacen posible el analisis de fallos.

Registra datos estructurados: Usa logging estructurado (JSON) con niveles de log. Incluye marcas de tiempo, IDs de hilo e IDs de solicitud. Usa spdlog o std::format para logging rapido y con seguridad de tipos.

Volcados de nucleo (Core dumps): Habilita los volcados de nucleo (ulimit -c unlimited) y sabe como analizarlos con gdb. Los servidores de simbolos almacenan informacion de depuracion para binarios desplegados.

Bugs comunes en produccion:
- Fugas de memoria — Usa ASan en entornos de staging, o herramientas como Valgrind / Heaptrack
- Deadlocks — Usa TSan en testing, registra el orden de adquisicion de locks, establece timeouts de locks
- Regresiones de rendimiento — El benchmarking continuo en CI detecta regresiones antes del despliegue
- Compatibilidad ABI — Cambiar layouts de structs o tablas de funciones virtuales rompe la ABI; usa el idioma Pimpl para ABIs estables

Puntos clave
  • Sigue las C++ Core Guidelines — RAII para recursos, Regla de 5, tipado fuerte, APIs publicas minimas
  • Haz todo const por defecto — documenta la intencion, previene bugs y habilita optimizaciones
  • Disena APIs que sean faciles de usar correctamente y dificiles de usar incorrectamente — tipos fuertes, [[nodiscard]], semantica de valor
  • Perfila antes de optimizar — usa perf, Instruments o Google Benchmark para encontrar los cuellos de botella reales
  • Revisa el codigo para gestion de recursos, correccion de const, seguridad de hilos y manejo adecuado de errores
  • Preparate para la depuracion en produccion: envia con simbolos de depuracion, usa logging estructurado, habilita volcados de nucleo

Quiz — Pon a prueba tus conocimientos

(15 XP)

1. Que establece la directriz 'Regla de 5' de las C++ Core Guidelines?

2. Cual es la regla de oro del diseno de APIs en C++?

3. Por que deberias perfilar antes de optimizar?