Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why this SFINAE snippet is not working in g++, but working in MSVC?

In MSVC2017 this works fine, both static_asserts are NOT triggered as expected:

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");
}

However, if I compile in g++7.1 or clang 4.0 I get following compiler error:

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'

From my understanding of SFINAE, substition of true_type returning function should fail for int parameter and next function shall be chosen, like it is done in MSVC. Why clang and g++ are not compiling it at all?

I've compiled with -std=c++17 switch only, maybe something more is needed?

like image 946
Starl1ght Avatar asked Jun 08 '17 11:06

Starl1ght


4 Answers

SFINAE doesn't work here, as the class is already instantiated with T = int in do_have_size<int>::type. SFINAE works only for a list of template function candidates, in your case you'll get a hard error since in the instantiation

do_have_size<int>::type

the member function

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

is surely ill-formed for int. The

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

won't ever be considered. So gcc is right here in rejecting your code and MSVC2017 should not accept the code.

Related: std::enable_if : parameter vs template parameter and SFINAE working in return type but not as template parameter

One solution is to use the magic of void_t (since C++17, but you can define your own in C++11/14), which maps any type list to void and enables crazy simple-looking SFINAE techniques, like so

#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");
}

Live on Wandbox

Here is a Cppcon video by Walter Brown which explains the void_t techniques in great detail, I highly recommend it!

like image 169
vsoftco Avatar answered Nov 16 '22 23:11

vsoftco


This has absolutely nothing to do with whether default template arguments are part of a function template's signature.

The real problem is that T is a class template parameter, and when you instantiate the class template's definition, the implementation can immediately substitute it into your default template argument, decltype(std::declval<T>().size()) outside of template argument deduction, which causes a hard error if size is not present.

The fix is simple; simply make it depend on a parameter of the function template.

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

(There are other problems with your implementation, such as it requires a move-constructible non-abstract T and doesn't require size() to be const-callable, but they aren't the cause of the error you are seeing.)

like image 35
T.C. Avatar answered Nov 16 '22 23:11

T.C.


@vsoftco answered "gcc is right to reject your code". I agree.

To fix, I say do this:

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...>;

This is a can_apply library that makes this kind of SFINAE simple.

Now writing one of these traits is as simple as:

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 >;

test code:

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

Live example.

In C++17 you can move over to less declval filled expressions.

#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 takes a function f and returns a "invokation tester". The invokation tester takes arguments, then returns true_type if those arguments would be valid to pass to f, and false_type otherwise.

RETURNS makes it easy to make a single-statement lambda SFINAE friendly. And in C++17, the lambda's operations are constexpr if possible (which is why we need C++17 here).

Then, this gives us:

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

Now, often we are doing this because we want to call .size() if possible, and otherwise return 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)...);
}

now we can

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; }
  );
}

As it happens, @Barry has proposed a feature in C++20 that replaces [](auto&& f) RETURNS(f.size()) with [](auto&& f)=>f.size() (and more).

like image 5
Yakk - Adam Nevraumont Avatar answered Nov 17 '22 01:11

Yakk - Adam Nevraumont


I got it to work by using std::enable_if to SFINAE away the template version of check based on either the parameter or the return type. The condition I used was std::is_fundamental to exclude int, float and other non-class types from instantiating the template. I used the -std=c++1z flag to clang and gcc. I expect -std=c++14 should work too.

#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");
}
like image 2
Cerius Avatar answered Nov 16 '22 23:11

Cerius