Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ SFINAE not failing

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?

like image 890
Hrvoje Jurić Avatar asked Feb 18 '21 14:02

Hrvoje Jurić


People also ask

Will concepts replace SFINAE?

So the simple answer is YES.

What is meant by SFINAE?

Substitution failure is not an error (SFINAE) refers to a situation in C++ where an invalid substitution of template parameters is not in itself an error. David Vandevoorde first introduced the acronym SFINAE to describe related programming techniques.

How SFINAE works?

This rule applies during overload resolution of function templates: When substituting the explicitly specified or deduced type for the template parameter fails, the specialization is discarded from the overload set instead of causing a compile error. This feature is used in template metaprogramming.

Why use SFINAE?

SFINAE is a bit like a windmill. It sits as a wart in the middle of an interface, BUT it's useful to create elaborate static polymorphism, in particular before C++17 and if constexpr , and even in some use cases in C++17.


2 Answers

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.

like image 90
Useless Avatar answered Oct 19 '22 10:10

Useless


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;
}
like image 38
prog-fh Avatar answered Oct 19 '22 09:10

prog-fh