Espacios de nombres
Variantes
Acciones

Restricciones y conceptos (desde C++20)

De cppreference.com
< cpp‎ | language
Esta página describe la característica principal del lenguaje adoptada para C++20. Para los requisitos de tipo con nombre 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 apropiadas.

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>
 
// Declaración del concepto "Hashable", que se satisface por
// cualquier tipo 'T', 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
 
// 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 /*nombreParámetro*/) {}
 
int main() 
{
    using std::operator""s;
 
    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 >

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

atr - secuencia de cualquier número de atributos
// 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, es false.

Los conceptos también pueden denominarse en una restricción-de-tipo, como parte de

En una restricción de tipo, un concepto toma un argumento de plantilla menos de lo que exige su lista de parámetros, porque el tipo deducido por el contexto se usa implícitamente como el primer argumento del concepto.

template<class T, class U>
concept Derivada = std::is_base_of<U, T>::value;
 
template<Derivada<Base> T>
void f(T); // T se restringe por Derivada<T, Base>

[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 o directamente como cuerpos de conceptos.

Hay tres tipos de restricciones:

1) conjunciones;
2) disyunciones; y
3) restricciones atómicas.

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:

  1. la expresión de restricción introducida para cada parámetro de plantilla de tipo restringido o parámetro de plantilla de no tipo declarado con un tipo de marcador de posición restringido, en orden de aparición;
  2. la expresión de restricción en la cláusula requires después de la lista de parámetros de plantilla;
  3. 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;
  4. 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.

[editar] Redeclaraciones

Una declaración restringida solo puede volver a declararse utilizando la misma forma sintáctica. No se requiere diagnóstico.

// Estas dos primeras declaraciones de f están bien
template<Incrementable T>
void f(T) requires Decrementable<T>;
 
template<Incrementable T>
void f(T) requires Decrementable<T>; // de acuerdo, redeclaración
 
//   La inclusión de esta tercera declaración de f lógicamente
//   equivalente pero sintácticamente diferente está mal formada, 
//   no se requiere diagnóstico
template<typename T>
    requires Incrementable<T> && Decrementable<T>
void f(T); 
 
// 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>; // mal formado, no requiere diagnostico


[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 BienGatoMaullable = 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<BienGatoMaullable T>
void f2(T); // #4
 
void g(){
    f1(0); // ERROR, ambiguo:
           // es_maullable<T> en Maullable y MalGatoMaullable forman distintas
           // restricciones atómicas que no son idénticas (y así no se subsumirán 
           // unos a otros)
 
    f2(0); // de acuerdo, llama a #4, más restringida que #3
           // BienGatoMaullable 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 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 denomina a un concepto, es la forma normal de la expresión de restricción de C, después de sustituir a A1, A2, ... , AN por los parámetros de plantilla respectivos de C en 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 inmediatamente 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] 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 require 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 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 hay 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 más arriba.

La relación de subsunción define el orden parcial de las 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 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 (no restringida)
 
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

[editar] Notas

Prueba de característica Valor Estándar Comentario
__cpp_concepts 201907L (C++20) Restricciones
202002L (C++20) Funciones miembro especiales condicionalmente triviales

[editar] Palabras clave

concept, requires

[editar] Informe de defectos

Los siguientes informes de defectos de cambio de comportamiento se aplicaron de manera retroactiva a los estándares de C++ publicados anteriormente.

ID Aplicado a Comportamiento según lo publicado Comportamiento correcto
CWG 2428 C++20 no se podían aplicar atributos a conceptos permitido

[editar] Véase también

Expresión requires (C++20) produce una expresión prvalue de tipo bool que describe las restricciones[editar]