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?
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!
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.)
@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).
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");
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With