Namespaces
Variants

Constraints and concepts (since C++20)

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

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 >

concept nombre-del-concepto attr  (opcional) = expresión-de-restricción ;

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

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:

1) conjunciones
2) disyunciones
3) restricciones atómicas
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:

  1. 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;
  2. la expresión de restricción en la requires cláusula después de la lista de parámetros de plantilla;
  3. 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 ;
  4. 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 C y un operador de plegado (ya sea && o || ). Una restricción expandida por plegado es una expansión de paquete .

Sea N el número de elementos en los parámetros de expansión del paquete:

  • Si la expansión del paquete es inválida (como expandir paquetes de diferente tamaño), la restricción expandida por plegado no se satisface.
  • Si N es 0 , la restricción expandida por plegado se satisface si el operador de plegado es && , o no se satisface si el operador de plegado es || .
  • Para una restricción expandida por plegado con un N positivo, para cada i en [ 1 , N ] , cada parámetro de expansión del paquete se reemplaza con el correspondiente i -ésimo elemento en orden creciente:
  • Para restricciones expandidas por plegado cuyo operador de plegado es && , si el reemplazo del j -ésimo elemento viola C , la restricción expandida por plegado no se satisface. En este caso, no ocurre sustitución para ningún i mayor que j . De lo contrario, la restricción expandida por plegado se satisface.
  • Para restricciones expandidas por plegado cuyo operador de plegado es || , si el reemplazo del j -ésimo elemento satisface C , la restricción expandida por plegado se satisface. En este caso, no ocurre sustitución para ningún i mayor que j . De lo contrario, la restricción expandida por plegado no se satisface.


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 C nombra un concepto, es la forma normal de la expresión de restricción de C , después de sustituir A1 , A2 , ... , AN por los parámetros de plantilla respectivos de C en los mapeos de parámetros de cada restricción atómica de C . 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
  • La forma normal de las expresiones ( E && ... ) y ( ... && E ) es una restricción expandida mediante plegado, donde C es la forma normal de E y el operador de plegado es && .
  • La forma normal de las expresiones ( E || ... ) y ( ... || E ) es una restricción expandida mediante plegado, donde C es la forma normal de E y el operador de plegado es || .
  • Las formas normales de las expresiones ( E1 && ... && E2 ) y ( E1 || ... || E2 ) son las formas normales de
  • ( E1 && ... ) && E2 y ( E1 || ... ) || E2 respectivamente, si E1 contiene un paquete sin expandir, o
  • E1 && ( ... && E2 ) y E1 || ( ... || E2 ) respectivamente en caso contrario.
(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 P subsume cada cláusula conjuntiva en la forma normal conjuntiva de Q , donde
  • una cláusula disyuntiva subsume una cláusula conjuntiva si y solo si existe una restricción atómica U en la cláusula disyuntiva y una restricción atómica V en la cláusula conjuntiva tal que U subsume V ;
  • una restricción atómica A subsume una restricción atómica B si y solo si son idénticas usando las reglas descritas arriba .
  • Una restricción expandida por plegado A subsume a otra restricción expandida por plegado B si tienen el mismo operador de plegado, la restricción C de A subsume a la de B , y ambas C contienen un paquete no expandido equivalente.
(desde C++26)

La relación de subsunción define un orden parcial de restricciones, que se utiliza para determinar:

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.
  • F1 está más restringida que F2 .
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

concept , requires , typename

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