C++ SFINAE not failing
Asked Answered
J

2

6

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?

Jutland answered 18/2, 2021 at 14:3 Comment(3)
If I had to venture a guess, it would be that the compiler being used there (I can't access it to see for myself), doesn't resolve the issue mentioned here. And trying the proposed fix on an old version of Clang, does the trick wandbox.org/permlink/8pOrfXpPj0nQA4OfTurves
fwiw, I can reproduce your outcome with gcc, but clang is true false godbolt.org/z/KjdecTPachalic
on gcc if you modify decltype(std::declval<T>() + std::declval<T>())> TSfinae = nullptr to decltype(std::declval<T>() + std::declval<T>())>* TSfinae = nullptr it works...Recording
H
3

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.

Hypostatize answered 18/2, 2021 at 14:28 Comment(1)
Thank you very much! The reason I'm not going for the simpler solutions is because the user can put whatever they want there (as far as I know) and it will work. The last one is the one I like the most, because I have to do the least amount of modifications in my code.Impart
F
1

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;
}
Fir answered 18/2, 2021 at 15:6 Comment(3)
Ok actually, I like this even more than the accepted answer (it will remain accepted as it solves the issue). I switched to the decltype(expression, nullptr) = nullptr form in my templates. It was quite easy, it looks cleaner and @Hypostatize answer would break my code in certain situations for which I had to introduce workarounds.Impart
If I was malicious (or just short sighted), I'd throw into this a type that has an operator, (yes a comma) overload. You can make it hardier by replacing each 1 with void(), 1. This will force the use of the built-in comma.Turves
@StoryTeller-UnslanderMonica Thanks for the good catch. I edited accordingly.Fir

© 2022 - 2024 — McMap. All rights reserved.