Estándares de Codificación C++ (C++ Core Guidelines)
Estándares de codificación integrales para C++ moderno (C++17/20/23) derivados de las C++ Core Guidelines. Aplica seguridad de tipos, seguridad de recursos, inmutabilidad y claridad.
Cuándo Usar
- Escribir nuevo código C++ (clases, funciones, plantillas)
- Revisar o refactorizar código C++ existente
- Tomar decisiones arquitectónicas en proyectos C++
- Aplicar un estilo consistente en un código base C++
- Elegir entre características del lenguaje (ej.
enum vs enum class, puntero sin procesar vs puntero inteligente)
Cuándo NO Usar
- Proyectos que no sean C++
- Código base C heredado que no pueda adoptar características modernas de C++
- Contextos embebidos/de metal desnudo donde guías específicas entren en conflicto con restricciones de hardware (adaptar selectivamente)
Principios Transversales
Estos temas se repiten en todas las guías y forman la base:
- RAII en todas partes (P.8, R.1, E.6, CP.20): Vincular la vida útil del recurso a la vida útil del objeto
- Inmutabilidad por defecto (P.10, Con.1-5, ES.25): Comenzar con
const/constexpr; la mutabilidad es la excepción
- Seguridad de tipos (P.4, I.4, ES.46-49, Enum.3): Usar el sistema de tipos para prevenir errores en tiempo de compilación
- Expresar la intención (P.3, F.1, NL.1-2, T.10): Los nombres, tipos y conceptos deben comunicar el propósito
- Minimizar la complejidad (F.2-3, ES.5, Per.4-5): El código simple es código correcto
- Semántica de valor sobre semántica de puntero (C.10, R.3-5, F.20, CP.31): Preferir devolver por valor y objetos con ámbito
Filosofía e Interfaces (P., I.)
Reglas Clave
| Regla | Resumen |
|---|
| P.1 | Expresar ideas directamente en código |
| P.3 | Expresar la intención |
| P.4 | Idealmente, un programa debe ser estáticamente seguro en tipos |
| P.5 | Preferir la comprobación en tiempo de compilación a la comprobación en tiempo de ejecución |
| P.8 | No filtrar ningún recurso |
| P.10 | Preferir datos inmutables a datos mutables |
| I.1 | Hacer las interfaces explícitas |
| I.2 | Evitar variables globales no constantes |
| I.4 | Hacer las interfaces precisas y fuertemente tipadas |
| I.11 | Nunca transferir propiedad mediante un puntero o referencia sin procesar |
| I.23 | Mantener bajo el número de argumentos de función |
HACER
struct Temperature {
double kelvin;
};
Temperature boil(const Temperature& water);
NO HACER
double boil(double* temp);
int g_counter = 0;
Funciones (F.*)
Reglas Clave
| Regla | Resumen |
|---|
| F.1 | Empaquetar operaciones significativas como funciones cuidadosamente nombradas |
| F.2 | Una función debe realizar una única operación lógica |
| F.3 | Mantener las funciones cortas y simples |
| F.4 | Si una función puede evaluarse en tiempo de compilación, declararla constexpr |
| F.6 | Si su función no debe lanzar excepciones, declararla noexcept |
| F.8 | Preferir funciones puras |
| F.16 | Para parámetros "de entrada", pasar tipos de copia barata por valor y otros por const& |
| F.20 | Para valores "de salida", preferir valores de retorno a parámetros de salida |
| F.21 | Para devolver múltiples valores "de salida", preferir devolver una estructura |
| F.43 | Nunca devolver un puntero o referencia a un objeto local |
Paso de Parámetros
void print(int x);
void analyze(const std::string& data);
void transform(std::string s);
struct ParseResult {
std::string token;
int position;
};
ParseResult parse(std::string_view input);
void parse(std::string_view input,
std::string& token, int& pos);
Funciones Puras y Constexpr
constexpr int factorial(int n) noexcept {
return (n <= 1) ? 1 : n * factorial(n - 1);
}
static_assert(factorial(5) == 120);
Antipatrones
- Devolver
T&& desde funciones (F.45)
- Usar
va_arg / variádicos al estilo C (F.55)
- Capturar por referencia en lambdas pasadas a otros hilos (F.53)
- Devolver
const T que inhibe la semántica de movimiento (F.49)
Clases y Jerarquías de Clases (C.*)
Reglas Clave
| Regla | Resumen |
|---|
| C.2 | Usar class si existe invariante; struct si los miembros de datos varían independientemente |
| C.9 | Minimizar la exposición de miembros |
| C.20 | Si puede evitar definir operaciones por defecto, hágalo (Regla de Cero) |
| C.21 | Si define o =delete cualquier copia/movimiento/destructor, manéjelos todos (Regla de Cinco) |
| C.35 | Destructor de clase base: virtual público o virtual protegido no virtual |
| C.41 | Un constructor debe crear un objeto completamente inicializado |
| C.46 | Declarar constructores de un solo argumento como explicit |
| C.67 | Una clase polimórfica debe suprimir la copia/movimiento públicos |
| C.128 | Funciones virtuales: especificar exactamente uno de virtual, override o final |
Regla de Cero
struct Employee {
std::string name;
std::string department;
int id;
};
Regla de Cinco
class Buffer {
public:
explicit Buffer(std::size_t size)
: data_(std::make_unique<char[]>(size)), size_(size) {}
~Buffer() = default;
Buffer(const Buffer& other)
: data_(std::make_unique<char[]>(other.size_)), size_(other.size_) {
std::copy_n(other.data_.get(), size_, data_.get());
}
Buffer& operator=(const Buffer& other) {
if (this != &other) {
auto new_data = std::make_unique<char[]>(other.size_);
std::copy_n(other.data_.get(), other.size_, new_data.get());
data_ = std::move(new_data);
size_ = other.size_;
}
return *this;
}
Buffer(Buffer&&) noexcept = default;
Buffer& operator=(Buffer&&) noexcept = default;
private:
std::unique_ptr<char[]> data_;
std::size_t size_;
};
Jerarquía de Clases
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0;
};
class Circle : public Shape {
public:
explicit Circle(double r) : radius_(r) {}
double area() const override { return 3.14159 * radius_ * radius_; }
private:
double radius_;
};
Antipatrones
- Llamar funciones virtuales en constructores/destructores (C.82)
- Usar
memset/memcpy en tipos no triviales (C.90)
- Proporcionar diferentes argumentos por defecto para función virtual y sobreescritura (C.140)
- Hacer miembros de datos
const o referencias, lo que suprime movimiento/copia (C.12)
Gestión de Recursos (R.*)
Reglas Clave
| Regla | Resumen |
|---|
| R.1 | Gestionar recursos automáticamente usando RAII |
| R.3 | Un puntero sin procesar (T*) no es propietario |
| R.5 | Preferir objetos con ámbito; no asignar en el montón innecesariamente |
| R.10 | Evitar malloc()/free() |
| R.11 | Evitar llamar a new y delete explícitamente |
| R.20 | Usar unique_ptr o shared_ptr para representar propiedad |
| R.21 | Preferir unique_ptr sobre shared_ptr a menos que se comparta la propiedad |
| R.22 | Usar make_shared() para crear shared_ptrs |
Uso de Punteros Inteligentes
auto widget = std::make_unique<Widget>("config");
auto cache = std::make_shared<Cache>(1024);
void render(const Widget* w) {
if (w) w->draw();
}
render(widget.get());
Patrón RAII
class FileHandle {
public:
explicit FileHandle(const std::string& path)
: handle_(std::fopen(path.c_str(), "r")) {
if (!handle_) throw std::runtime_error("Error al abrir: " + path);
}
~FileHandle() {
if (handle_) std::fclose(handle_);
}
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
FileHandle(FileHandle&& other) noexcept
: handle_(std::exchange(other.handle_, nullptr)) {}
FileHandle& operator=(FileHandle&& other) noexcept {
if (this != &other) {
if (handle_) std::fclose(handle_);
handle_ = std::exchange(other.handle_, nullptr);
}
return *this;
}
private:
std::FILE* handle_;
};
Antipatrones
new/delete sin envoltorio (R.11)
malloc()/free() en código C++ (R.10)
- Múltiples asignaciones de recursos en una sola expresión (R.13 -- riesgo de seguridad de excepciones)
shared_ptr donde unique_ptr es suficiente (R.21)
Expresiones y Sentencias (ES.*)
Reglas Clave
| Regla | Resumen |
|---|
| ES.5 | Mantener ámbitos pequeños |
| ES.20 | Inicializar siempre un objeto |
| ES.23 | Preferir sintaxis de inicialización con {} |
| ES.25 | Declarar objetos const o constexpr a menos que se pretenda modificar |
| ES.28 | Usar lambdas para inicialización compleja de variables const |
| ES.45 | Evitar constantes mágicas; usar constantes simbólicas |
| ES.46 | Evitar conversiones aritméticas restrictivas/pérdidas |
| ES.47 | Usar nullptr en lugar de 0 o NULL |
| ES.48 | Evitar conversiones de tipo (casts) |
| ES.50 | No eliminar const mediante cast |
Inicialización
const int max_retries{3};
const std::string name{"widget"};
const std::vector<int> primes{2, 3, 5, 7, 11};
const auto config = [&] {
Config c;
c.timeout = std::chrono::seconds{30};
c.retries = max_retries;
c.verbose = debug_mode;
return c;
}();
Antipatrones
- Variables no inicializadas (ES.20)
- Usar
0 o NULL como puntero (ES.47 -- usar nullptr)
- Conversiones de tipo al estilo C (ES.48 -- usar
static_cast, const_cast, etc.)
- Eliminar
const mediante cast (ES.50)
- Números mágicos sin constantes con nombre (ES.45)
- Mezclar aritmética con y sin signo (ES.100)
- Reutilizar nombres en ámbitos anidados (ES.12)
Manejo de Errores (E.*)
Reglas Clave
| Regla | Resumen |
|---|
| E.1 | Desarrollar una estrategia de manejo de errores temprano en el diseño |
| E.2 | Lanzar una excepción para señalar que una función no puede realizar su tarea asignada |
| E.6 | Usar RAII para evitar fugas |
| E.12 | Usar noexcept cuando lanzar sea imposible o inaceptable |
| E.14 | Usar tipos definidos por el usuario diseñados específicamente como excepciones |
| E.15 | Lanzar por valor, capturar por referencia |
| E.16 | Destructores, desasignación y swap nunca deben fallar |
| E.17 | No intentar capturar cada excepción en cada función |
Jerarquía de Excepciones
class AppError : public std::runtime_error {
public:
using std::runtime_error::runtime_error;
};
class NetworkError : public AppError {
public:
NetworkError(const std::string& msg, int code)
: AppError(msg), status_code(code) {}
int status_code;
};
void fetch_data(const std::string& url) {
throw NetworkError("conexión rechazada", 503);
}
void run() {
try {
fetch_data("https://api.example.com");
} catch (const NetworkError& e) {
log_error(e.what(), e.status_code);
} catch (const AppError& e) {
log_error(e.what());
}
}
Antipatrones
- Lanzar tipos integrados como
int o literales de cadena (E.14)
- Capturar por valor (riesgo de división/doble tipo) (E.15)
- Bloques catch vacíos que tragan errores silenciosamente
- Usar excepciones para control de flujo (E.3)
- Manejo de errores basado en estado global como
errno (E.28)
Constantes e Inmutabilidad (Con.*)
Todas las Reglas
| Regla | Resumen |
|---|
| Con.1 | Por defecto, hacer los objetos inmutables |
| Con.2 | Por defecto, hacer las funciones miembro const |
| Con.3 | Por defecto, pasar punteros y referencias a const |
| Con.4 | Usar const para valores que no cambian después de la construcción |
| Con.5 | Usar constexpr para valores computables en tiempo de compilación |
class Sensor {
public:
explicit Sensor(std::string id) : id_(std::move(id)) {}
const std::string& id() const { return id_; }
double last_reading() const { return reading_; }
void record(double value) { reading_ = value; }
private:
const std::string id_;
double reading_{0.0};
};
void display(const Sensor& s) {
std::cout << s.id() << ": " << s.last_reading() << '\n';
}
constexpr double PI = 3.14159265358979;
constexpr int MAX_SENSORS = 256;
Concurrencia y Paralelismo (CP.*)
Reglas Clave
| Regla | Resumen |
|---|
| CP.2 | Evitar condiciones de carrera |
| CP.3 | Minimizar el intercambio explícito de datos modificables |
| CP.4 | Pensar en términos de tareas, en lugar de hilos |
| CP.8 | No usar volatile para sincronización |
| CP.20 | Usar RAII, nunca usar lock()/unlock() simples |
| CP.21 | Usar std::scoped_lock para adquirir múltiples mutex |
| CP.22 | Nunca llamar código desconocido mientras se mantiene un bloqueo |
| CP.42 | No esperar sin una condición |
| CP.44 | Recordar nombrar sus lock_guards y unique_locks |
| CP.100 | No usar programación libre de bloqueos a menos que sea absolutamente necesario |
Bloqueo Seguro
class ThreadSafeQueue {
public:
void push(int value) {
std::scoped_lock lock(mutex_);
queue_.push(value);
}
std::optional<int> pop() {
std::scoped_lock lock(mutex_);
if (queue_.empty()) return std::nullopt;
int val = queue_.front();
queue_.pop();
return val;
}
bool empty() const {
std::scoped_lock lock(mutex_);
return queue_.empty();
}
private:
mutable std::mutex mutex_;
std::queue<int> queue_;
};
Tareas vs Hilos
std::future<int> result = std::async(std::launch::async, [] {
return heavy_computation();
});
int value = result.get();
Antipatrones
- Usar
volatile para sincronización (CP.8)
- Esperar ocupado sin una variable de condición (CP.42)
- Llamar código de usuario mientras se mantiene un bloqueo (CP.22)
lock() / unlock() manual (CP.20)
- Adquirir múltiples mutex sin
std::scoped_lock (CP.21)