¿Es la modificación de un concepto una mejora o una mala práctica?
Parece que puede poner lambda en el concepto y luego escribir código en él. Tomemos esto como ejemplo. Preferiré los conceptos estándar para tales conceptos y tener en cuenta que esto es solo para los propósitos de este ejemplo: godbolt
template<class T>
concept labdified_concept =
requires {
[](){
T t, tt; // default constructible
T ttt{t}; // copy constructible
tt = t; //copy assignable
tt = std::move(t); // move assignable
};
};
En vez de:
template<class T>
concept normal_concept =
std::default_initializable<T> && std::movable<T> && std::copy_constructible<T>;
¿Es la lambdificación una mejora o una mala práctica? También desde el punto de legibilidad.
Respuestas
Esto no debería ser válido. El punto de permitir lambdas en contextos no evaluados no era permitir repentinamente SFINAE en declaraciones.
Tenemos algunas palabras en [temp.deduct] / 9 que aclaran esto:
Una expresión lambda que aparece en un tipo de función o un parámetro de plantilla no se considera parte del contexto inmediato a los efectos de la deducción del argumento de plantilla. [ Nota : La intención es evitar requerir implementaciones para lidiar con fallas de sustitución que involucren declaraciones arbitrarias. [ Ejemplo :
template <class T> auto f(T) -> decltype([]() { T::invalid; } ()); void f(...); f(0); // error: invalid expression not part of the immediate context template <class T, std::size_t = sizeof([]() { T::invalid; })> void g(T); void g(...); g(0); // error: invalid expression not part of the immediate context template <class T> auto h(T) -> decltype([x = T::invalid]() { }); void h(...); h(0); // error: invalid expression not part of the immediate context template <class T> auto i(T) -> decltype([]() -> typename T::invalid { }); void i(...); i(0); // error: invalid expression not part of the immediate context template <class T> auto j(T t) -> decltype([](auto x) -> decltype(x.invalid) { } (t)); // #1 void j(...); // #2 j(0); // deduction fails on #1, calls #2
- ejemplo final ] - nota final ]
Simplemente no tenemos algo equivalente para los requisitos. El comportamiento de gcc es realmente el que esperarías:
template <typename T> concept C = requires { []{ T t; }; };
struct X { X(int); };
static_assert(!C<X>); // ill-formed
Debido a que el cuerpo de la lambda está fuera del contexto inmediato, no es un error de sustitución, es un error grave.
Ignorando las obvias fallas de legibilidad en este mecanismo, en realidad no funciona . Considera lo siguiente:
template<labdified_concept T>
void foo(T t) {}
template<typename T>
void foo(T t) {}
Las reglas de los conceptos nos dicen que si un dado T
no satisface labdified_concept
, entonces el otro foo
debería ser instanciado en su lugar. Pero eso no es lo que sucede si proporcionamos SS
tal plantilla. En cambio, obtenemos un error grave porque labdified_concept<SS>
no se puede crear una instancia.
El contenido de una requires
expresión tiene un manejo especial que permite que ciertos tipos de errores se consideren fallas para cumplir con el requisito. Pero ese manejo no se aplica al cuerpo de una lambda. Allí, el código mal formado está mal formado y, por lo tanto, obtiene un error de compilación al intentar crear una instancia.
E incluso si funcionó, todavía no funciona. Los conceptos tienen reglas complejas para la subsunción de conceptos, lo que permite considerar diferentes conceptos más altamente especializados que otros. Esto permite sobrecargar diferentes conceptos , lo que permite llamar al concepto más restringido. Por ejemplo, un concepto que solo requiere default_initializable
es más genérico que uno que requiere default_initializable
y moveable
. Así, si un tipo cumple con ambos, se tomará este último porque es más restringido.
Pero esto solo funciona debido a las reglas especiales para concept
s. Ocultar requisitos en lambdas no permitiría que esto funcionara.