Qué subset de C++ usar (y cuál evitar)
C++ no es un solo lenguaje en la práctica. Es una caja de herramientas lo bastante grande para escribir un kernel, un GUI toolkit, una biblioteca de template metaprogramming o un accidente ilegible. Low-level C++ funciona mejor cuando el proyecto elige un house subset: las features que hacen explícitos ownership, layout e invariantes, más las features que banea o pone en cuarentena porque esconden costo de runtime.
El reset: "usamos C++" no es una política. "Usamos RAII, move-only ownership, spans para views, errores explícitos en bordes de sistema, sin raw owning
newy sin RTTI/exceptions en la mitad kernel" sí es una política.
Cómo funciona de verdad
Un buen subset no es anti-moderno. Es selectivo. Conserva features cuyos mecanismos son visibles y cuyo código generado podés inspeccionar; evita features que crean sorpresas de lifetime global, trampas ABI, allocation accidental o control flow no-local donde el runtime no puede soportarlo.
| Preferí | Tené cuidado con | Evitá normalmente en hot paths low-level |
|---|---|---|
| Wrappers RAII | destructores custom con ownership difícil | cleanup manual emparejado repartido en returns |
std::unique_ptr y move-only types |
std::shared_ptr en grafos de ownership confusos |
raw owning pointers |
std::span y std::string_view |
devolver views | views a temporaries |
std::vector, std::array, std::string |
allocator behavior y relocation | std::list por defecto |
enum class y strong types |
demasiados wrappers diminutos | sopa de enums unscoped |
std::optional, std::variant, expected |
tipos de error débiles o solo string | sentinel values que se solapan con datos válidos |
| Templates y concepts | compile time y tamaño de código | interfaces template unconstrained |
| Lambdas en algorithms locales | captures por referencia que cruzan lifetime | callbacks type-erased en hot loops |
| Assertions y sanitizers | errores recoverable como asserts | continuar después de invariantes violadas |
La política del proyecto está arriba de esta tabla. Google C++ y LLVM toman decisiones no-exceptions por motivos de codebase grande y systems. C++ Core Guidelines recomienda RAII, ownership explícito y exceptions para algunas fallas de contrato. No son contradicciones; son constraints distintas de runtime y compatibilidad.
Artefacto ejecutable: un house subset chico
El demo ejecutable vive en examples/modern-cpp/which-subset-of-cpp-to-use/. Compila con
-fno-exceptions -fno-rtti para mostrar un subset systems-friendly: RAII para logging,
enum class para protocol tags, std::span para una byte view, std::optional para falla
local de parseo y ningún raw owning allocation.
cd examples/modern-cpp/which-subset-of-cpp-to-use
./run.sh
El parser recibe una view, no ownership:
static std::optional<Packet> parse_packet(std::span<std::uint8_t const> bytes)
{
if (bytes.size() < 2)
return std::nullopt;
auto kind = decode_kind(bytes[0]);
if (!kind.has_value())
return std::nullopt;
std::size_t payload_size = bytes[1];
if (bytes.size() != payload_size + 2)
return std::nullopt;
return Packet {
*kind,
std::vector<std::uint8_t>(bytes.begin() + 2, bytes.end()),
};
}
El objeto ConnectionLog es RAII aburrido: construction abre el scope, destruction lo
cierra y copying está borrado. Esa es la forma que querés para file descriptors, locks,
temporary mappings, trace scopes y cualquier otra operación emparejada.
El subset con el que empezaría
- Baseline: C++20 para proyectos systems hosted nuevos, con features C++23 admitidas solo después de checks de compiler y standard-library.
- Ownership: value types por defecto,
std::unique_ptrpara ownership exclusivo de heap,std::shared_ptrsolo cuando lifetime compartido es el modelo real. - Views:
std::span<T>,std::span<const T>ystd::string_viewpara parámetros non-owning, con lifetime documentado por la relación caller/callee. - Política de errores: exceptions en código hosted exception-safe; valores estilo
expecteden bordes de sistema, parsers, allocators, drivers y no-exceptions. - Generic code: concepts en templates públicos,
if constexprpara ramas type-dependent y headers chicos que no instancien un universo. - Containers:
std::array,std::vectory layout contiguo primero; node containers solo cuando importe su comportamiento específico de invalidation o splice. - Interop C:
extern "C"en bordes ABI, fixed-width types donde importe layout y adapters explícitos alrededor de APIs errno o puntero-longitud. - Prohibido por defecto: raw owning
new/delete, C-style casts, raw pointers owning, estado global mutable, narrowing unchecked y overload sets ingeniosos que requieren una pausa de language-lawyer. - En cuarentena por política: exceptions, RTTI, coroutines,
std::function, custom allocators, static initialization con side effects y runtime polymorphism en hot paths.
Fallas típicas y trade-offs
- Un subset que solo es tradición oral. Si las reglas no están escritas, cada review vuelve a discutir gustos.
- Banear demasiado. Si el subset prohíbe RAII,
vectoro templates por completo, la gente recrea versiones peores con macros y cleanup manual. - Permitir todo en código frío y descubrir hot code después. Una API linda de plugins puede volverse un call path por paquete. Re-chequeá abstracciones cuando el código se mueve.
- Confundir no-exceptions con no-errors. Deshabilitar exceptions exige una disciplina de errores explícitos más fuerte, no menos diseño.
- ABI drift. Templates, inline functions y standard-library types son incómodos como interfaces binarias estables entre compilers o versiones de libraries.
- Sorpresas de static initialization. Objetos globales con constructors pueden crear bugs de initialization order y costo de startup.
En la práctica
Escribí el subset como un documento corto de ingeniería y enforcealo con compiler flags, warnings, sanitizers, code review y ejemplos. El punto no es pureza. El punto es que un lector pueda mirar una function signature y saber quién posee memoria, quién puede fallar, quién puede alocar, quién puede throwear y de qué lifetime dependen las referencias.
Después dejá una válvula de presión. Hay código que de verdad necesita runtime polymorphism, type erasure, shared ownership o exceptions. El subset debería volver esas decisiones visibles y reviewable, no imposibles.
Conecta con: C vs C++: qué ganás de verdad (y qué pagás) · RAII: la idea que cambia todo · Smart pointers: unique_ptr, shared_ptr, ownership · Abstracciones de costo cero · Error handling: exceptions vs expected · SerenityOS: un sistema operativo escrito en C++ · Memory leaks y disciplina de ownership · Dynamic linking y shared libraries
Fuentes
- C++ Core Guidelines - referencia amplia para ownership, resource management, interfaces y política de error handling. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines
- Google C++ Style Guide - subset real de codebase grande con bans y trade-offs explícitos, incluida política de exceptions. https://google.github.io/styleguide/cppguide.html
- LLVM Coding Standards - subset de proyecto systems que enfatiza portabilidad, no RTTI/exceptions y disciplina de tamaño de código. https://llvm.org/docs/CodingStandards.html
- cppreference -
std::span- detalles de view contigua non-owning y caveats de invalidation. https://en.cppreference.com/w/cpp/container/span - cppreference -
std::optional- vocabulary type para return paths de maybe-a-value. https://en.cppreference.com/w/cpp/utility/optional - cppreference -
std::variant- storage de alternativas closed-set y modelo de visitation. https://en.cppreference.com/w/cpp/utility/variant - cppreference - Constraints and concepts - requirements de templates públicos y overload constraints. https://en.cppreference.com/w/cpp/language/constraints
- Compiler Explorer - chequeá si una decisión de política cambió calls, branches, layout o tamaño de código. https://godbolt.org/