Constraints and concepts
Esta página describe una característica experimental del lenguaje central. Para los requisitos de tipo con nombre utilizados en la especificación de la biblioteca estándar, consulte named requirements
Plantillas de clase , plantillas de función , y funciones no plantilla (típicamente miembros de plantillas de clase) pueden estar asociadas con una restricción , que especifica los requisitos sobre los argumentos de plantilla, los cuales pueden utilizarse para seleccionar las sobrecargas de función y especializaciones de plantilla más apropiadas.
Las restricciones también pueden utilizarse para limitar la deducción automática de tipos en declaraciones de variables y tipos de retorno de funciones únicamente a los tipos que satisfacen los requisitos especificados.
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 <string> #include <locale> using namespace std::literals; // Declaración del concepto "EqualityComparable", que es satisfecho por // cualquier tipo T tal que para valores a y b de tipo T, // la expresión a==b compila y su resultado es convertible a bool template<typename T> concept bool EqualityComparable = requires(T a, T b) { { a == b } -> bool; }; void f(EqualityComparable&&); // declaración de una plantilla de función restringida // template<typename T> // void f(T&&) requires EqualityComparable<T>; // forma larga equivalente int main() { f("abc"s); // OK, std::string es EqualityComparable f(std::use_facet<std::ctype<char>>(std::locale{})); // Error: no es EqualityComparable }
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
El propósito de los concepts es modelar categorías semánticas (Number, Range, RegularFunction) en lugar de restricciones sintácticas (HasPlus, Array). Según la guía principal de ISO C++ T.20 , "La capacidad de especificar una semántica significativa es una característica definitoria de un verdadero concept, a diferencia de una restricción sintáctica."
Si las pruebas de características son compatibles, las características descritas aquí se indican mediante la constante macro __cpp_concepts con un valor igual o mayor a 201507 .
Contenidos |
Marcadores de posición
El
placeholder
sin restricciones
auto
y los
placeholders con restricciones
que tienen la forma
concept-name
<
template-argument-list
(opcional)
>
, son placeholders para el tipo que será deducido.
Los placeholders pueden aparecer en declaraciones de variables (en cuyo caso se deducen del inicializador) o en tipos de retorno de funciones (en cuyo caso se deducen de las sentencias return)
std::pair<auto, auto> p2 = std::make_pair(0, 'a'); // primer auto es int, // segundo auto es char Sortable x = f(y); // el tipo de x se deduce del tipo de retorno de f, // solo compila si el tipo satisface la restricción Sortable auto f(Container) -> Sortable; // el tipo de retorno se deduce de la sentencia return // solo compila si el tipo satisface Sortable
Los placeholders también pueden aparecer en parámetros, en cuyo caso convierten las declaraciones de función en declaraciones de template (con restricciones si el placeholder está restringido)
void f(std::pair<auto, EqualityComparable>); // esta es una plantilla con dos parámetros: // parámetro de tipo sin restricciones y parámetro no-tipo restringido
Los placeholders restringidos pueden usarse en cualquier lugar donde auto pueda utilizarse, por ejemplo, en declaraciones de lambdas genéricas
auto gl = [](Assignable& a, auto* b) { a = *b; };
Si un especificador de tipo restringido designa un no-tipo o una plantilla, pero se utiliza como un marcador de posición restringido, el programa está mal formado:
template<size_t N> concept bool Even = (N%2 == 0); struct S1 { int n; }; int Even::* p2 = &S1::n; // error, uso inválido de un concepto no-tipo void f(std::array<auto, Even>); // error, uso inválido de un concepto no-tipo template<Even N> void f(std::array<auto, N>); // OK
Plantillas abreviadas
Si uno o más marcadores de posición aparecen en una lista de parámetros de función, la declaración de función es en realidad una declaración de plantilla de función, cuya lista de parámetros de plantilla incluye un parámetro inventado para cada marcador de posición único, en orden de aparición
// forma corta void g1(const EqualityComparable*, Incrementable&); // forma larga: // template<EqualityComparable T, Incrementable U> void g1(const T*, U&); // forma más larga: // template<typename T, typename U> // void g1(const T*, U&) requires EqualityComparable<T> && Incrementable<U>; void f2(std::vector<auto*>...); // forma larga: template<typename... T> void f2(std::vector<T*>...); void f4(auto (auto::*)(auto)); // forma larga: template<typename T, typename U, typename V> void f4(T (U::*)(V));
Todos los placeholders introducidos por especificadores de tipo con restricciones equivalentes tienen el mismo parámetro de plantilla inventado. Sin embargo, cada especificador sin restricciones (
auto
) siempre introduce un parámetro de plantilla diferente
void f0(Comparable a, Comparable* b); // forma larga: template<Comparable T> void f0(T a, T* b); void f1(auto a, auto* b); // forma larga: template<typename T, typename U> f1(T a, U* b);
Tanto las plantillas de función como las plantillas de clase pueden declararse utilizando una
introducción de plantilla
, que tiene la sintaxis
nombre-del-concepto
{
lista-de-parámetros
(opcional)
}
, en cuyo caso la palabra clave
template
no es necesaria: cada parámetro de la
lista-de-parámetros
de la introducción de plantilla se convierte en un parámetro de plantilla cuyo tipo (tipo, no-tipo, plantilla) está determinado por el tipo del parámetro correspondiente en el concepto nombrado.
Además de declarar una plantilla, la introducción de plantilla asocia una restricción de predicado (ver abajo) que nombra (para conceptos de variable) o invoca (para conceptos de función) el concepto nombrado por la introducción.
EqualityComparable{T} class Foo; // forma larga: template<EqualityComparable T> class Foo; // forma más larga: template<typename T> requires EqualityComparable<T> class Foo; template<typename T, int N, typename... Xs> concept bool Example = ...; Example{A, B, ...C} struct S1; // forma larga template<class A, int B, class... C> requires Example<A,B,C...> struct S1;
Para plantillas de función, la introducción de plantilla se puede combinar con marcadores de posición:
Sortable{T} void f(T, auto); // forma larga: template<Sortable T, typename U> void f(T, U); // alternativa usando solo placeholders: void f(Sortable, auto);
|
Esta sección está incompleta
Razón: retocar las páginas de declaración de plantillas para enlazar aquí |
Conceptos
Un concepto es un conjunto nombrado de requisitos. La definición de un concepto aparece en el ámbito del espacio de nombres y tiene la forma de una plantilla de función (en cuyo caso se denomina concepto de función ) o una plantilla de variable (en cuyo caso se denomina concepto de variable ). La única diferencia es que la palabra clave concept aparece en la secuencia de especificadores de declaración :
// concepto de variable de la biblioteca estándar (Ranges TS) template <class T, class U> concept bool Derived = std::is_base_of<U, T>::value; // concepto de función de la biblioteca estándar (Ranges TS) template <class T> concept bool EqualityComparable() { return requires(T a, T b) { {a == b} -> Boolean; {a != b} -> Boolean; }; }
Las siguientes restricciones se aplican a los conceptos de función:
-
inlineyconstexprno están permitidos, la función es automáticamenteinlineyconstexpr -
friendyvirtualno están permitidos -
la especificación de excepciones no está permitida, la función es automáticamente
noexcept(true). - no puede ser declarada y definida posteriormente, no puede ser redeclarada
-
el tipo de retorno debe ser
bool - la deducción del tipo de retorno no está permitida
- la lista de parámetros debe estar vacía
-
el cuerpo de la función debe consistir únicamente en una sentencia
return, cuyo argumento debe ser una expresión de restricción (restricción de predicado, conjunción/disyunción de otras restricciones, o una expresión requires, ver más abajo)
Las siguientes restricciones se aplican a los conceptos de variable:
-
Debe tener el tipo
bool - No puede declararse sin un inicializador
- No puede declararse en el ámbito de clase
-
constexprno está permitido, la variable es automáticamenteconstexpr - el inicializador debe ser una expresión de restricción (restricción de predicado, conjunción/disyunción de restricciones, o una expresión requires, ver más abajo)
Los conceptos no pueden referirse recursivamente a sí mismos en el cuerpo de la función o en el inicializador de la variable:
template<typename T> concept bool F() { return F<typename T::type>(); } // error template<typename T> concept bool V = V<T*>; // error
Las instanciaciones explícitas, especializaciones explícitas o especializaciones parciales de conceptos no están permitidas (no se puede cambiar el significado de la definición original de una constraint)
Restricciones
Una restricción es una secuencia de operaciones lógicas que especifica requisitos sobre los argumentos de plantilla. Pueden aparecer dentro de expresiones requires (ver abajo) y directamente como cuerpos de conceptos
Hay 9 tipos de restricciones:
Los primeros tres tipos de restricciones pueden aparecer directamente como el cuerpo de un concepto o como una cláusula requires ad-hoc:
template<typename T> requires // cláusula requires (restricción ad-hoc) sizeof(T) > 1 && get_value<T>() // conjunción de dos restricciones de predicado void f(T);
Cuando múltiples restricciones están asociadas a la misma declaración, la restricción total es una conjunción en el siguiente orden: la restricción introducida por la introducción de plantilla , restricciones para cada parámetro de plantilla en orden de aparición, la cláusula requires después de la lista de parámetros de plantilla, restricciones para cada parámetro de función en orden de aparición, cláusula final requires :
// las declaraciones declaran la misma plantilla de función restringida // con la restricción Incrementable<T> && Decrementable<T> template<Incrementable T> void f(T) requires Decrementable<T>; template<typename T> requires Incrementable<T> && Decrementable<T> void f(T); // correcto // 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 son lógicamente equivalentes. // La segunda declaración está mal formada, no se requiere diagnóstico template<Incrementable T> requires Decrementable<T> void g(); template<Decrementable T> requires Incrementable<T> void g(); // error
Conjunciones
Conjunción de restricciones
P
y
Q
se especifica como
P
&&
Q
.
// ejemplo de conceptos de la biblioteca estándar (Ranges TS) template <class T> concept bool Integral = std::is_integral<T>::value; template <class T> concept bool SignedIntegral = Integral<T> && std::is_signed<T>::value; template <class T> concept bool 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 debido a sustitución fuera del contexto inmediato). No se permiten sobrecargas definidas por el usuario de
operator&&
en conjunciones de restricciones.
Disyunciones
Disyunción de restricciones
P
y
Q
se especifica como
P
||
Q
.
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, la deducción de argumentos de plantilla en la restricción derecha no se intenta). No se permiten sobrecargas definidas por el usuario de
operator||
en disyunciones de restricciones.
// ejemplo de restricción de la biblioteca estándar (Ranges TS) template <class T = void> requires EqualityComparable<T>() || Same<T, void> struct equal_to;
Restricciones de predicado
Una restricción de predicado es una expresión constante de tipo bool . Se satisface únicamente si se evalúa como true
template<typename T> concept bool Size32 = sizeof(T) == 4;
Las restricciones de predicado pueden especificar requisitos sobre parámetros de plantilla no tipo y sobre argumentos de plantilla de plantilla.
Las restricciones de predicado deben evaluarse directamente a bool , no se permiten conversiones:
template<typename T> struct S { constexpr explicit operator bool() const { return true; } }; template<typename T> requires S<T>{} // restricción de predicado incorrecta: S<T>{} no es bool void f(T); f(0); // error: restricción nunca satisfecha
Requisitos
La palabra clave requires se utiliza de dos maneras:
template<typename T> void f(T&&) requires Eq<T>; // can appear as the last element of a function declarator template<typename T> requires Addable<T> // or right after a template parameter list T add(T a, T b) { return a + b; }
true
si el concepto correspondiente se satisface, y false en caso contrario:
template<typename T> concept bool Addable = requires (T x) { x + x; }; // requires-expression template<typename T> requires Addable<T> // requires-clause, not requires-expression T add(T a, T b) { return a + b; } template<typename T> requires requires (T x) { x + x; } // ad-hoc constraint, note keyword used twice T add(T a, T b) { return a + b; }
La sintaxis de la requires-expression es la siguiente:
requires
(
lista-de-parámetros
(opcional)
)
{
secuencia-de-requisitos
}
|
|||||||||
| parameter-list | - |
una lista separada por comas de parámetros como en una declaración de función, excepto que no se permiten argumentos predeterminados y el último parámetro no puede ser una elipsis. Estos parámetros no tienen almacenamiento, vinculación ni duración. Estos parámetros están en alcance hasta el cierre
}
de la
requirement-seq
. Si no se utilizan parámetros, los paréntesis también pueden omitirse
|
| requirement-seq | - | secuencia separada por espacios en blanco de requisitos , descritos a continuación (cada requisito termina con un punto y coma). Cada requisito añade otra restricción a la conjunción de restricciones que esta expresión de requisitos define. |
Cada requisito en la requirements-seq es uno de los siguientes:
- requisito simple
- requisitos de tipo
- requisitos compuestos
- requisitos anidados
Los requisitos pueden referirse a los parámetros de plantilla que están en alcance y a los parámetros locales introducidos en la parameter-list . Cuando está parametrizada, una expresión requires se dice que introduce una restricción parametrizada
La sustitución de argumentos de plantilla en una expresión requires puede resultar en la formación de tipos o expresiones inválidos en sus requisitos. En tales casos,
- Si ocurre un fallo de sustitución en una expresión requires que se utiliza fuera de una entidad con plantilla declaración, entonces el programa está mal formado.
- Si la expresión requires se utiliza en una declaración de una entidad con plantilla , la restricción correspondiente se trata como "no satisfecha" y el fallo de sustitución no es un error , sin embargo
- Si ocurriría un fallo de sustitución en una expresión requires para cada posible argumento de plantilla, el programa está mal formado, sin requerir diagnóstico:
template<class T> concept bool C = requires { new int[-(int)sizeof(T)]; // inválido para todo T: mal formado, no se requiere diagnóstico };
Requisitos simples
Un requisito simple es una declaración de expresión arbitraria. El requisito es que la expresión sea válida (esto es una restricción de expresión ). A diferencia de las restricciones de predicado, no se realiza la evaluación, solo se verifica la corrección del lenguaje.
template<typename T> concept bool Addable = requires (T a, T b) { a + b; // "la expresión a+b es una expresión válida que compilará" }; // ejemplo de restricción de la biblioteca estándar (ranges TS) template <class T, class U = T> concept bool Swappable = requires(T&& t, U&& u) { swap(std::forward<T>(t), std::forward<U>(u)); swap(std::forward<U>(u), std::forward<T>(t)); };
Requisitos de tipo
Un requisito de tipo es la palabra clave typename seguida de un nombre de tipo, opcionalmente calificado. El requisito es que el tipo nombrado exista (una restricción de tipo ): esto puede usarse para verificar que existe cierto tipo anidado con nombre, o que una especialización de plantilla de clase denomina un tipo, o que una plantilla de alias denomina un tipo.
template<typename T> using Ref = T&; template<typename T> concept bool 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 }; // Ejemplo de concepto de la biblioteca estándar (Ranges TS) template <class T, class U> using CommonType = std::common_type_t<T, U>; template <class T, class U> concept bool Common = requires (T t, U u) { typename CommonType<T, U>; // CommonType<T, U> es válido y nombra un tipo { CommonType<T, U>{std::forward<T>(t)} }; { CommonType<T, U>{std::forward<U>(u)} }; };
Requisitos Compuestos
Un requisito compuesto tiene la forma
{
expresión
}
noexcept
(opcional)
tipo-de-retorno-final
(opcional)
;
|
|||||||||
y especifica una conjunción de las siguientes restricciones:
noexcept
, la expresión también debe ser noexcept (
restricción de excepción
)
template<typename T> concept bool C2 = requires(T x) { {*x} -> 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 }; // Ejemplo de concepto de la biblioteca estándar (Ranges TS) template <class T, class U> concept bool Same = std::is_same<T,U>::value; template <class B> concept bool Boolean = requires(B b1, B b2) { { bool(b1) }; // la restricción de inicialización directa debe usar expresión { !b1 } -> bool; // restricción compuesta requires Same<decltype(b1 && b2), bool>; // restricción anidada, ver abajo requires Same<decltype(b1 || b2), bool>; };
Requisitos anidados
Un requisito anidado es otra cláusula requires , terminada con un punto y coma. Esto se utiliza para introducir restricciones de predicado (ver arriba) expresadas en términos de otros conceptos nombrados aplicados a los parámetros locales (fuera de una cláusula requires, las restricciones de predicado no pueden usar parámetros, y colocar una expresión directamente en una cláusula requires la convierte en una restricción de expresión lo que significa que no se evalúa)
// ejemplo de restricción del Ranges TS template <class T> concept bool Semiregular = DefaultConstructible<T> && CopyConstructible<T> && Destructible<T> && CopyAssignable<T> && requires(T a, size_t n) { requires Same<T*, decltype(&a)>; // anidada: "Same<...> se evalúa como verdadero" { a.~T() } noexcept; // compuesta: "a.~T()" es una expresión válida que no lanza excepciones requires Same<T*, decltype(new T)>; // anidada: "Same<...> se evalúa como verdadero" requires Same<T*, decltype(new T[n])>; // anidada { delete new T }; // compuesta { delete new T[n] }; // compuesta };
Resolución de conceptos
Al igual que cualquier otra plantilla de función, un concepto de función (pero no un concepto de variable) puede sobrecargarse: pueden proporcionarse múltiples definiciones de concepto que utilicen el mismo concept-name .
La resolución de conceptos se realiza cuando un concept-name (que puede ser calificado) aparece en
template<typename T> concept bool C() { return true; } // #1 template<typename T, typename U> concept bool C() { return true; } // #2 void f(C); // el conjunto de conceptos referenciados por C incluye tanto #1 como #2; // la resolución de conceptos (ver abajo) selecciona #1.
Para realizar la resolución de conceptos, template parameters de cada concepto que coincide con el nombre (y la calificación, si la hay) se empareja con una secuencia de concept arguments , que son argumentos de plantilla y wildcards . Un wildcard puede coincidir con un parámetro de plantilla de cualquier tipo (tipo, no-tipo, plantilla). El conjunto de argumentos se construye de manera diferente, dependiendo del contexto
template<typename T> concept bool C1() { return true; } // #1 template<typename T, typename U> concept bool C1() { return true; } // #2 void f1(const C1*); // <wildcard> matches <T>, selects #1
template<typename T> concept bool C1() { return true; } // #1 template<typename T, typename U> concept bool C1() { return true; } // #2 void f2(C1<char>); // <wildcard, char> matches <T, U>, selects #2
template<typename... Ts> concept bool C3 = true; C3{T} void q2(); // OK: <T> coincide con <...Ts> C3{...Ts} void q1(); // OK: <...Ts> coincide con <...Ts>
template<typename T> concept bool C() { return true; } // #1 template<typename T, typename U> concept bool C() { return true; } // #2 template <typename T> void f(T) requires C<T>(); // matches #1
La resolución de conceptos se realiza emparejando cada argumento con el parámetro correspondiente de cada concepto visible. Los argumentos de plantilla predeterminados (si se utilizan) se instancian para cada parámetro que no corresponde a un argumento, y luego se añaden a la lista de argumentos. Un parámetro de plantilla coincide con un argumento solo si tiene el mismo tipo (tipo, no-tipo, plantilla), a menos que el argumento sea un comodín. Un paquete de parámetros coincide con cero o más argumentos siempre que todos los argumentos coincidan con el patrón en tipo (a menos que sean comodines).
Si algún argumento no coincide con su parámetro correspondiente o si hay más argumentos que parámetros y el último parámetro no es un pack, el concepto no es viable. Si hay cero o más de un concepto viable, el programa está mal formado.
template<typename T> concept bool C2() { return true; } template<int T> concept bool C2() { return true; } template<C2<0> T> struct S1; // error: <comodín, 0> no coincide con // ni <typename T> ni <int T> template<C2 T> struct S2; // tanto #1 como #2 coinciden: error
|
Esta sección está incompleta
Motivo: necesita un ejemplo con conceptos significativos, no estos marcadores de posición 'return true' |
Ordenamiento parcial de restricciones
Antes de cualquier análisis adicional, las restricciones son normalizadas sustituyendo el cuerpo de cada concepto de nombre y cada expresión requires hasta que lo que queda es una secuencia de conjunciones y disyunciones sobre restricciones atómicas, que son restricciones de predicado, restricciones de expresión, restricciones de tipo, restricciones de conversión implícita, restricciones de deducción de argumentos y restricciones de excepción.
Se dice que el concepto
P
subsume
al concepto
Q
si se puede demostrar que
P
implica
Q
sin analizar tipos y expresiones para equivalencia (por lo que
N >= 0
no subsume a
N > 0
)
Específicamente, primero
P
se convierte a forma normal disyuntiva y
Q
se convierte a forma normal conjuntiva, y se comparan de la siguiente manera:
-
cada restricción atómica
Asubsume a la restricción atómica equivalenteA -
cada restricción atómica
Asubsume a una disyunciónA||By no subsume a una conjunciónA&&B -
cada conjunción
A&&Bsubsume aA, pero una disyunciónA||Bno subsume aA
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 lo anterior hacia aquí |
Si las declaraciones
D1
y
D2
están restringidas y las restricciones normalizadas de D1 subsumen las restricciones normalizadas de D2 (o si D1 está restringida y 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.
template<typename T> concept bool Decrementable = requires(T t) { --t; }; template<typename T> concept bool RevIterator = Decrementable<T> && requires(T t) { *t; }; // RevIterator subsume a Decrementable, pero no al revés // RevIterator está más restringido que Decrementable void f(Decrementable); // #1 void f(RevIterator); // #2 f(0); // int solo satisface Decrementable, selecciona #1 f((int*)0); // int* satisface ambas restricciones, selecciona #2 por estar más restringido void g(auto); // #3 (sin restricciones) void g(Decrementable); // #4 g(true); // bool no satisface Decrementable, selecciona #3 g(0); // int satisface Decrementable, selecciona #4 porque está más restringido
Palabras clave
Soporte del compilador
GCC >= 6.1 admite esta especificación técnica (opción requerida - fconcepts )