Always auto: Una sintaxis moderna para C++

Artículos de la serie:

Introducción


En sus más de treinta años de evolución, C++ ha adquirido estilos diversos con los que inicializar sus variables, hasta el punto de no existir un consenso definitivo en su comunidad de desarrolladores acerca de cuál resulta preferible. Sin embargo, tras los últimos estándares del lenguaje (particularmente C++17) podemos observar una clara tendencia a adoptar una sintaxis declarativa de izquierda-a-derecha, en sintonía con lenguajes más recientes como Go, Rust o Swift.

Así sucede, en efecto, al definir un concepto (C++20):

   template<typename I, typename S, typename Comp>    concept Insertion_sortable = std::bidirectional_iterator<I>                                 and std::movable<std::iter_value_t<I>>                                 and std::sentinel_for<S, I>                                and std::strict_weak_order<Comp, std::iter_value_t<I>,   std::iter_value_t<I>>;  

un alias para un tipo:

using Phone_book = std::map<std::string, std::vector<int>>;

o al indicar el tipo de retorno de una función mediante el especificador auto:

auto read_data(std::string_view file_name) -> Phone_book { /* ... */ }

Notemos, en todos los casos anteriores, cómo el flujo de lectura se produce de forma natural desde el margen izquierdo --donde figura el nombre del concepto Floating_point, del alias Phone_book o de la función read_data-- hacia el derecho --donde encontramos la definición del concepto, el tipo original para el que definimos el alias y el tipo retornado por la función, respectivamente.


Estilo 'always auto'


Herb Sutter (convener del comité ISO WG21 de estandarización del lenguaje) identificó en su post GotW#94 la emergencia de un estilo declarativo moderno y de aplicación general para C++ de la forma

categoría nombre = tipo y/o inicializador

donde la categoría es sustituida por autousing, etcétera, dependiendo del contexto. Para más información, puedes consultar la referencia [1].

En el caso específico de la introducción de variables, C++17 permite inicializarlas sin pérdida alguna de rendimiento siguiendo el esquema anterior, en el que distinguimos dos áreas bien diferenciadas:
  • A la izquierda del signo igual, situaremos el nombre de la variable, precedido de la palabra clave auto y, si fuese necesario, de una lista de calificadores/especificadores adicionales (const, constexpr, static, thread_local, etcétera).
  • A la derecha del signo igual, indicaremos el tipo de la variable (siempre que no deseemos su inferencia automática a través de auto) y, entre llaves {}, sus valores de inicialización.
Así, podemos escribir:

   auto option = char{'y'};    auto counter = int{0};    auto width = double{3.4};    auto height = float{1.2f};    auto name = std::string{"Edmond Dantes"};    auto const numbers = std::array<int5>{12345};

Los casos anteriores pueden simplificarse aún más, sin dejar lugar para la ambigüedad, si nos servimos de la inferencia automática de tipos, los sufijos estandarizados y la técnica CTAD (class template argument deduction, C++17):

   auto option = 'y';  // char    auto counter = 0;   // int    auto width = 3.4;   // double    auto height = 1.2f; // float    using namespace std::literals;    auto name = "Edmond Dantes"s; // std::string (char const* si suprimimos el sufijo s)    auto const numbers = std::array{12345}; // CTAD: std::array<int, 5> const

En particular, el empleo de sufijos o alias resulta necesario al trabajar con tipos de nombre compuesto:

   auto m = unsigned long long{1'671'987}; // inválido    auto n = 1'671'987ull;                  // OK: n es unsigned long long    auto p = int*{&n}; // sintaxis inválida para punteros    template<typename T> using ptr = T*;    auto q = ptr<int>{&n}; // OK: q es de tipo int*

Con carácter general, deberíamos confiar en la inferencia automática siempre que el tipo de la variable sea evidente por el contexto. Por ejemplo:

   auto i = 0; auto p = &i; // p es de tipo int*    auto q = new int{7};  // q es de tipo int*    auto r = std::make_unique<Dinosaur>("TRex"); // r es de tipo std::unique_ptr<Dinosaur>    auto v = std::vector<int>{}; auto sz = v.size(); // sz de tipo std::vector<int>::size_type


Ventajas de la nueva sintaxis


Al adoptar el estilo 'always auto' no nos movemos únicamente por criterios de consistencia entre distintos tipos de declaraciones. Es importante resaltar que esta notación evita la introducción de variables locales sin inicializar, pues expresiones como las siguientes están prohibidas:

   auto i; // error de compilación    auto j = int; // error de compilación

Esta propiedad resulta crítica a la hora de producir software correcto, pues una operación de lectura sobre una variable no inicializada conduce a comportamiento indefinido (undefined behavior).

Asimismo, podemos ahora evitar un problema de parseo recurrente conocido como the most vexing parse:

   S s(); // ¡declaración de función!

La expresión anterior no define una variable de tipo S construida por defecto, sino que el compilador la interpreta como la declaración de una función sin parámetros de nombre s que retorna un objeto de tipo S. La nueva sintaxis nos protege de incidencias de este tipo:

   auto s = S{}; // OK: objeto inicializado por defecto

Es importante resaltar que, a partir de C++17, el compilador elude tanto la creación de un temporal como la realización de una operación posterior de movimiento al inicializar variables a través de prvalues. Ello permite emplear esta técnica incluso con objetos de clases cuyos constructores de movimiento estén declarados como delete, o en casos donde el movimiento del objeto hubiese resultado costoso (por ejemplo un array):

   auto mtx = std::mutex{}; // OK, aunque std::mutex no sea movible    void thread_safe_op(std::vector<int>& v)    {       auto lck = std::lock_guard{mtx}; // OK, aunque std::lock_guard no sea movible       // ...    }

Finalmente, conviene señalar que el nuevo estilo puede facilitar el aprendizaje a aquellos programadores principiantes en C++ que, sin embargo, cuenten con experiencia previa con lenguajes más recientes como Go, Rust o Swift.


Referencias bibliográficas:

[1] https://herbsutter.com/2013/08/12/gotw-94-solution-aaa-style-almost-always-auto/
[2] https://www.fluentcpp.com/2018/09/28/auto-stick-changing-style/

No hay comentarios:

Publicar un comentario