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");
}
Here hay un video de Cppcon de Walter Brown que explica las técnicas de void_t
con gran detalle, ¡lo recomiendo altamente!