Conceptos de C++20:una introducción rápida

Conceptos de C++20:una introducción rápida

¡Los conceptos son un enfoque revolucionario para escribir plantillas! Le permiten imponer restricciones a los parámetros de la plantilla que mejoran la legibilidad del código, aceleran el tiempo de compilación y generan mejores mensajes de error.

¡Sigue leyendo y aprende a usarlos en tu código!

¿Qué es un concepto?

En resumen, un concepto es un conjunto de restricciones sobre los parámetros de la plantilla evaluados en tiempo de compilación. Puede usarlos para plantillas de clase y plantillas de función para controlar la sobrecarga de funciones y la especialización parcial.

C++20 nos brinda soporte de idiomas (nuevas palabras clave - requires , concept ) y un conjunto de conceptos predefinidos de la Biblioteca estándar.

En otras palabras, puede restringir los parámetros de la plantilla con una sintaxis "natural" y fácil. Antes de C++20, había varias formas de agregar tales restricciones. Vea mi otra publicación Simplifique el código con if constexpr y Concepts in C++17/C++20 - C++ Stories.

He aquí un ejemplo de un concepto simple:

template <class T>
concept integral = std::is_integral_v<T>;

El código anterior define el integral concepto. Como puede ver, se parece a otros template<> construcciones.

Este usa una condición que podemos calcular a través de un rasgo de tipo bien conocido (de C++11/C++14) - std::is_integral_v . Produce true o false dependiendo del parámetro de la plantilla de entrada.

También podemos definir otro usando un requires expresión:

template <typename T>
concept ILabel = requires(T v)
{
    {v.buildHtml()} -> std::convertible_to<std::string>;
};

¡Este parece un poco más serio! Pero después de un tiempo, parece "legible":

Definimos un concepto que requiere que un objeto de tipo T tenga una función miembro llamada buildHtml() , que devuelve algo convertible a std::string .

Esos dos ejemplos deberían darle una probada; intentemos usarlos en algún código real.

Cómo usar conceptos

En uno de los casos más comunes, para una plantilla de función pequeña, verá la siguiente sintaxis:

template <typename T>
requires CONDITION
void DoSomething(T param) { }

También puedes usar requires clause como la última parte de una declaración de función:

template <typename T>
void DoSomething(T param) requires CONDITION
{ 
    
}

La parte clave es el requires cláusula. Nos permite especificar varios requisitos en los parámetros de la plantilla de entrada.

Veamos una plantilla de función simple que calcula un promedio de un contenedor de entrada.

#include <numeric>
#include <vector>
#include <iostream>
#include <concepts>

template <typename T> 
requires std::integral<T> || std::floating_point<T>
constexpr double Average(std::vector<T> const &vec) {
    const double sum = std::accumulate(vec.begin(), vec.end(), 0.0);        
    return sum / vec.size();
}

int main() {
    std::vector ints { 1, 2, 3, 4, 5};
    std::cout << Average(ints) << '\n';                                      
}

Juega con el código @Compiler Explorer

Con el código fuente anterior, utilicé dos conceptos disponibles en la biblioteca estándar (std::integral y std::floating_point ) y los combinó.

Una ventaja:mejores errores de compilación

Si juegas con el ejemplo anterior y escribes:

std::vector strings {"abc", "xyz"};
auto test = Average(strings); 

Puede obtener:

<source>:23:24: error: no matching function for call to 'Average(std::vector<const char*, std::allocator<const char*> >&)'
   23 |     auto test = Average(strings);
      |                 ~~~~~~~^~~~~~~~~
<source>:10:18: note: candidate: 'template<class T>  requires (integral<T>) || (floating_point<T>) constexpr double Average(const std::vector<T>&)'
   10 | constexpr double Average(std::vector<T> const &vec) {
      |                  ^~~~~~~

¡Es muy agradable!

Puede ver que la instanciación de la plantilla falló porque su parámetro de plantilla - const char* no es un número entero ni un punto flotante.

Por lo general, con las plantillas, antes de la función de conceptos, puede recibir algunos mensajes crípticos largos sobre alguna operación fallida que no es posible en un tipo determinado en algún nivel profundo de la pila de llamadas.

Conceptos predefinidos

Aquí está la lista de conceptos predefinidos que obtenemos en C++20 con <concepts> encabezado:

Conceptos fundamentales del lenguaje Notas
same_as
derived_from
convertible_to
common_reference_with
common_with
integral
signed_integral
unsigned_integral
floating_point
assignable_from
swappable /swappable_with
destructible
constructible_from
default_initializable
move_constructible
copy_constructible
Conceptos de comparación Notas
boolean-testable se puede usar un tipo en casos de prueba booleanos
equality_comparable /equality_comparable_with
totally_ordered /totally_ordered_with Definido en <compare>
three_way_comparable /three_way_comparable_with
Conceptos de objetos Notas
movable
copyable
semiregular un tipo se puede copiar, mover, intercambiar y construir por defecto
regular un tipo es a la vez semiregular y equality_comparable
Conceptos invocables Notas
invocable /regular_invocable
predicate
relation especifica una relación binaria
equivalence_relation
strict_weak_order

Puede encontrar la lista aquí:Biblioteca de conceptos (C++20) - cppreference.com

Y aquí está mi publicación de blog separada sobre los conceptos invocables:

  • Conceptos predefinidos de C++20:Invocables - Historias de C++

Simplificación de código

Como puede ver, la sintaxis de los conceptos y las restricciones es relativamente sencilla, pero aún así, en C++20, ¡tenemos mucho más!

Hay varios accesos directos y sintaxis concisa que nos permiten hacer que el código de la plantilla sea súper simple.

Tenemos varias cosas:

  • Plantillas de funciones abreviadas
  • Automático restringido
  • Sintaxis concisa para conceptos

Por ejemplo:

template <typename T>
void print(const std::vector<T>& vec) {
    for (size_t i = 0; auto& elem : vec)
        std::cout << elem << (++i == vec.size() ? "\n" : ", ");
}

Podemos "comprimirlo" en:

void print2(const std::vector<auto>& vec) {
    for (size_t i = 0; auto& elem : vec)
        std::cout << elem << (++i == vec.size() ? "\n" : ", ");
}

En el caso anterior, utilicé auto sin restricciones . En general, puedes escribir:

auto func(auto param) { }

Y se expande en:

template <typename T>
auto func(T param) { }

Se parece a lo que obtenemos con C++14 y lambdas genéricas (Lambda Week:Going Generic).

Además, también podemos usar auto restringido :

void print3(const std::ranges::range auto& container) {
    for (size_t i = 0; auto && elem : container)
        std::cout << elem << (++i == container.size() ? "\n" : ", ");
};

Con print3 , eliminé la necesidad de pasar un vector y lo restringí para todos los rangos.

Juega con el código @Compiler Explorer

Aquí tenemos:

auto func(concept auto param) { }

Se traduce en:

template <typename T>
requires concept<T>
auto func(T param) { }

Además, en lugar de especificar template <typename T> requires... puedes escribir:

template <std::integral T>
auto sum(const std::vector<T>& vec) {
    // return ...;
}

El requires expresión

Uno de los elementos más poderosos con conceptos es el requires palabra clave. Tiene dos formas:

  • el requires cláusula - como requires std::integral<T> o similar
  • el requires expresión.

El último es muy flexible y permite especificar restricciones bastante avanzadas. En la introducción, ha visto un caso con una detección de buildHtml() función miembro. Aquí hay otro ejemplo:

template<typename T>
concept has_string_data_member = requires(T v) { 
    { v.name_ } -> std::convertible_to<std::string>; 
};

struct Person {
    int age_ { 0 };
    std::string name_;
};

struct Box {
    double weight_ { 0.0 };
    double volume_ { 0.0 };
};

int main() {
    static_assert(has_string_data_member<Person>);
    static_assert(!has_string_data_member<Box>);
}

Juega con el código @Compiler Explorer

Como puede ver arriba, podemos escribir requires(T v) , y de ahora en adelante, podemos pretender que tenemos un valor del tipo T , y luego podemos enumerar qué operaciones podemos usar.

Otro ejemplo:

template <typename T>
concept Clock = requires(T c) { 
    c.start();  
    c.stop();
    c.getTime();
  };

El concepto anterior restringe una "interfaz" para relojes básicos. Requerimos que tenga las tres funciones miembro, pero no especificamos qué tipo devuelven.

Desde una perspectiva, podemos decir que el requires expresión toma un tipo e intenta instanciar los requisitos especificados. Si falla, entonces una clase dada no cumple con este concepto. Es como SFINAE pero en una sintaxis amigable y fácil de expresar.

Acabo de mostrar algunos ejemplos básicos para darle una idea, pero mire este artículo de A. Krzemienski:Requires-expression | El blog de C++ de Andrzej que analiza este tema con mayor profundidad.

El modismo de detección actualizado

Gracias a Concepts ahora podemos detectar fácilmente una función, una función miembro o incluso una sobrecarga particular. Esto es mucho más sencillo que con las complicadas técnicas de SFINAE que teníamos antes.

Consulte mi otro artículo sobre ese tema:Cómo detectar sobrecargas de funciones en C++ 17/20, ejemplo std::from_chars - Historias de C++

Soporte del compilador

A partir de mayo de 2021, puede usar conceptos con todos los compiladores principales:GCC (desde 10.0), Clang (10.0) y MSVC (2019 16.3 soporte básico, 16.8 automático restringido, 16.9 plantillas de funciones abreviadas ver notas). Solo recuerde usar el indicador apropiado para el estándar C++ 20 - -std=c++20 /-std=c++2a para Clang/GCC, o /std:c++latest para MSVC.

Resumen

¡Es solo la punta de un iceberg!

Gracias a la introducción de dos nuevas palabras clave de idioma:requires y concept , puede especificar un requisito con nombre en un argumento de plantilla. Esto hace que el código sea mucho más legible y menos "pirateado" (como con las técnicas anteriores basadas en SFINAE...).

Además, la Biblioteca estándar está equipada con un conjunto de conceptos predefinidos (principalmente obtenidos de rasgos de tipo existentes), lo que facilita el inicio.

Además, C++20 ofrece aún más funciones de lenguaje para que la sintaxis sea aún más compacta. Se debe principalmente a un auto restringido. En algunos casos, ni siquiera necesitarás escribir template <> al frente de su plantilla de función!

Lo que me gusta de esta característica es que puede introducirla lentamente en su código. Puede agregar conceptos aquí y allá, experimentar, ver cómo funciona. Y luego use gradualmente construcciones más avanzadas y aplíquelas en otros lugares.

De vuelta a ti

¿Has probado los conceptos? ¿Cuáles son sus primeros pensamientos sobre esa función?

¿Cuáles son los casos de uso más importantes para ti?

Comparta sus comentarios debajo del artículo.

Referencias

  • Restricciones y conceptos (desde C++20) - cppreference.com
  • Programación con... por Andreas Fertig [Leanpub PDF/iPad/Kindle]
  • C++20 de Rainer Grimm [Leanpub PDF/iPad/Kindle]
  • Plantillas de funciones abreviadas y auto restringida | Blog del equipo de C++
  • Requiere-expresión | Blog de C++ de Andrzej