c++ templates sfinae decltype c++1z

c++ - void_t y tipo de retorno final con decltype: ¿son completamente intercambiables?



templates sfinae (2)

Este es el equivalente de metaprogramación de: ¿debería escribir una función o debería simplemente escribir mi código en línea? Las razones para preferir escribir un rasgo de tipo son las mismas que las razones para preferir escribir una función: es más autodocumentable, es reutilizable, es más fácil de depurar. Las razones para preferir escribir decltype final son similares a las razones para preferir escribir código en línea: es una operación única que no es reutilizable, entonces ¿por qué poner el esfuerzo de factorizarlo y encontrar un nombre sensato para él? ?

Pero aquí hay un montón de razones por las que podría querer un rasgo de tipo:

Repetición

Supongamos que tengo un rasgo que quiero comprobar muchas veces. Como fooable Si escribo el rasgo de tipo una vez, puedo tratarlo como un concepto:

template <class, class = void> struct fooable : std::false_type {}; template <class T> struct fooable<T, void_t<decltype(std::declval<T>().foo())>> : std::true_type {};

Y ahora puedo usar ese mismo concepto en toneladas de lugares:

template <class T, std::enable_if_t<fooable<T>{}>* = nullptr> void bar(T ) { ... } template <class T, std::enable_if_t<fooable<T>{}>* = nullptr> void quux(T ) { ... }

Para conceptos que verifican más de una expresión, no desea tener que repetirlo cada vez.

Compostabilidad

Seguir la repetición, componer dos rasgos de tipo diferentes es fácil:

template <class T> using fooable_and_barable = std::conjunction<fooable<T>, barable<T>>;

La composición de dos tipos de devolución final requiere escribir todas las expresiones ...

Negación

Con un rasgo de tipo, es fácil verificar que un tipo no satisfaga un rasgo. Eso es solo !fooable<T>::value . No puede escribir una expresión de tipo decltype para verificar que algo no sea válido. Esto podría surgir cuando tienes dos sobrecargas disjuntas:

template <class T, std::enable_if_t<fooable<T>::value>* = nullptr> void bar(T ) { ... } template <class T, std::enable_if_t<!fooable<T>::value>* = nullptr> void bar(T ) { ... }

que conduce muy bien en ...

Despacho de etiqueta

Suponiendo que tenemos un rasgo de tipo corto, es mucho más claro etiquetar el envío con un rasgo de tipo:

template <class T> void bar(T , std::true_type fooable) { ... } template <class T> void bar(T , std::false_type not_fooable) { ... } template <class T> void bar(T v) { bar(v, fooable<T>{}); }

de lo que sería de otra manera:

template <class T> auto bar(T v, int ) -> decltype(v.foo(), void()) { ... } template <class T> void bar(T v, ... ) { ... } template <class T> void bar(T v) { bar(v, 0); }

El 0 e int/... es un poco extraño, ¿verdad?

static_assert

¿Qué pasa si no quiero SFINAE en un concepto, sino simplemente quiero fracasar con un mensaje claro?

template <class T> struct requires_fooability { static_assert(fooable<T>{}, "T must be fooable!"); };

Conceptos

¿Cuándo (si?) Alguna vez obtenemos conceptos, obviamente el uso de conceptos es mucho más poderoso cuando se trata de todo lo relacionado con la metaprogramación:

template <fooable T> void bar(T ) { ... }

Considere el siguiente ejemplo básico basado en void_t :

template<typename, typename = void_t<>> struct S: std::false_type {}; template<typename T> struct S<T, void_t<decltype(std::declval<T>().foo())>>: std::true_type {};

Se puede usar como sigue:

template<typename T> std::enable_if_t<S<T>::value> func() { }

Lo mismo se puede hacer usando el tipo de devolución final y decltype :

template<typename T> auto func() -> decltype(std::declval<T>().foo(), void()) { }

Esto es cierto para todos los ejemplos que pensé. Fallé al encontrar un caso en el que void_t o el tipo de devolución final con decltype se pueden usar, mientras que su contraparte no puede.
Los casos más complejos se pueden resolver con una combinación de tipo de retorno final y sobrecarga (por ejemplo, cuando el detector se usa para cambiar entre dos funciones en lugar de como un disparador para desactivar o habilitar algo).

¿Es este el caso? ¿Son ( void_t y decltype como el tipo de retorno final más la sobrecarga si es necesario) completamente intercambiables?
De lo contrario, ¿cuál es el caso en el que uno no puede usarse para evitar las restricciones y me veo obligado a usar un método específico?


Usé void_t y declintpe final cuando estaba implementando mi propia versión homebrew de Concepts Lite (tuve éxito, dicho sea de paso), que requería la creación de muchos rasgos de tipo adicionales, la mayoría de los cuales utilizan la expresión de detección de una manera u otra. Utilicé void_t, declintpe final y decltype anterior.

Por lo que yo entiendo, estas opciones son lógicamente equivalentes, por lo que un compilador ideal del 100% debe producir el mismo resultado usando todas ellas. El problema, sin embargo, es que un compilador particular puede (y seguirá) diferentes patrones de creación de instancias en diferentes casos, y algunos de estos patrones pueden ir más allá de los límites internos del compilador. Por ejemplo, cuando traté de hacer que MSVC 2015 Update 2 3 detectara la presencia de multiplicación por el mismo tipo, la única solución que funcionaba era anterior a decltype:

template<typename T> struct has_multiplication { static no_value test_mul(...); template<typename U> static decltype(*(U*)(0) *= std::declval<U>() * std::declval<U>()) test_mul(const U&); static constexpr bool value = !std::is_same<no_value, decltype(test_mul(std::declval<T>())) >::value; };

Cada otra versión producía errores internos del compilador, aunque algunos funcionaban bien con Clang y GCC. También tuve que usar *(U*)(0) lugar de declval , porque usar tres declval seguidos, aunque perfectamente legales, fue demasiado para el compilador en este caso particular.

Mi mal, lo olvidé. En realidad usé *(U*)(0) porque declval produce rvalue-ref de tipo, que no se puede asignar, y es por eso que usé esto. Pero todo lo demás sigue siendo válido, esta versión funcionó donde otros no.

Así que por ahora mi respuesta sería: "son idénticos, siempre y cuando el compilador piense que son". Y esto es algo que tienes que descubrir probando. Espero que esto deje de ser un problema en los siguientes lanzamientos de MSVC y otros.