Primera impresi贸n de conceptos.



Decid铆 tratar con la nueva caracter铆stica C ++ 20: conceptos.

Los conceptos (o conceptos , como escribe el Wiki de habla rusa) es una caracter铆stica muy interesante y 煤til que ha faltado durante mucho tiempo.

Esencialmente, est谩 escribiendo argumentos de plantilla.

El principal problema de las plantillas antes de C ++ 20 es que podr铆a sustituir cualquier cosa en ellas, incluso algo para lo que no fueron dise帽adas. Es decir, el sistema de plantillas estaba completamente sin tipo. Como resultado, se produjeron mensajes de error incre铆blemente largos y completamente ilegibles al pasar el par谩metro incorrecto a la plantilla. Intentaron luchar contra esto con la ayuda de diferentes hacks de idiomas, que ni siquiera quiero mencionar (aunque tuve que lidiar).

Los conceptos est谩n dise帽ados para corregir este malentendido. A帽aden un sistema de mecanograf铆a a las plantillas, y es muy poderoso. Y ahora, entendiendo las caracter铆sticas de este sistema, comenc茅 a estudiar los materiales disponibles en Internet.

Francamente, estoy un poco sorprendido :) C ++ es un lenguaje ya complicado, pero al menos hay una excusa: sucedi贸. La metaprogramaci贸n en plantillas se descubri贸, no se estableci贸, al dise帽ar un lenguaje. Y luego, al desarrollar las pr贸ximas versiones del lenguaje, se vieron obligados a adaptarse a este "descubrimiento", ya que se hab铆a escrito mucho c贸digo en el mundo. Los conceptos son una oportunidad fundamentalmente nueva. Y, me parece, ya hay algo de opacidad en su implementaci贸n. 驴Quiz谩s esto es una consecuencia de la necesidad de tener en cuenta la gran cantidad de capacidades heredadas? Tratemos de resolverlo ...

Informaci贸n general


Un concepto es una nueva entidad de lenguaje basada en la sintaxis de la plantilla. Un concepto tiene un nombre, par谩metros y un cuerpo, un predicado que devuelve un valor l贸gico constante (es decir, calculado en la etapa de compilaci贸n) dependiendo de los par谩metros del concepto. Me gusta esto:

template<int I> 
concept Even = I % 2 == 0;  

template<typename T>
concept FourByte = sizeof(T)==4;

T茅cnicamente, los conceptos son muy similares a las expresiones constexpr de plantilla como bool:

template<int I>
constexpr bool EvenX = I % 2 == 0; 

template<typename T>
constexpr bool FourByteX = sizeof(T)==4;

Incluso puede usar conceptos en expresiones comunes:

bool b1 = Even<2>; 

Utilizando


La idea principal de los conceptos es que se pueden usar en lugar del nombre de tipo o las palabras clave de clase en las plantillas. Al igual que los metatipos ("tipos para tipos"). Por lo tanto, la escritura est谩tica se introduce en las plantillas.

template<FourByte T>
void foo(T const & t) {}

Ahora, si usamos int como par谩metro de plantilla, entonces el c贸digo en la gran mayor铆a de los casos se compilar谩; y si es doble, se emitir谩 un mensaje de error breve y comprensible. Escritura simple y clara de plantillas, hasta ahora todo est谩 bien.

requiere


Esta es una nueva palabra clave "contextual" C ++ 20 con un doble prop贸sito: requiere cl谩usula y requiere expresi贸n. Como se mostrar谩 m谩s adelante, este extra帽o ahorro de palabras clave genera cierta confusi贸n.

requiere expresi贸n


Primero, considerar requiere expresi贸n. La idea es bastante buena: esta palabra tiene un bloque entre llaves, cuyo c贸digo se eval煤a para su compilaci贸n. Es cierto que el c贸digo no debe estar escrito en C ++, sino en un lenguaje especial, cercano a C ++, pero que tiene sus propias caracter铆sticas (esta es la primera rareza, era muy posible hacer solo c贸digo C ++).

Si el c贸digo es correcto, la expresi贸n requiere devuelve verdadero, de lo contrario es falso. El c贸digo en s铆, por supuesto, nunca entra en la generaci贸n de c贸digo, al igual que las expresiones en sizeof o decltype.

Desafortunadamente, la palabra es contextual y funciona solo dentro de las plantillas, es decir, fuera de la plantilla, esto no compila:

bool b = requires { 3.14 >> 1; };

Y en la plantilla, por favor:

template<typename T>
constexpr bool Shiftable = requires(T i) { i>>1; };

Y funcionar谩:

bool b1 = Shiftable<int>; // true
bool b2 = Shiftable<double>; // false

El uso principal de requiere expresi贸n es la creaci贸n de conceptos. Por ejemplo, as铆 es como puede verificar la presencia de campos y m茅todos en un tipo. Un caso muy popular.

template <typename T>
concept Machine = 
  requires(T m) {  //   `m` ,   Machine
	m.start();     //    `m.start()` 
	m.stop();      //   `m.stop()`
};  

Por cierto, todas las variables que pueden ser necesarias en el c贸digo probado (no solo los par谩metros de la plantilla) deben declararse entre par茅ntesis requiere expresi贸n. Por alguna raz贸n, declarar una variable simplemente no es posible.

La verificaci贸n de tipo en el interior requiere


Aqu铆 es donde comienzan las diferencias entre el c贸digo requerido y el est谩ndar C ++. Para verificar los tipos devueltos, se utiliza una sintaxis especial: el objeto se toma entre llaves, se coloca una flecha y despu茅s se escribe un concepto que el tipo debe satisfacer. Adem谩s, el uso de tipos directamente no est谩 permitido.

Compruebe que el retorno de la funci贸n se puede convertir a int:

requires(T v, int i) {
  { v.f(i) } -> std::convertible_to<int>;
}  

Verifique que la funci贸n de retorno sea exactamente int:

requires(T v, int i) {
  { v.f(i) } -> std::same_as<int>; 
}  

(std :: same_as y std :: convertible_to son conceptos de la biblioteca est谩ndar).

Si no encierra una expresi贸n cuyo tipo est谩 marcado entre llaves, el compilador no entiende lo que quiere de 茅l e interpreta toda la cadena como una sola expresi贸n que debe verificarse para la compilaci贸n.

requiere adentro requiere


La palabra clave require tiene un significado especial en el interior requiere expresiones. Las expresiones obligatorias anidadas (ya sin llaves) se comprueban no para la compilaci贸n, sino para la igualdad de verdadero o falso. Si dicha expresi贸n resulta ser falsa, entonces la expresi贸n que lo encierra resulta ser falsa inmediatamente (y se interrumpe el an谩lisis posterior de la compilaci贸n). Forma general:

requires { 
  expression;         // expression is valid
  requires predicate; // predicate is true
};

Como predicado, por ejemplo, se pueden utilizar conceptos o rasgos de tipo previamente definidos. Ejemplo:

requires(Iter it) {
  //     (   Iter   *  ++)
  *it++;
 
  //    -  
  requires std::convertible_to<decltype(*it++), typename Iter::value_type>;
 
  //    -  
  requires std::is_convertible_v<decltype(*it++), typename Iter::value_type>;
}

Al mismo tiempo, se permiten expresiones obligatorias anidadas con c贸digo entre llaves, que se verifica para verificar su validez. Sin embargo, si simplemente escribe una expresi贸n requerida dentro de otra, entonces se verificar谩 la validez de la expresi贸n anidada (todo en su conjunto, incluida la palabra clave requerida anidada):

requires (T v) { 
  requires (typename T::value_type x) { ++x; }; //     , 
												//     !
};  

Por lo tanto, surgi贸 una forma extra帽a con doble:

requires (T v) { 
  requires requires (typename T::value_type x) { ++x; }; //       "++x"
};  

Aqu铆 hay una secuencia de escape tan divertida de "requiere".

Por cierto, otra combinaci贸n de dos requisitos es esta cl谩usula de tiempo (ver m谩s abajo) y expresi贸n:

template <typename T>
  requires requires(T x, T y) { bool(x < y); }
bool equivalent(T const& x, T const& y)
{
  return !(x < y) && !(y < x);
};

requiere cl谩usula


Ahora pasemos a otro uso de la palabra requiere: declarar restricciones de un tipo de plantilla. Esta es una alternativa al uso de nombres de concepto en lugar de typename. En el siguiente ejemplo, los tres m茅todos son equivalentes:

//  require
template<typename Cont>
	requires Sortable<Cont>
void sort(Cont& container);

//   require (  )
template<typename Cont>
void sort(Cont& container) requires Sortable<Cont>;

//    typename
template<Sortable Cont>
void sort(Cont& container)  

La declaraci贸n requerida puede usar varios predicados combinados por operadores l贸gicos.

template <typename T>
  requires is_standard_layout_v<T> && is_trivial_v<T>
void fun(T v); 
 
int main()
{
  std::string s;
 
  fun(1);  // ok
  fun(s);  // compiler error
}

Sin embargo, solo invierta una de las condiciones, ya que se produce un error de compilaci贸n:

template <typename T>
  requires is_standard_layout_v<T> && !is_trivial_v<T>
void fun(T v); 

Aqu铆 hay un ejemplo que tampoco compilar谩

template <typename T>
  requires !is_trivial_v<T>
void fun(T v);	

La raz贸n de esto son las ambig眉edades que surgen al analizar algunas expresiones. Por ejemplo, en dicha plantilla:

template <typename T> 
  requires (bool)&T::operator short unsigned int foo();

no est谩 claro a qu茅 atribuir sin firmar: el operador o el prototipo de la funci贸n foo (). Por lo tanto, los desarrolladores decidieron que sin par茅ntesis, ya que los argumentos requieren una cl谩usula, solo se puede usar un conjunto muy limitado de entidades: literales verdaderos o falsos, nombres de campo del tipo bool del valor del formulario, valor, T :: valor, ns :: trait :: value, Nombres de concepto del tipo Concepto y requiere expresiones. Todo lo dem谩s debe estar entre par茅ntesis:

template <typename T>
  requires (!is_trivial_v<T>)
void fun(T v);

Ahora sobre las caracter铆sticas del predicado en la cl谩usula require


Considere otro ejemplo.

template <typename T>
  requires is_trivial_v<typename T::value_type> 
void fun(T v); 

En este ejemplo, require utiliza un rasgo que depende del tipo value_type anidado. No se sabe de antemano si un tipo arbitrario tiene un tipo anidado que se puede pasar a la plantilla. Si pasa, por ejemplo, un tipo int simple a dicha plantilla, habr谩 un error de compilaci贸n, sin embargo, si tenemos dos especializaciones de la plantilla, entonces no habr谩 error; solo se elegir谩 otra especializaci贸n.

template <typename T>
  requires is_trivial_v<typename T::value_type> 
void fun(T v) { std::cout << "1"; } 
 
template <typename T>
void fun(T v) { std::cout << "2"; } 
 
int main()
{
  fun(1);  // displays: "2"
}

Por lo tanto, la especializaci贸n se descarta no solo cuando el predicado de la cl谩usula require devuelve falso, sino tambi茅n cuando resulta ser incorrecto.

Los par茅ntesis alrededor del predicado son un recordatorio importante de que en la cl谩usula require el inverso del predicado no es lo opuesto al predicado en s铆. Entonces,

requires is_trivial_v<typename T::value_type> 

significa que el rasgo es correcto y devuelve verdadero. Donde

!is_trivial_v<typename T::value_type> 

significar铆a "el rasgo es correcto y devuelve falso" La
inversi贸n l贸gica real del primer predicado NO es ("el rasgo es correcto y devuelve verdadero") == "el rasgo es INCORRECTO o devuelve falso" - esto se logra de una manera un poco m谩s compleja - a trav茅s de una definici贸n expl铆cita del concepto:

template <typename T>
concept value_type_valid_and_trivial 
  = is_trivial_v<typename T::value_type>; 
 
template <typename T>
  requires (!value_type_valid_and_trivial<T>)
void fun(T v); 

Conjunci贸n y disyunci贸n


Los operadores l贸gicos de conjunci贸n y disyunci贸n se ven como de costumbre, pero en realidad funcionan un poco diferente que en C ++ normal.

Considere dos fragmentos de c贸digo muy similares.

El primero es un predicado sin par茅ntesis:

template <typename T, typename U>
  requires std::is_trivial_v<typename T::value_type>
		|| std::is_trivial_v<typename U::value_type>
void fun(T v, U u); 

El segundo es con par茅ntesis:

template <typename T, typename U>
  requires (std::is_trivial_v<typename T::value_type>
		 || std::is_trivial_v<typename U::value_type>)
void fun(T v, U u); 

La diferencia est谩 solo entre par茅ntesis. Pero debido a esto, en la segunda plantilla, no hay dos restricciones unidas por una "cl谩usula de exigencia", sino una unida por un OR l贸gico habitual.

Esta diferencia es la siguiente. Considera el c贸digo

std::optional<int> oi {};
int i {};
fun(i, oi);

Aqu铆 la plantilla se instancia por los tipos int y std :: opcional.

En el primer caso, el tipo int :: value_type no es v谩lido y, por lo tanto, la primera limitaci贸n no se cumple.

Pero el tipo opcional :: value_type es v谩lido, el segundo rasgo devuelve verdadero, y dado que hay un operador OR entre las restricciones, todo el predicado se satisface como un todo.

En el segundo caso, esta es una expresi贸n 煤nica que contiene un tipo no v谩lido, por lo que no es v谩lido en general y el predicado no est谩 satisfecho. Entonces, los corchetes simples cambian imperceptiblemente el significado de lo que est谩 sucediendo.

En conclusi贸n


Por supuesto, no todas las caracter铆sticas de los conceptos se muestran aqu铆. Simplemente no fui m谩s all谩. Pero como primera impresi贸n, una idea muy interesante y una implementaci贸n confusa algo extra帽a. Y una sintaxis divertida con repeticiones requiere, lo que realmente confunde. 驴Hay realmente tan pocas palabras en ingl茅s que tuvo que usar una palabra para prop贸sitos completamente diferentes?

La idea con el c贸digo compilado es definitivamente buena. Incluso es algo similar a "cuasi-citando" en macros de sintaxis. 驴Pero vali贸 la pena mezclar la sintaxis especial para verificar los tipos de retorno? En mi humilde opini贸n, para esto simplemente ser铆a necesario hacer una palabra clave por separado.

Mezcla impl铆cita de los conceptos "verdadero / falso" y "compila / no compila" en un mont贸n, y como resultado, las bromas entre par茅ntesis tambi茅n est谩n mal. Estos son conceptos diferentes, y deben existir estrictamente en diferentes contextos (aunque entiendo de d贸nde vino, de la regla SFINAE, donde el c贸digo no compilado excluy贸 silenciosamente la especializaci贸n de la consideraci贸n). Pero si el objetivo de los conceptos es hacer que el c贸digo sea lo m谩s expl铆cito posible, 驴vali贸 la pena arrastrar todas estas cosas impl铆citas a nuevas caracter铆sticas?

El art铆culo est谩 escrito principalmente sobre la base de
akrzemi1.wordpress.com/2020/01/29/requires-expression
akrzemi1.wordpress.com/2020/03/26/requires-clause
(hay muchos m谩s ejemplos y caracter铆sticas interesantes)
con mis adiciones de otras fuentes,
todos los ejemplos se pueden consultarwandbox.org

All Articles