c++ c++11 templates sfinae

c++ - ¿Por qué este fragmento de código SFINAE no funciona en g++, pero funciona en MSVC?



c++11 templates (4)

En MSVC2017 esto funciona bien, ambas static_asserts NO se activan como se esperaba:

template <typename T> struct do_have_size { template <typename = decltype(std::declval<T>().size())> static std::true_type check(T); static std::false_type check(...); using type = decltype(check(std::declval<T>())); }; int main() { using TR = typename do_have_size<std::vector<int>>::type; using FL = typename do_have_size<int>::type; static_assert(std::is_same<TR, std::true_type>::value, "TRUE"); static_assert(std::is_same<FL, std::false_type>::value, "FALSE"); }

Sin embargo, si compilo en g ++ 7.1 o clang 4.0 obtengo el siguiente error de compilación:

In instantiation of ''struct do_have_size<int>'': 20:39: required from here 9:24: error: request for member ''size'' in ''declval<do_have_size<int>::TP>()'', which is of non-class type ''int''

Desde mi entendimiento de SFINAE, la true_type función de retorno de tipo true_type debería fallar para el parámetro int y la siguiente función será elegida, como se hace en MSVC. ¿Por qué clang y g ++ no lo están compilando en absoluto?

He compilado solo con el -std=c++17 , ¿quizás se necesita algo más?


@vsoftco respondió "gcc tiene razón al rechazar su código". Estoy de acuerdo.

Para arreglarlo, digo hacer esto:

namespace details { template<template<class...>class Z, class, class...Ts> struct can_apply:std::false_type{}; template<class...>struct voider{using type=void;}; template<class...Ts>using void_t = typename voider<Ts...>::type; template<template<class...>class Z, class...Ts> struct can_apply<Z, void_t<Z<Ts...>>, Ts...>:std::true_type{}; } template<template<class...>class Z, class...Ts> using can_apply=details::can_apply<Z,void,Ts...>;

Esta es una biblioteca can_apply que hace que este tipo de SFINAE sea simple.

Ahora escribir uno de estos rasgos es tan simple como:

template<class T> using dot_size_r = decltype( std::declval<T>().size() ); template<class T> using has_dot_size = can_apply< dot_size_r, T >;

código de prueba:

int main() { static_assert( has_dot_size<std::vector<int>>{}, "TRUE" ); static_assert( !has_dot_size<int>{}, "FALSE" ); }

Ejemplo vivo.

En C ++ 17 puede pasar a expresiones rellenas con menos declval.

#define RETURNS(...) / noexcept(noexcept(__VA_ARGS__)) / -> decltype(__VA_ARGS__) / { return __VA_ARGS__; } template<class F> constexpr auto can_invoke(F&&) { return [](auto&&...args) { return std::is_invocable< F(decltype(args)...) >{}; }; }

can_invoke toma una función f y devuelve un "probador de invocación". El probador de invocación toma argumentos, luego devuelve true_type si esos argumentos serían válidos para pasar a f , y false_type contrario.

RETURNS hace que sea fácil hacer que un SFINAE lambda de declaración única sea amigable. Y en C ++ 17, las operaciones de la lambda son constexpr si es posible (razón por la cual necesitamos C ++ 17 aquí).

Entonces, esto nos da:

template<class T> constexpr auto can_dot_size(T&& t) { return can_invoke([](auto&& x) RETURNS(x.size()))(t); }

Ahora, a menudo estamos haciendo esto porque queremos llamar a .size() si es posible, y de lo contrario devolveremos 0.

template<class T, class A, class...Bs> decltype(auto) call_first_valid(T&& t, A&& a, Bs&&...bs) { if constexpr( can_invoke(std::forward<A>(a))(std::forward<T>(t)) ) { return std::forward<A>(a)(std::forward<T>(t)); else return call_first_valid(std::forward<T>(t), std::forward<Bs>(bs)...); }

Ahora podemos

template<class T> std::size_t size_at_least( T&& t ) { return call_first_valid( std::forward<T>(t), [](auto&& t) RETURNS(t.size()), [](auto&&)->std::size_t { return 0; } ); }

A medida que sucede, @Barry ha propuesto una función en C ++ 20 que reemplaza a [](auto&& f) RETURNS(f.size()) con [](auto&& f)=>f.size() (y más).


Conseguí que funcionara utilizando std::enable_if para que SFINAE elimine la versión de la plantilla de cheque según el parámetro o el tipo de devolución. La condición que utilicé fue std::is_fundamental para excluir int, float y otros tipos que no sean de clase de la creación de instancias de la plantilla. -std=c++1z bandera -std=c++1z para clang y gcc. Espero que -std=c++14 también funcione.

#include <type_traits> #include <utility> #include <vector> template <typename T> struct do_have_size { static std::false_type check(...); template <typename U = T, typename = decltype(std::declval<U>().size())> static std::true_type check(std::enable_if_t<!std::is_fundamental<U>::value, U>); // OR //template <typename U = T, typename = decltype(std::declval<U>().size())> //static auto check(U) // -> std::enable_if_t<!std::is_fundamental<U>::value, std::true_type>; using type = decltype(check(std::declval<T>())); }; int main() { using TR = typename do_have_size<std::vector<int>>::type; using FL = typename do_have_size<int>::type; static_assert(std::is_same<TR, std::true_type>::value, "TRUE"); static_assert(std::is_same<FL, std::false_type>::value, "FALSE"); }


Esto no tiene absolutamente nada que ver con si los argumentos de la plantilla predeterminada son parte de la firma de una plantilla de función.

El problema real es que T es un parámetro de plantilla de clase, y cuando se crea una instancia de la definición de la plantilla de clase, la implementación puede sustituirlo inmediatamente en su argumento de plantilla predeterminada, decltype(std::declval<T>().size()) de la deducción de argumentos de la plantilla, que causa un error grave si el size no está presente

La solución es simple; simplemente haga que dependa de un parámetro de la plantilla de función.

template <typename U, typename = decltype(std::declval<U>().size())> static std::true_type check(U);

(Hay otros problemas con su implementación, como que requiere una T no abstracta no construible por movimiento y no requiere que el size() sea ​​constante, pero no son la causa del error que está viendo).


SFINAE no funciona aquí, ya que la clase ya está instanciada con T = int en do_have_size<int>::type . SFINA funciona solo para una lista de candidatos de función de plantilla, en su caso obtendrá un error grave ya que en la instanciación

do_have_size<int>::type

la función miembro

template <typename = decltype(std::declval<int>().size())> static std::true_type check(T);

seguramente está mal formado para int . los

static std::false_type check(...);

Nunca será considerado. Entonces, gcc está aquí al rechazar su código y MSVC2017 no debería aceptar el código.

Relacionado: std :: enable_if: parámetro vs parámetro de plantilla y SFINAE trabajando en el tipo de retorno pero no como parámetro de plantilla

Una solución es usar la magia de void_t (desde C ++ 17, pero puede definir la suya propia en C ++ 11/14), que mapea cualquier tipo de lista para void y habilita técnicas SFINAE de aspecto simple y loco, como

#include <utility> #include <vector> template<typename...> using void_t = void; // that''s how void_t is defined in C++17 template <typename T, typename = void> struct has_size : std::false_type {}; template <typename T> struct has_size<T, void_t<decltype(std::declval<T>().size())>> : std::true_type {}; int main() { using TR = typename has_size<std::vector<int>>::type; using FL = typename has_size<int>::type; static_assert(std::is_same<TR, std::true_type>::value, "TRUE"); static_assert(std::is_same<FL, std::false_type>::value, "FALSE"); }

Vive en Wandbox

Here hay un video de Cppcon de Walter Brown que explica las técnicas de void_t con gran detalle, ¡lo recomiendo altamente!