conceptC++ Moderno~5 min de lecturaActualizado 2026-07-02#cpp#style#systems#guidelines#subset

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 new y 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_ptr para ownership exclusivo de heap, std::shared_ptr solo cuando lifetime compartido es el modelo real.
  • Views: std::span<T>, std::span<const T> y std::string_view para 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 expected en bordes de sistema, parsers, allocators, drivers y no-exceptions.
  • Generic code: concepts en templates públicos, if constexpr para ramas type-dependent y headers chicos que no instancien un universo.
  • Containers: std::array, std::vector y 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, vector o 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/