Reputation: 95
Code:
#include <iostream>
using std::nullptr_t;
template<typename... T>
using nullptr_vt = nullptr_t;
struct not_addable{};
template<
typename T,
nullptr_vt<decltype(std::declval<T>() + std::declval<T>())> TSfinae = nullptr>
bool test_addable(int)
{ return true; }
template<typename>
bool test_addable(...)
{ return false; }
int main()
{
std::cout << std::boolalpha;
std::cout << test_addable<int>(0) << std::endl;
std::cout << test_addable<not_addable>(0) << std::endl;
// Gives error ("invalid operands to binary expression"):
// nullptr_vt<decltype(std::declval<not_addable>() + std::declval<not_addable>())> a{};
}
I thought this would print:
true
false
, but it doesn't. It prints:
true
true
. At least on https://repl.it/@Hrle/sfinaetemplatesuccess.
I thought that nullptr_vt<decltype(std::declval<T>() + std::declval<T>())>
from the first overload would be an error for not_addable
and it would discard it from the overload set, thus choosing the second overload.
Does the compiler have the ability to discard the type of TSfinae
if there is a default?
Upvotes: 6
Views: 166
Reputation: 16805
This doesn't explain the problem, and it does not pretend to be better than @Useless answer, but it is an alternative solution I find convenient.
I replace the typename
by an integer in order to save a bit of writing, and use the comma operator in order to enumerate many conditions if necessary.
Of course, an alias declaration with using
can help increase readability when the same conditions have to be used many times.
EDIT
As suggested by @StoryTeller comment, if we declare an operator,
that combines with the last 1
, then that 1
will be consumed and we can emit instead in decltype()
a type that will make SFINAE fail.
He suggests inserting a void()
in the sequence of conditions just before the 1
.
Actually, it is not possible to declare an operator,
without a right-hand-side operand; thus nothing will combine with this void()
and finally 1
will be emitted in decltype()
.
It's not as minimal as just 1
, but it's safer.
/**
g++ -std=c++17 -o prog_cpp prog_cpp.cpp \
-pedantic -Wall -Wextra -Wconversion -Wno-sign-conversion \
-g -O0 -UNDEBUG -fsanitize=address,undefined
**/
#include <iostream>
struct A
{
A operator+(A r);
A operator-(A r);
A operator,(int r); // try to mislead SFINAE
};
struct B
{
B operator+(B r);
// no -
};
struct C
{
// no +
// no -
};
template<
typename T,
decltype((std::declval<T>()+std::declval<T>()),
void(),1) =1>
bool test_add(int)
{ return true; }
template<typename>
bool test_add(...)
{ return false; }
template<
typename T,
decltype((std::declval<T>()+std::declval<T>()),
(std::declval<T>()-std::declval<T>()),
void(),1) =1>
bool test_add_sub(int)
{ return true; }
template<typename>
bool test_add_sub(...)
{ return false; }
template<typename T>
using has_add =
decltype((std::declval<T>()+std::declval<T>()),
void(),1);
template<typename T>
using has_add_sub =
decltype((std::declval<T>()+std::declval<T>()),
(std::declval<T>()-std::declval<T>()),
void(),1);
template<
typename T,
has_add<T> =1>
bool test_add2(int)
{ return true; }
template<typename>
bool test_add2(...)
{ return false; }
template<
typename T,
has_add_sub<T> =1>
bool test_add_sub2(int)
{ return true; }
template<typename>
bool test_add_sub2(...)
{ return false; }
int main()
{
std::cout << std::boolalpha;
std::cout << "test_add<int>(0) " << test_add<int>(0) << '\n';
std::cout << "test_add<A>(0) " << test_add<A>(0) << '\n';
std::cout << "test_add<B>(0) " << test_add<B>(0) << '\n';
std::cout << "test_add<C>(0) " << test_add<C>(0) << '\n';
std::cout << "test_add_sub<int>(0) " << test_add_sub<int>(0) << '\n';
std::cout << "test_add_sub<A>(0) " << test_add_sub<A>(0) << '\n';
std::cout << "test_add_sub<B>(0) " << test_add_sub<B>(0) << '\n';
std::cout << "test_add_sub<C>(0) " << test_add_sub<C>(0) << '\n';
std::cout << "test_add2<int>(0) " << test_add2<int>(0) << '\n';
std::cout << "test_add2<A>(0) " << test_add2<A>(0) << '\n';
std::cout << "test_add2<B>(0) " << test_add2<B>(0) << '\n';
std::cout << "test_add2<C>(0) " << test_add2<C>(0) << '\n';
std::cout << "test_add_sub2<int>(0) " << test_add_sub2<int>(0) << '\n';
std::cout << "test_add_sub2<A>(0) " << test_add_sub2<A>(0) << '\n';
std::cout << "test_add_sub2<B>(0) " << test_add_sub2<B>(0) << '\n';
std::cout << "test_add_sub2<C>(0) " << test_add_sub2<C>(0) << '\n';
return 0;
}
Upvotes: 1
Reputation: 67723
I thought that
nullptr_vt<decltype(std::declval<T>() + std::declval<T>())>
from the first overload would be an error for not_addable and it would discard it from the overload set, thus choosing the second overload.
The idea is actually fine, the problem is just with GCC and nullptr_vt
This line:
nullptr_vt<decltype(std::declval<T>() + std::declval<T>())> TSfinae = nullptr
works where you don't want it to on GCC 10.2 but is correct on Clang 11.0.1. Changing it to
nullptr_vt<decltype(std::declval<T>() + std::declval<T>())> *TSfinae = nullptr
is correct on both, as are the simpler
typename TSfinae = nullptr_vt<decltype(std::declval<T>() + std::declval<T>())>
typename _ = decltype(std::declval<T>() + std::declval<T>())
And finally the make_void trick
template<typename... T> struct make_nullptr_vt { using type = nullptr_t; };
template<typename T>
using nullptr_vt = typename make_nullptr_vt<T>::type;
fixes the original version on GCC as well.
Upvotes: 3