Restricciones y conceptos (desde C++20)
- Esta página describe la característica principal del lenguaje adoptada para C++20. Para los tipos de requerimientos denominados usados en la especificación de la biblioteca estándar, véase requerimientos denominados. Para la versión de la Especificación Técnica de Conceptos de esta característica, véase aquí.
Las plantillas de clase, plantillas de función, y funciones que no son de plantilla (típicamente miembros de plantillas de clase) pueden estar asociadas con una restricción que especifica los requerimientos impuestos sobre los argumentos de plantilla, que pueden usarse para seleccionar las sobrecargas de función y las especializaciones de plantilla más apropriadas.
A los conjuntos denominados de tales requerimientos se les llama conceptos. Cada concepto es un predicado, evaluado en tiempo de compilación, y se vuelve parte de la interfaz de una plantilla donde se usa como una restricción:
#include <string> #include <cstddef> #include <concepts> using namespace std::literals; // Declaración del concepto "Hashable", que se satisface por // cualquier tipo 'T' de tal manera 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 miau {}; template<Hashable T> void f(T); // plantilla de función de C++20 restringida // Maneras alternas de aplicar la misma restricción: // template<typename T> // requires Hashable<T> // void f(T); // // template<typename T> // void f(T) requires Hashable<T>; int main() { f("abc"s); // de acuerdo, std::string satisface a Hashable f(miau{}); // ERROR: miau no satisface a Hashable }
Las violaciones de las restricciones se detectan en tiempo de compilación, pronto durante el proceso de instanciación de la plantilla, lo que conlleva a mensajes de error fáciles de comprender.
std::list<int> l = {3,-1,10}; std::sort(l.begin(), l.end()); //Diagnóstico típico de un compilador sin conceptos: // operandos inválidos para la expresión binaria ('std::_List_iterator<int>' y // 'std::_List_iterator<int>') // std::__lg(__last - __first) * 2); // ~~~~~~ ^ ~~~~~~~ // ... 50 líneas de salida ... // //Diagnóstico típico de un compilador con conceptos: // error: no se puede llamar a std::sort con std::_List_iterator<int> // nota: el concepto RandomAccessIterator<std::_List_iterator<int>> no se satisfizo
El propósito de los conceptos es modelar categorías semánticas (Number, Range, RegularFunction) en lugar de restricciones sintácticas (HasPlus, Array). De acuerdo a la guía principal ISO C++ T.20, "La habilidad de especificar una semántica significativa es una característica determinante de un verdadero concepto, a diferencia de una restricción sintáctica."
Contenido |
[editar] Conceptos
Un concepto es un conjunto denominado de requerimientos. La definición de un concepto debe aparecer en el ámbito de un espacio de nombres.
La definición de un concepto tiene la forma
template < lista-de-parámetros-de-plantilla >
|
|||||||||
// concepto template <class T, class U> concept Derived = std::is_base_of<U, T>::value;
Los conceptos no pueden referirse a sí mismos recursivamente y no pueden restringirse:
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
No se permiten instanciaciones explícitas, especializaciones explícitas o especializaciones parciales de conceptos (el significado de la definición original de una restricción no se puede cambiar).
Los conceptos pueden denominarse en una expresión-id. El valor de la expresión-id es true si la expresión de restricción se satisface; de lo contrario, false. Los conceptos también pueden denominarse en una restricción-de-tipo, como parte de
- una declaración de parámetro de plantilla de tipo;
- un especificador de tipo de marcador de posición; y
- un requerimiento compuesto.
[editar] Restricciones
Una restricción es una secuencia de operaciones lógicas y operandos que especifica los requerimientos impuestos en los argumentos de plantilla. Pueden aparecer dentro de "expresiones-requires" (véase más abajo) y directamente como cuerpos de conceptos.
Hay tres tipos de restricciones:
La restricción asociada con una declaración se determina al normalizar 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, en orden de aparición;
- la expresión de restricción en la cláusula requires después de la lista de parámetros de plantilla;
- la expresión de restricción introducida para cada parámetro con un 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 cláusula requires al final.
Este orden determina el orden en el que se instancian las restricciones al verificar la satisfacción.
Una declaración restringida solo puede volver a declararse utilizando la misma forma sintáctica. No se requiere diagnóstico.
template<Incrementable T> void f(T) requires Decrementable<T>; template<Incrementable T> void f(T) requires Decrementable<T>; // de acuerdo, redeclaración template<typename T> requires Incrementable<T> && Decrementable<T> void f(T); // mal formado, no se requiere diagnóstico // las siguientes dos declaraciones tienen distintas restricciones: // la primera declaración tiene Incrementable<T> && Decrementable<T> // la segunda declaración tiene Decrementable<T> && Incrementable<T> // incluso cuando son lógicamente equivalentes. template<Incrementable T> void g(T) requires Decrementable<T>; template<Decrementable T> void g(T) requires Incrementable<T>; // la primera declaración tiene
Un concepto que toma múltiples argumentos de plantilla puede restringir un tipo deducido contextualmente. El concepto aplicado resulta de anteponer el tipo deducido a la plantilla de argumentos del concepto. Véase Parámetro de plantilla de tipo.
template <class T, class U> concept Derived = std::is_base_of<U, T>::value; template<Derived<Base> T> void f(T); // T is constrained by Derived<T, Base>
[editar] Conjunciones
La conjunción de dos restricciones se forma usando 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 se satisfacen ambas restricciones. Las conjunciones se evalúan de izquierda a derecha y se cortocircuitan (si la restricción de la izquierda no se satisface, no se intenta la sustitución del argumento de plantilla en la restricción de la derecha: esto evita fallas debido 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'); // de acuerdo, llama a #2. Al comprobar las restricciones de #1, // 'sizeof(char) > 1' no se satisface, así que get_value<T>() no se comprueba }
[editar] Disyunciones
La disyunción de dos restricciones se forma usando el operador || en la expresión de restricción.
Una disyunción de dos restricciones se satisface si se satisface cualquiera de las restricciones. Las disyunciones se evalúan de izquierda a derecha y se cortocircuitan (si se satisface la restricción izquierda, no se intenta la sustitución del argumento de plantilla en la restricción derecha).
template <class T = void> requires EqualityComparable<T> || Same<T, void> struct equal_to;
[editar] Restricciones atómicas
Una restricción atómica consiste en una expresión E y una correspondencia de los parámetros de la plantilla que aparecen dentro de E a los argumentos de la plantilla que involucran los parámetros de la plantilla de la entidad restringida, llamado su correspondencia de parámetros.
Las restricciones atómicas se forman durante la normalización de restricciones. E nunca es una expresión lógica AND o lógica OR (que forman conjunciones y disyunciones, respectivamente).
La satisfacción de una restricción atómica se verifica sustituyendo la correspondencia de parámetros y los argumentos de plantilla en la expresión E. Si la sustitución da como resultado un tipo o expresión no válidos, la restricción no se cumple. De lo contrario, E, después de cualquier conversión de lvalue a rvalue, será una expresión constante prvalue de tipo bool, y la restricción se cumple 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 comprobar #1, // aun cuando #2 es una mejor coincidencia }
Dos restricciones atómicas se consideran "idénticas" si se forman a partir de la misma expresión en el nivel de origen y sus correspondencias de parámetros son equivalentes.
template<class T> constexpr bool es_maullable = true; template<class T> constexpr bool es_gato = true; template<class T> concept Maullable = es_maullable<T>; template<class T> concept MalGatoMaullable = es_maullable<T> && es_gato<T>; template<class T> concept BuenGatoMaullable = Maullable<T> && es_gato<T>; template<Maullable T> void f1(T); // #1 template<MalGatoMaullable T> void f1(T); // #2 template<Maullable T> void f2(T); // #3 template<BuenGatoMaullable T> void f2(T); // #4 void g(){ f1(0); // ERROR, ambiguo: // es_maullable<T> en Maullable y MalGatoMaullable forman distintas // retricciones atómicas que no son idénticas (y así no se subsumirán) f2(0); // de acuerdo, llama a #4, más restringida que #3 // BuenGatoMaullable obtuvo su es_maullable<T> de Maullable }
[editar] Normalización de restricciones
La normalización de restricciones es el proceso que transforma una expresión de restricción en una secuencia de conjunciones y disyunciones de restricción 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
Cdenomina a un concepto, es la forma normal de la expresión de restricción deC, después de sustituir a A1, A2, ... , AN por los parámetros de plantilla respectivos deCen las correspondencias de parámetros de cada restricción atómica de C. Si tal sustitución en las correspondencias de parámetros da como resultado un tipo o expresión no válida, el programa está mal formado y no se requiere diagnóstico.
template<typename T> concept A = T::value || true; template<typename U> concept B = A<U*>; // de acuerdo: normalizado a la disyunción de // - T::value (con correspondencia T -> U*) y // - true (con una correspondencia vacía). // No hay tipo inválido en la correspondencia aun cuando // T::value está mal formado para todos los tipos puntero template<typename V> concept C = B<V&>; // Se normaliza a la disyunción de // - T::value (con correspondencia T-> V&*) y // - true (con una correspondencia vacía). // Tipo inválido V&* formado al corresponder => mal formado, no se requiere diagnóstico
- La forma normal de cualquier otra expresión E es la restricción atómica cuya expresión es E y cuya correspondencia de parámetros es la correspondencia de identidad. Esto incluye todas las expresiones expresiones de pliegue, incluso aquellas que se pliegan sobre los operadores
&&o||.
Las sobrecargas definidas por el usuario de && o || no tienen efecto en la normalización de restricciones.
[editar] Cláusulas requires
La palabra clave requires se usa para introducir una cláusula-requires, que especifica las restricciones impuestas sobre los 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 inmediatemente después de T add(T a, T b) { return a + b; } // una lista de parámetros de plantilla
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 denominado (como en el ejemplo anterior) o una conjunción/disyunción de conceptos denominados, 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
&&; o - una secuencia de las expresiones mencionadas anteriormente unidas con el operador
||.
template<class T> constexpr bool es_maullable = true; template<class T> constexpr bool es_ronroneable() { return true; } template<class T> void f(T) requires es_maullable<T>; // de acuerdo template<class T> void g(T) requires es_ronroneable<T>(); // ERROR, es_ronroneable<T>() no es una expresión primaria template<class T> void h(T) requires (es_ronroneable<T>()); // de acuerdo
[editar] Expresiones requires
La palabra clave requires también se usa para comenzar una expresión-requires, que es una expresión de valor de tipo bool que describe las restricciones en algunos argumentos de plantilla. Tal expresión es true si se cumplen las restricciones, y false en caso contrario:
template<typename T> concept Addable = requires (T x) { x + x; }; // expresión-requires template<typename T> requires Addable<T> // cláusula-requires, no expresión-requires T add(T a, T b) { return a + b; } template<typename T> requires requires (T x) { x + x; } // restricción para el caso , observa que la palabra clave se usa dos veces T add(T a, T b) { return a + b; }
La sintaxis de la expresión-requires es la siguiente:
requires { sec-de-requerimientos }
|
|||||||||
requires ( lista-de-parámetros(opcional) ) { sec-de-requerimientos }
|
|||||||||
| lista-de-parámetros | - | Una lista de parámetros separados por comas como en una declaración de función, excepto que los argumentos por defecto no están permitidos y no puede terminar con puntos suspensivos (que no sea uno que signifique una expansión de paquete). Estos parámetros no tienen almacenamiento, enlace o duración, y solo se utilizan para ayudar a especificar requerimientos. Estos parámetros están en el ámbito hasta el cierre } de la sec-de-requerimientos.
|
| sec-de-requerimientos | - | Secuencia de requerimientos, descrita más abajo (cada requerimiento termina con un punto y coma). |
Cada requerimiento en la sec-de-requerimientos es uno de los siguientes:
- requerimiento simple;
- requerimientos de tipo;
- requerimientos compuestos; o
- requerimientos anidados
Los requerimientos pueden hacer referencia a los parámetros de plantilla que están dentro del ámbito, a los parámetros locales introducidos en la lista-de-parámetros y a cualquier otra declaración que sea visible desde el contexto circundante.
La sustitución de argumentos de plantilla en una expresión-require usada en una declaración de una entidad emplantillada puede resultar en la formación de tipos o expresiones no válidos en sus requerimientos, o la violación de las restricciones semánticas de esos requerimientos. En tales casos, la expresión-requires se evalúa como false y no causa que el programa esté mal formado. La verificación de sustitución y restricción semántica procede en orden léxico y se detiene cuando se encuentra una condición que determina el resultado de la expresión-require. Si la verificación de la sustitución (si la hay) y la restricción semántica tiene éxito, la expresión-require se evalúa como true.
Si ocurriera un error de sustitución en una expresión-require para cada argumento de plantilla posible, el programa está mal formado y no se requiere diagnóstico:
template<class T> concept C = requires { new int[-(int)sizeof(T)]; // inválido para cada T: }; // mal formado, no se requiere diagnóstico
Si una expresión-requires contiene tipos o expresiones no válidos en sus requerimientos, y no aparece en la declaración de una entidad emplantillada, entonces el programa está mal formado.
[editar] Requerimientos simples
Un requerimiento simple es una declaración de expresión arbitraria que no comience con la palabra clave requires. Afirma que la expresión es válida. La expresión es un operando no evaluado; solo se verifica la corrección del lenguaje.
template<typename T> concept Addable = requires (T a, T b) { a + b; // "la expresión a+b es una expresión válida que compilará" }; template <class T, class U = T> concept Swappable = requires(T&& t, U&& u) { swap(std::forward<T>(t), std::forward<U>(u)); swap(std::forward<U>(u), std::forward<T>(t)); };
Un requerimiento que comienza con la palabra clave requires siempre se interpreta como un requerimiento anidado. Por lo tanto, un requerimiento simple no puede comenzar con una expresión-requires sin paréntesis.
[editar] Requerimientos de tipo
Un requerimiento de tipo es la palabra clave typename seguida de un nombre de tipo, opcionalmente calificado. El requerimiento es que el tipo denominado sea válido: esto se puede usar para verificar que existe un cierto tipo anidado con nombre, o que una especialización de plantilla de clase denomina un tipo, o que una especialización de plantilla de alias denomina un tipo. Un requerimiento de tipo para denominar una especialización de plantilla de clase no requiere que el tipo esté completo.
template<typename T> using Ref = T&; template<typename T> concept C = requires { typename T::inner; // nombre de miembro anidado requerido typename S<T>; // especialización de plantilla de clase requerida typename Ref<T>; // sustitución de plantilla de alias requerida }; template <class T, class U> using CommonType = std::common_type_t<T, U>; template <class T, class U> concept Common = requires (T&& t, U&& u) { typename CommonType<T, U>; // CommonType<T, U> es válido y denomina un tipo { CommonType<T, U>{std::forward<T>(t)} }; { CommonType<T, U>{std::forward<U>(u)} }; };
[editar] Requerimientos compuestos
Un requerimiento compuesto tiene la forma
{ expresión } noexcept(opcional) requerimiento-de-tipo-de-retorno(opcional) ;
|
|||||||||
| requerimiento-de-tipo-de-retorno | - | -> restricción-de-tipo
|
y afirma las propiedades de la expresión nombrada. La comprobación de la restricción semántica y de sustitución se realiza en el siguiente orden:
template<typename T> concept C2 = requires(T x) { {*x} -> std::convertible_to<typename T::inner>; // la expresión *x debe ser válida // Y el tipo T::inner debe ser válido // Y el resultado de *x debe ser convertible a T::inner {x + 1} -> std::same_as<int>; // la expresión x + 1 debe ser válida // Y std::same_as<decltype((x + 1)), int> debe satisfacerse // es decir, (x + 1) debe ser un prvalue de tipo int {x * 1} -> std::convertible_to<T>; // la expresión x * 1 debe ser válida // Y su resultado debe ser convertible a T };
[editar] Requerimientos anidados
Un requerimiento anidado tiene la forma
requires expresión-de-restricción ;
|
|||||||||
Se puede usar para especificar restricciones adicionales en términos de parámetros locales. La expresión-de-restricción debe cumplirse con los argumentos de la plantilla sustituidos, si los hubiera. La sustitución de argumentos de plantilla en un requisito anidado provoca la sustitución en la expresión-de-restricción solo en la medida necesaria para determinar si se cumple con la expresión-de-restricción.
template <class T> concept Semiregular = DefaultConstructible<T> && CopyConstructible<T> && Destructible<T> && CopyAssignable<T> && requires(T a, size_t n) { requires Same<T*, decltype(&a)>; // anidado: "Same<...> se evalúa como true" { a.~T() } noexcept; // compuesto: "a.~T()" es una expresión válida que no lanza requires Same<T*, decltype(new T)>; // anidado: "Same<...> se evalúa como true" requires Same<T*, decltype(new T[n])>; // anidado { delete new T }; // compuesto { delete new T[n] }; // compuesto };
[editar] Orden parcial de restricciones
Antes de cualquier análisis adicional, las restricciones se normalizan sustituyendo el cuerpo de cada concepto denominado y cada expresión requerida hasta que lo que queda es una secuencia de conjunciones y disyunciones sobre restricciones atómicas.
Se dice que una restricción P subsume la restricción Q si se puede probar que P implica Q hasta la identidad de las restricciones atómicas en P y Q (los tipos y expresiones no se analizan para determinar la equivalencia: N> 0 no subsume N>= 0).
Específicamente, primero P se convierte a la forma normal disyuntiva y Q se convierte a la forma normal conjuntiva. P subsume 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 hay 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 más arriba.
La relación de subsunción define el orden parcial de las restricciones, que se utiliza para determinar:
- el mejor candidato viable para una función que no es de plantilla en la resolución de sobrecarga;
- la dirección de una función que no es de plantilla en un conjunto de sobrecargas;
- la mejor coincidencia para un argumento de plantilla de plantilla;
- el orden parcial de especializaciones de plantillas de clase;
- el orden parcial de plantillas de función.
| Esta sección está incompleta Razón: enlazar partes anteriores de esta página a este punto |
Si las declaraciones D1 y D2 están restringidas y las restricciones asociadas de D1 subsumen las restricciones asociadas de D2 (o si D2 está irrestringida), entonces se dice que D1 está al menos tan restringida como D2. Si D1 está al menos tan restringida como D2 y D2 no está al menos tan restringida como D1, entonces D1 está más restringida que D2.
template<typename T> concept Decrementable = requires(T t) { --t; }; template<typename T> concept RevIterator = Decrementable<T> && requires(T t) { *t; }; // RevIterator subsume 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 a Decrementable, selecciona #1 f((int*)0); // int* satisface ambas restricciones, selecciona #2 como más restringida template<class T> void g(T); // #3 (irrestringida) template<Decrementable T> void g(T); // #4 g(true); // bool no satisface a Decrementable, selecciona #3 g(0); // int satisface a Decrementable, selecciona #4 porque está 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); // ambigua

