Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to simplify complicated SFINAE syntax, in pre-C++11, C++11, 14 and 17?

This question was inspired by this answer. I wonder what are/were the best ways to simplify it in given standards. One I know and personally used/still use since C++14 is macro REQUIRES(x):

With definition:

template<long N>
struct requires_enum
{
    enum class type
    {
        none,
        all
    };
};

#define REQUIRES(...) requires_enum<__LINE__>::type = \
                      requires_enum<__LINE__>::type::none, \
                      bool PrivateBool = true, \
                      typename std::enable_if<PrivateBool && (__VA_ARGS__), int>::type = 0

And use if even for non-templated function calls:

template<REQUIRES(sizeof(int)==4)>
int fun() {return 0;}

int main()
{ 
    fun(); //only if sizeof(int)==4
}

The original REQUIRES I use is from this post.

What are the other good techniques?


Some examples of SFINAE that require some, or long time to understand for reader that just started the adventure with SFINAE:

Pre-C++11 SFINAE example (Source):

template <typename T>
struct has_typedef_foobar {
    // Types "yes" and "no" are guaranteed to have different sizes,
    // specifically sizeof(yes) == 1 and sizeof(no) == 2.
    typedef char yes[1];
    typedef char no[2];

    template <typename C>
    static yes& test(typename C::foobar*);

    template <typename>
    static no& test(...);

    // If the "sizeof" of the result of calling test<T>(nullptr) is equal to sizeof(yes),
    // the first overload worked and T has a nested type named foobar.
    static const bool value = sizeof(test<T>(nullptr)) == sizeof(yes);
};
like image 421
xinaiz Avatar asked Sep 27 '16 14:09

xinaiz


People also ask

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.

Will concepts replace SFINAE?

So the simple answer is YES.

What is Typename C++?

" typename " is a keyword in the C++ programming language used when writing templates. It is used for specifying that a dependent name in a template definition or declaration is a type.


3 Answers

If you are working with C++11 (the example code contains std::enable_if, so I guess this is the case) or a successive revision, I would use a static_assert in this case:

int fun() {
    static_assert(sizeof(int)==4, "!");
    return 0;
}

int main() {
    fun();
}

You don't have a set of functions from which to pick a working one up.
As I've been said once, this is more a substitution failure is always an error than a substitution failure is not an error.
What you want is a compile-time trigger and a static_assert does it with gentle error messages.

Of course, it's also far easier to read than a complicated sfinae expression too!!


If you want to choose between two functions and you don't want to use template machinery or macros, do not forget that overloading is part of the language (pre-C++11 working example):

#include <iostream>

template<bool> struct tag {};
int fun(tag<true>) { return 0; } 
int fun(tag<false>) { return 1; }
int fun() { return fun(tag<sizeof(int) == 4>()); }

int main() {
    std::cout << fun() << std::endl;
}

This can be easily extended to the cases where the functions are more than two:

#include <iostream>

template<int> struct tag {};
int fun(tag<0>) { return 0; }
int fun(tag<1>) { return 1; }
int fun(tag<2>) { return 2; }

int fun(bool b) {
    if(b) { return fun(tag<0>()); }
    else { return fun(tag<(sizeof(int) == 4) ? 1 : 2>());
}

int main() {
    std::cout << fun(false) << std::endl;
}

You can put those functions in an anonymous namespace and get away with them.


Of course, note also that in pre-C++11 we were authorized to write enable_if and all the other things from type_traits for ourselves.
As an example:

template<bool b, typename = void>
struct enable_if { };

template<typename T>
struct enable_if<true, T> { typedef T type; };
like image 178
skypjack Avatar answered Oct 27 '22 21:10

skypjack


Enable if is pretty easy to implement. Take a look at this implementation:

template<bool b, typename T = void>
struct enable_if {
    typedef T type;
};

template<typename T>
struct enable_if<false, T> {};

In C++11 I usually declare some aliases. Since you're stuck in pre C++11 era, you can do that instead:

template<bool b>
struct enable_if_parameter : enable_if<b, int*> {};

Then you can use the struct like this:

template<typename T, typename enable_if_parameter<(sizeof(T) >= 0)>::type = 0>
void someFunc() {
    // ...
}

If you can allow yourself some C++17, you could do that:

template<bool b>
using enable_if_parameter = std::enable_if_t<b, int*>;

And then do that:

template<typename T, enable_if_parameter<std::is_same_v<T, int>> = 0>

I also love the void_t idom to create new type traits:

template<typename T, typename = void>
struct has_callme : std::false_type {};

template<typename T>
struct has_callme<T, void_t<decltype(std::declval<T>().callme())>> : std::true_type {};
like image 29
Guillaume Racicot Avatar answered Oct 27 '22 21:10

Guillaume Racicot


In C++03 you simply write enable_if yourself. It requires no C++11 features.

The reason why you use different techniques is that the pre-C++11 compilers sometimes have a funny definition of what SFINAE and what should be an error. MSVC is the current major compiler who still (in the pre-C++17 era) has very quirky definition of what is valid SFINAE due to their "SFINAE decltype" issues.


In C++11 you should write void_t and enable_if_t to simplify your SFINAE stuff.

You should also write this:

namespace details {
  template<template<class...>class Z, class always_void, class...Ts>
  struct can_apply:std::false_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...>;

which lets you write traits and ask if something is valid easily (can you call a method? Create an alias that does a decltype on the invocation, then ask if you can apply the type to the alias). This is still needed in C++14 and 17, but C++20 probably will get is_detected which serves a similar purpose.

So can_print is:

template<class T>using print_result = decltype(
  std::declval<std::ostream&>() << std::declval<T>()
);
template<class T>using can_print = can_apply< print_result, T >;

it is either truthy or falsy depending on if << works on a stream with it.

In C++14 you can start using hana-style metaprogramming to create lambdas that do type manipulation for you. In C++17 they become constexpr, which gets rid of some issues.


Using techniques like the OP's macro tends to lead to ill formed programs, no diagnostic required. This is because if a template has no valid template parameters that would lead to the body of the template being valid code, your program is ill formed, no diagnostic required.

So this:

template<REQUIRES(sizeof(int)==4)>
int fun() {
  // code that is ill-formed if `int` does not have size 4
}

will most likely compile and run and "do what you want", but it is actually an ill-formed program when sizeof(int) is 8.

The same may be true of using this technique to disable methods on classes based on the template arguments of the class. The stanard is unclear on the issue, so I avoid it.

The REQUIRES macro tries to hide how it works behind magic, but it is far too easy to step over the line and generate an ill-formed program. Hiding the magic when the details of the magic cause your code to be ill-formed is not a good plan.


Tag dispatching can be used to simplify otherwise complex SFINAE issues. It can be used to order overloads or pick between them, or pass more than one bundle of types to a helper template function.

template<std::size_t N>
struct overload_priority : overload_priority<N-1> {};
template<>
struct overload_priority<0> {};

Now you can pass overload_priority<50>{} to a set of functions, and the one with the highest overload_priority<?> in that slot will be preferred.

template<class T>struct tag_t{using type=T;};

namespace details {
  inline int fun( tag_t<int[4]> ) { return 0; }
  inline int fun( tag_t<int[8]> ) { return 1; }
}
int fun() { return details::fun( tag_t<int[sizeof(int)]>{} ); }

just dispatched to a different function depending on the size of int.

Both fun overloads get compiled and checked, so you don't run into the stealth ill-formed program problem.

A function whose validity is not a function of its template arguments is not safe to use in C++. You must use a different tactic. Machinery that makes this easier to do just makes it easier to write ill-formed programs.

like image 39
Yakk - Adam Nevraumont Avatar answered Oct 27 '22 23:10

Yakk - Adam Nevraumont