Constraints and concepts (since C++20)
Plantillas de clase , plantillas de función (incluyendo lambdas genéricas ), y otras funciones con plantilla (típicamente miembros de plantillas de clase) pueden estar asociadas con una restricción , que especifica los requisitos sobre los argumentos de plantilla, que pueden utilizarse para seleccionar las sobrecargas de función y especializaciones de plantilla más apropiadas.
Los conjuntos nombrados de tales requisitos se denominan concepts . Cada concepto es un predicado, evaluado en tiempo de compilación, y se convierte en parte de la interfaz de una plantilla donde se utiliza como restricción:
#include <cstddef> #include <concepts> #include <functional> #include <string> // Declaración del concepto "Hashable", que es satisfecho por cualquier tipo "T" // tal que para valores "a" de tipo "T", la expresión std::hash<T>{}(a) // compila y su resultado es convertible a std::size_t template<typename T> concept Hashable = requires(T a) { { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>; }; struct meow {}; // Plantilla de función restringida de C++20: template<Hashable T> void f(T) {} // // Formas alternativas de aplicar la misma restricción: // template<typename T> // requires Hashable<T> // void f(T) {} // // template<typename T> // void f(T) requires Hashable<T> {} // // void f(Hashable auto /* nombre-del-parámetro */) {} int main() { using std::operator""s; f("abc"s); // OK, std::string satisface Hashable // f(meow{}); // Error: meow no satisface Hashable }
Las violaciones de restricciones se detectan en tiempo de compilación, al inicio del proceso de instanciación de plantillas, lo que genera mensajes de error fáciles de seguir:
std::list<int> l = {3, -1, 10}; std::sort(l.begin(), l.end()); // Diagnóstico típico del compilador sin conceptos: // invalid operands to binary expression ('std::_List_iterator<int>' and // 'std::_List_iterator<int>') // std::__lg(__last - __first) * 2); // ~~~~~~ ^ ~~~~~~~ // ... 50 líneas de salida ... // // Diagnóstico típico del compilador con conceptos: // error: cannot call std::sort with std::_List_iterator<int> // note: concept RandomAccessIterator<std::_List_iterator<int>> was not satisfied
La intención de los concepts es modelar categorías semánticas (Number, Range, RegularFunction) en lugar de restricciones sintácticas (HasPlus, Array). Según la directriz central T.20 de ISO C++ , "La capacidad de especificar semántica significativa es una característica definitoria de un verdadero concept, a diferencia de una restricción sintáctica."
Contenidos |
Conceptos
Un concepto es un conjunto nombrado de requisitos . La definición de un concepto debe aparecer en el ámbito del espacio de nombres.
La definición de un concepto tiene la forma
template <
lista-de-parámetros-de-plantilla
>
|
|||||||||
| attr | - | secuencia de cualquier número de attributes |
// concepto template<class T, class U> concept Derived = std::is_base_of<U, T>::value;
Los conceptos no pueden referirse recursivamente a sí mismos y no pueden estar restringidos:
template<typename T> concept V = V<T*>; // error: concepto recursivo template<class T> concept C1 = true; template<C1 T> concept Error1 = true; // Error: C1 T intenta restringir una definición de concepto template<class T> requires C1<T> concept Error2 = true; // Error: la cláusula requires intenta restringir un concepto
Las instanciaciones explícitas, las especializaciones explícitas o las especializaciones parciales de conceptos no están permitidas (no se puede cambiar el significado de la definición original de una restricción).
Los conceptos pueden ser nombrados en una expresión de identificación. El valor de la expresión de identificación es true si la expresión de restricción se satisface, y false en caso contrario.
Los conceptos también pueden nombrarse en una restricción de tipo, como parte de
- declaración de parámetro de plantilla de tipo ,
- especificador de tipo de marcador de posición ,
- requisito compuesto .
En una type-constraint , un concepto toma un argumento de plantilla menos de lo que exige su lista de parámetros, porque el tipo deducido contextualmente se utiliza implícitamente como el primer argumento del concepto.
template<class T, class U> concept Derived = std::is_base_of<U, T>::value; template<Derived<Base> T> void f(T); // T está restringido por Derived<T, Base>
Restricciones
Una restricción es una secuencia de operaciones lógicas y operandos que especifica requisitos sobre los argumentos de plantilla. Pueden aparecer dentro de requires expresiones o directamente como cuerpos de conceptos.
Hay tres (until C++26) cuatro (since C++26) tipos de constraints:
|
4)
plegar restricciones expandidas
|
(since C++26) |
La restricción asociada con una declaración se determina normalizando una expresión lógica AND cuyos operandos están en el siguiente orden:
- la expresión de restricción introducida para cada parámetro de plantilla de tipo restringido o parámetro de plantilla constante declarado con un tipo de marcador de posición restringido , en orden de aparición;
- la expresión de restricción en la requires cláusula después de la lista de parámetros de plantilla;
- la expresión de restricción introducida para cada parámetro con tipo de marcador de posición restringido en una declaración de plantilla de función abreviada ;
- la expresión de restricción en la requires cláusula final .
Este orden determina el orden en el que se instancian las restricciones al verificar su satisfacción.
Redeclaraciones
Una declaración restringida solo puede ser redeclarada usando la misma forma sintáctica. No se requiere diagnóstico:
// Estas dos primeras declaraciones de f son correctas template<Incrementable T> void f(T) requires Decrementable<T>; template<Incrementable T> void f(T) requires Decrementable<T>; // OK, redeclaración // La inclusión de esta tercera declaración, lógicamente equivalente pero sintácticamente diferente // de f está mal formada, no se requiere diagnóstico template<typename T> requires Incrementable<T> && Decrementable<T> void f(T); // Las siguientes dos declaraciones tienen restricciones diferentes: // la primera declaración tiene Incrementable<T> && Decrementable<T> // la segunda declaración tiene Decrementable<T> && Incrementable<T> // Aunque sean lógicamente equivalentes. template<Incrementable T> void g(T) requires Decrementable<T>; template<Decrementable T> void g(T) requires Incrementable<T>; // mal formada, no se requiere diagnóstico
Conjunciones
La conjunción de dos restricciones se forma utilizando el
&&
operador en la expresión de restricción:
template<class T> concept Integral = std::is_integral<T>::value; template<class T> concept SignedIntegral = Integral<T> && std::is_signed<T>::value; template<class T> concept UnsignedIntegral = Integral<T> && !SignedIntegral<T>;
Una conjunción de dos restricciones se satisface solo si ambas restricciones se satisfacen. Las conjunciones se evalúan de izquierda a derecha y tienen cortocircuito (si la restricción izquierda no se satisface, la sustitución de argumentos de plantilla en la restricción derecha no se intenta: esto evita fallos debidos a la sustitución fuera del contexto inmediato).
template<typename T> constexpr bool get_value() { return T::value; } template<typename T> requires (sizeof(T) > 1 && get_value<T>()) void f(T); // #1 void f(int); // #2 void g() { f('A'); // OK, llama a #2. Al verificar las restricciones de #1, // 'sizeof(char) > 1' no se satisface, por lo que get_value<T>() no se verifica }
Disyunciones
La disyunción de dos restricciones se forma utilizando el
||
operador en la expresión de restricción.
Una disyunción de dos restricciones se satisface si cualquiera de las restricciones se satisface. Las disyunciones se evalúan de izquierda a derecha y tienen cortocircuito (si la restricción izquierda se satisface, no se intenta la sustitución de argumentos de plantilla en la restricción derecha).
template<class T = void> requires EqualityComparable<T> || Same<T, void> struct equal_to;
Restricciones atómicas
Una restricción atómica consiste en una expresión E y un mapeo de los parámetros de plantilla que aparecen dentro de E a argumentos de plantilla que involucran los parámetros de plantilla de la entidad restringida, llamado su mapeo de parámetros .
Las restricciones atómicas se forman durante la normalización de restricciones . E nunca es una expresión AND lógico u OR lógico (esas forman conjunciones y disyunciones, respectivamente).
La satisfacción de una restricción atómica se verifica sustituyendo el mapeo de parámetros y los argumentos de plantilla en la expresión E . Si la sustitución resulta en un tipo o expresión inválida, la restricción no se satisface. De lo contrario, E , después de cualquier conversión de lvalue-a-rvalue, debe ser una expresión constante prvalue de tipo bool , y la restricción se satisface si y solo si se evalúa como true .
El tipo de E después de la sustitución debe ser exactamente bool . No se permite ninguna conversión:
template<typename T> struct S { constexpr operator bool() const { return true; } }; template<typename T> requires (S<T>{}) void f(T); // #1 void f(int); // #2 void g() { f(0); // error: S<int>{} no tiene tipo bool al verificar #1, // aunque #2 es una coincidencia mejor }
Dos restricciones atómicas se consideran idénticas si están formadas a partir de la misma expresión a nivel de código fuente y sus mapeos de parámetros son equivalentes.
template<class T> constexpr bool is_meowable = true; template<class T> constexpr bool is_cat = true; template<class T> concept Meowable = is_meowable<T>; template<class T> concept BadMeowableCat = is_meowable<T> && is_cat<T>; template<class T> concept GoodMeowableCat = Meowable<T> && is_cat<T>; template<Meowable T> void f1(T); // #1 template<BadMeowableCat T> void f1(T); // #2 template<Meowable T> void f2(T); // #3 template<GoodMeowableCat T> void f2(T); // #4 void g() { f1(0); // error, ambiguo: // el is_meowable<T> en Meowable y BadMeowableCat forma átomos distintos // que no son idénticos (y por lo tanto no se subsumen entre sí) f2(0); // OK, llama a #4, más restringido que #3 // GoodMeowableCat obtuvo su is_meowable<T> de Meowable }
Restricciones expandidas por plegado
Una
restricción expandida por plegado
se forma a partir de una restricción
Sea N el número de elementos en los parámetros de expansión del paquete:
template <class T> concept A = std::is_move_constructible_v<T>; template <class T> concept B = std::is_copy_constructible_v<T>; template <class T> concept C = A<T> && B<T>; // en C++23, estas dos sobrecargas de g() tienen restricciones atómicas distintas // que no son idénticas y por lo tanto no se subsumen entre sí: las llamadas a g() son ambiguas // en C++26, los plegados se expanden y la restricción en la sobrecarga #2 (requiere tanto move como copy) // subsume la restricción en la sobrecarga #1 (solo requiere move) template <class... T> requires (A<T> && ...) void g(T...); // #1 template <class... T> requires (C<T> && ...) void g(T...); // #2
|
(desde C++26) |
Normalización de restricciones
Normalización de restricciones es el proceso que transforma una expresión de restricción en una secuencia de conjunciones y disjunciones de restricciones atómicas. La forma normal de una expresión se define de la siguiente manera:
- La forma normal de una expresión ( E ) es la forma normal de E .
- La forma normal de una expresión E1 && E2 es la conjunción de las formas normales de E1 y E2 .
- La forma normal de una expresión E1 || E2 es la disyunción de las formas normales de E1 y E2 .
-
La forma normal de una expresión
C
<
A1, A2, ... , AN
>
, donde
Cnombra un concepto, es la forma normal de la expresión de restricción deC, después de sustituirA1,A2, ... ,ANpor los parámetros de plantilla respectivos deCen los mapeos de parámetros de cada restricción atómica deC. Si cualquier sustitución en los mapeos de parámetros resulta en un tipo o expresión inválido, el programa está mal formado, sin requerir diagnóstico.
template<typename T> concept A = T::value || true; template<typename U> concept B = A<U*>; // OK: normalizado a la disyunción de // - T::value (con mapeo T -> U*) y // - true (con un mapeo vacío). // Ningún tipo inválido en el mapeo aunque // T::value está mal formado para todos los tipos puntero template<typename V> concept C = B<V&>; // Normaliza a la disyunción de // - T::value (con mapeo T-> V&*) y // - true (con un mapeo vacío). // Tipo inválido V&* formado en el mapeo => mal formado NDR
|
(desde C++26) |
-
La forma normal de cualquier otra expresión
E
es la restricción atómica cuya expresión es
E
y cuyo mapeo de parámetros es el mapeo identidad. Esto incluye todas las
expresiones de plegado
, incluso aquellas que pliegan sobre los operadores
&&o||.
Las sobrecargas definidas por el usuario de
&&
o
||
no tienen efecto en la normalización de restricciones.
requires cláusulas
La palabra clave requires se utiliza para introducir una requires cláusula , que especifica restricciones sobre argumentos de plantilla o en una declaración de función.
template<typename T> void f(T&&) requires Eq<T>; // puede aparecer como el último elemento de un declarador de función template<typename T> requires Addable<T> // o justo después de una lista de parámetros de plantilla T add(T a, T b) { return a + b; }
En este caso, la palabra clave requires debe ir seguida de alguna expresión constante (por lo que es posible escribir requires true ), pero la intención es que se utilice un concepto nombrado (como en el ejemplo anterior) o una conjunción/disyunción de conceptos nombrados o una expresión requires .
La expresión debe tener una de las siguientes formas:
- Una expresión primaria , por ejemplo Swappable < T > , std:: is_integral < T > :: value , ( std:: is_object_v < Args > && ... ) , o cualquier expresión entre paréntesis.
-
Una secuencia de expresiones primarias unidas con el operador
&&. -
Una secuencia de las expresiones mencionadas anteriormente unidas con el operador
||.
template<class T> constexpr bool is_meowable = true; template<class T> constexpr bool is_purrable() { return true; } template<class T> void f(T) requires is_meowable<T>; // CORRECTO template<class T> void g(T) requires is_purrable<T>(); // error, is_purrable<T>() no es una expresión primaria template<class T> void h(T) requires (is_purrable<T>()); // CORRECTO
Ordenamiento parcial de restricciones
Antes de cualquier análisis adicional, las restricciones son normalizadas mediante la sustitución del cuerpo de cada concepto nombrado y cada requires expression hasta que lo que queda es una secuencia de conjunciones y disyunciones sobre restricciones atómicas.
Se dice que una restricción
P
subsume
a la restricción
Q
si se puede demostrar que
P
implica
Q
hasta la identidad de las restricciones atómicas en P y Q. (Los tipos y expresiones no se analizan para equivalencia:
N > 0
no subsume
N >= 0
).
Específicamente, primero
P
se convierte a forma normal disyuntiva y
Q
se convierte a forma normal conjuntiva.
P
subsume a
Q
si y solo si:
-
cada cláusula disyuntiva en la forma normal disyuntiva de
Psubsume cada cláusula conjuntiva en la forma normal conjuntiva deQ, donde -
una cláusula disyuntiva subsume una cláusula conjuntiva si y solo si existe una restricción atómica
Uen la cláusula disyuntiva y una restricción atómicaVen la cláusula conjuntiva tal queUsubsumeV; -
una restricción atómica
Asubsume una restricción atómicaBsi y solo si son idénticas usando las reglas descritas arriba .
|
(desde C++26) |
La relación de subsunción define un orden parcial de restricciones, que se utiliza para determinar:
- el mejor candidato viable para una función no plantilla en resolución de sobrecarga
- la dirección de una función no plantilla en un conjunto de sobrecarga
- la mejor coincidencia para un argumento de plantilla de plantilla
- ordenación parcial de especializaciones de plantillas de clase
- ordenación parcial de plantillas de función
|
Esta sección está incompleta
Razón: enlaces de retroceso desde arriba hacia aquí |
Si las declaraciones
D1
y
D2
están restringidas y las restricciones asociadas de
D1
subsumen las restricciones asociadas de
D2
(o si
D2
no está restringida), entonces se dice que
D1
es
al menos tan restringida
como
D2
. Si
D1
es al menos tan restringida como
D2
, y
D2
no es al menos tan restringida como
D1
, entonces
D1
es
más restringida
que
D2
.
Si se cumplen todas las siguientes condiciones, una función no plantilla
F1
es
más restringida por ordenamiento-parcial
que una función no plantilla
F2
:
- Tienen la misma lista de tipos de parámetros , omitiendo los tipos de explicit object parameters (since C++23) .
- Si son funciones miembro, ambas son miembros directos de la misma clase.
- Si ambas son funciones miembro no estáticas, tienen los mismos tipos para sus parámetros de objeto.
-
F1está más restringida queF2.
template<typename T> concept Decrementable = requires(T t) { --t; }; template<typename T> concept RevIterator = Decrementable<T> && requires(T t) { *t; }; // RevIterator subsume a Decrementable, pero no al revés template<Decrementable T> void f(T); // #1 template<RevIterator T> void f(T); // #2, más restringida que #1 f(0); // int solo satisface Decrementable, selecciona #1 f((int*)0); // int* satisface ambas restricciones, selecciona #2 por ser más restringida template<class T> void g(T); // #3 (sin restricciones) template<Decrementable T> void g(T); // #4 g(true); // bool no satisface Decrementable, selecciona #3 g(0); // int satisface Decrementable, selecciona #4 por ser más restringida template<typename T> concept RevIterator2 = requires(T t) { --t; *t; }; template<Decrementable T> void h(T); // #5 template<RevIterator2 T> void h(T); // #6 h((int*)0); // ambiguo
Notas
| Macro de prueba de características | Valor | Std | Característica |
|---|---|---|---|
__cpp_concepts
|
201907L
|
(C++20) | Restricciones |
202002L
|
(C++20) | Funciones miembro especiales condicionalmente triviales |
Palabras clave
Informes de defectos
Los siguientes informes de defectos que modifican el comportamiento se aplicaron retroactivamente a los estándares publicados anteriormente de C++.
| DR | Aplicado a | Comportamiento publicado | Comportamiento correcto |
|---|---|---|---|
| CWG 2428 | C++20 | no se podían aplicar atributos a conceptos | permitido |
Véase también
| Expresión requires (C++20) | produce una expresión prvalue de tipo bool que describe las restricciones |