C++20: Nuevas características del lenguaje

C++ se encuentra en constante evolución. El primer proceso de estandarización internacional del lenguaje (ISO C++ 1998-2003) permitió la incorporación de técnicas de programación genérica y, con ellas, el desarrollo de una biblioteca estándar en la que algoritmos, estructuras de datos e iteradores se interrelacionaban de forma natural y eficiente.

Los estándares posteriores C++11/14/17 han proporcionado mejoras sustanciales al núcleo del lenguaje (inferencia automática de tipos, semántica de movimiento, inicialización uniforme, expresiones constantes, funciones lambda, etcétera), así como una notable extensión de la biblioteca estándar mediante la inclusión de punteros inteligentes, concurrencia, sistema de ficheros, expresiones regulares, tipos variantes y un largo etcétera.

El comité de estandarización se ha reunido recientemente en Kona (Hawaii, USA, Feb 18-23 2019) para concluir la especificación de C++20 [1]. El nuevo estándar promete revolucionar el modo en que pensamos y expresamos nuestro código, a la par que permanece fiel a los principios fundacionales de C++ (eficiencia, empleo de abstracciones de sobrecoste cero, retrocompatibilidad). Este post ofrece un breve adelanto de algunas de las principales funcionalidades incluidas en esta nueva versión del lenguaje:
  1. Módulos.
  2. Conceptos.
  3. Corrutinas.
  4. Contratos [Nota del autor: Esta funcionalidad fue retirada del estándar en el encuentro de Cologne en julio de 2019 para su reevaluación por parte del subgrupo de trabajo SG21].
  5. Rangos.


1. Módulos

Al igual que C, C++ permite acceder a la API de una biblioteca a través de directivas de inclusión #include gestionadas por el precompilador. Este modelo, que ha permanecido vigente durante tres décadas, resulta sin duda versátil, pero presenta importantes desventajas, como su falta de escalabilidad en tiempo de compilación y su fragilidad ante la definición de macros externas. La inclusión de módulos proporciona una semántica más robusta y eficiente que redefine el modo en que estructuramos nuestras interfaces e implementaciones, impide la exportación de macros y disminuye (en muchos casos de forma drástica) los tiempos de compilación de nuestros proyectos. Compañías tecnológicas de primer orden como Facebook, Google y Microsoft han participado activamente en la puesta a punto de este nuevo modelo, sobre el que puede encontrarse más información en la referencia [2].

Así, frente al empleo tradicional de ficheros de cabecera y de implementación, la gestión de unidades de traducción tomará ahora, típicamente, una forma similar a la del siguiente ejemplo:

   // unidad de traducción #1:    export module Math;     export template<typename T> auto square(T i) { return i*i; }

   // unidad de traducción #2:    import Math;     auto main() -> int { return square(9); }

A la espera de que la biblioteca estándar pueda ser sometida a una reorganización completa en módulos [3], C++20 permitirá que bibliotecas de cabecera actuales como <vector> o <string> puedan ser importadas como header units. Así, el programa clásico 'Hello world!' se reescribirá en la forma siguiente:

   import <iostream>;    auto main() -> int {       std::cout << "Hello world!";    }

Los header units brindan un beneficio en tiempo de compilación análogo al ofrecido por los ficheros de cabecera precompilados PCH. A diferencia de un PCH, sin embargo, al modificar un header unit sólo éste y sus dependencias deberán reconstruirse.


2. Conceptos

Tras más de dos décadas de investigación y puesta a punto, C++ ofrecerá la posibilidad de restringir los parámetros de plantillas de clase y de función a través de expresiones requires [4]. Hasta ahora, las condiciones a verificar por dichos parámetros debían indicarse en la documentación de los proyectos o, de ser posible, mediante aserciones estáticas static_assert. Gracias al empleo de conceptos, será posible especificar categorías semánticas como hashable, random_access_iterator o sortable para restringir el tipo de datos con los que nuestras clases genéricas y algoritmos deban operar. Estas ligaduras pueden también utilizarse para restringir la inferencia automática de tipos en las declaraciones de variables.

Los conceptos se implementan en forma de predicados evaluados en tiempo de compilación. En el siguiente ejemplo de polimorfismo estático, una clase satisfará el concepto Vehicle siempre que implemente las funciones miembro públicas de signatura string model() const y double speed() const. Tal es el caso de la clase Car, pero no así de Comet, tal y como se comprueba en tiempo de compilación al intentar invocar en ambos casos la función genérica print_vehicle_speed(), que se encuentra restringida para operar únicamente con clases que cumplan el concepto Vehicle:

   template<class T> concept Vehicle = requires (T const& vhc) {       { vhc.model() } -> std::same_as<std::string>;       { vhc.speed() } -> std::same_as<double>;     };     class Car {        std::string model_;        double speed_; // km por hora    public:        Car(std::string model, double speed) : model_{std::move(model)}, speed_{speed} { }        auto model() const { return model_; }        auto speed() const { return speed_; }     };        class Comet {        double speed_;     public:        Comet(double speed) : speed_{speed} { }        auto speed() const { return speed_; }     };     // plantilla de función con ligadura (sintaxis simplificada):    void print_vehicle_speed(Vehicle auto const& v) { /* ... */ };    auto main() -> int {        auto const v_1 = Car{"ABC"60.8};        print_vehicle_speed(v_1);      // OK, v_1 satisface el concepto Vehicle        auto const v_2 = Comet{29317.5};        print_vehicle_speed(v_2);      // error de compilación: v_2 no es Vehicle     } 

El lector puede encontrar información más detallada acerca de conceptos en este post reciente.


3. Corrutinas

Una corrutina constituye una generalización del concepto de subrutina que permite la suspensión de su ejecución, pudiendo retomarse posteriormente [5, 6]. Un ejemplo clásico viene dado por el siguiente generador de números enteros, que suspende temporalmente su ejecución en cada iteración de su bucle for  para retornar el entero recién calculado:

   auto int_generator(int first, int last) -> my_generator<int> {       for (auto current = first; current < last; ++current)           co_yield current;    }    // ...    for (auto const i : int_generator(15))        std::cout << i << " "; // output: 1 2 3 4 

El autor principal de esta propuesta, Gor Nishanov (Microsoft), analiza en la referencia [7] el impacto de las corrutinas en varias áreas del lenguaje. En el caso de código asíncrono, el empleo de corrutinas debiera simplificar en gran medida la programación de redes en conjunción con la especificación técnica Networking TS.


4. Contratos [Nota del autor: Esta funcionalidad fue retirada del estándar en el encuentro de Cologne en julio de 2019 para su reevaluación por parte del subgrupo de trabajo SG21]

Un contrato es un conjunto de precondiciones, postcondiciones y aserciones asociadas a una función [8]. En concreto:
  • Las precondiciones establecen los requisitos a verificar por los argumentos de una función (y/o el estado de otros objetos) en el punto de entrada de su ejecución.
  • Las postcondiciones fijan los requisitos a cumplir por los valores retornados por la función (y/o el estado de otros objetos) al finalizar su ejecución.
  • Las aserciones, por su parte, son predicados que deben satisfacerse en puntos específicos del cuerpo de la función.
Puede encontrarse una introducción a la programación con contratos en el seminario [9] impartido por uno de los autores principales de esta propuesta, el profesor J. Daniel García (Universidad Carlos III de Madrid). Como primer ejemplo, la siguiente función establece como precondición que el puntero pasado como argumento no sea nulo:

   void print(Dvd const* p) [[ pre: p != nullptr ]] {       std::cout << p->title << ", " << p->price;    }

Como segundo ejemplo, la siguiente función para el cálculo de raíces cuadradas reales requiere un argumento positivo o nulo y garantiza la obtención de un valor, a su vez, positivo o nulo:

   auto sqrt(double x) -> double        [[ pre: x >= 0.0 ]]        [[ post result: result >= 0.0 ]]; 


5. Rangos

Esta extensión del lenguaje, inspirada en la reconocida biblioteca Range-v3 de Eric Niebler (Facebook), proporciona una gran variedad de componentes para el tratamiento de rangos de elementos, incluyendo adaptadores views para el filtrado o la transformación de valores [10].

Esta propuesta simplifica enormemente el empleo de algoritmos STL y su composición. Por ejemplo, el siguiente código ordena de mayor a menor los enteros contenidos en un vector para, a continuación, filtrar aquellos valores que sean pares e imprimirlos en pantalla multiplicados por dos. El estilo de programación adoptado es, como puede observarse, altamente funcional:

   using namespace std;    auto integers = vector{3165247};    ranges::sort(integers, greater{}); // 7 6 5 4 3 2 1    auto is_even = [](auto i){ return i%2 == 0; };    auto times_2 = [](auto i){ return i*2; };    for (auto const i : integers | views::filter(is_even) | views::transform(times_2))       cout << i << ' '; // output: 12 8 4


Referencias bibliográficas:
  1. https://www.reddit.com/r/cpp/comments/au0c4x/201902_kona_iso_c_committee_trip_report_c20/
  2. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1103r2.pdf
  3. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1453r0.html
  4. https://en.cppreference.com/w/cpp/language/constraints
  5. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/n4736.pdf
  6. https://lewissbaker.github.io/2017/11/17/understanding-operator-co-await
  7. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0975r0.html
  8. https://en.cppreference.com/w/cpp/language/attributes/contract
  9. https://www.arcos.inf.uc3m.es/jdgarcia/2017/05/04/contracts-programming-after-c17/
  10. https://en.cppreference.com/w/cpp/experimental/ranges

No hay comentarios:

Publicar un comentario