type_traits, SFINAE, and Concepts

Concepts are coming. They’ll probably remove a lot of boilerplate vs using type_traits, and bring about some nicer syntax. In this post, with C++17, we’ll go through basic type_traits SFINAE, then we’ll go through expression SFINAE and show how we can make something Concept like. We’ll also go through “_traits” classes so we can support optional concept requirements.

Prerequisites

templates

type_traits

enable_if_t

void_t

decltype

declval

noexcept

Concept

This concept will be used throughout the examples.

Let Bar be some other concept.

Consider a concept Foo with requirements:

The type T satifies Foo if

T satisfies Bar

Given:

u is an identifier

is an identifier t is a value of type T

is a value of type r is a value of type T const

is a value of type i, j are values of type int

The following types must be valid:

Type difference_type

The following expressions must be valid:

Expression Return Type Exception T u{} T t = i t.foo(i, j) T & r.size() difference_type noexcept r.empty() convertible to bool noexcept

A type that meets Foo is:

struct A { using difference_type = int ; A & operator = ( int ); A & foo( int , int ); difference_type size () const noexcept ; bool empty () const noexcept ; };

We’ll also define IsBar as:

template < typename T > using IsBar = void ;

Intro

For all examples we’ll be using namespace std;

The simplest way to do SFINAE with type traits is to use enable_if_t . So we can do something like this:

template < typename T, typename = void > class something ; template < typename T > class something < T, enable_if_t < is_default_constructible_v < T >>> {}; template < typename T, typename = enable_if_t < is_default_constructible_v < T >>> void func(T & t){}

We’ll leave out the class template style for the rest of this because it’s very similar.

For something more complex, we can use void_t , decltype , and declval :

template < typename T, typename = void_t < IsBar < T > , typename T :: difference_type, enable_if_t < is_default_constructible_v < T >> , enable_if_t < is_assignable_v < T, int >> , enable_if_t < is_same_v < decltype (declval < T &> ().foo(declval < int > (),declval < int > ())), T &>> , enable_if_t < is_same_v < decltype (declval < T const &> ().size()), typename T :: difference_type > && noexcept (declval < T const &> ().size()) > , enable_if_t < is_convertible_v < decltype (declval < T const &> ().empty()), bool > && noexcept (declval < T const &> ().empty()) > >> void func(T & t){}

To avoid throwing everything into the function or class template, we’ll probably want to create our own trait:

template < typename T, typename = void > struct is_foo : false_type {}; template < typename T > struct is_foo < T, void_t < IsBar < T > , typename T :: difference_type, enable_if_t < is_default_constructible_v < T >> , enable_if_t < is_assignable_v < T, int >> , enable_if_t < is_same_v < decltype (declval < T &> ().foo(declval < int > (), declval < int > ())), T &>> , enable_if_t < is_same_v < decltype (declval < T const &> ().size()), typename T :: difference_type > && noexcept (declval < T const &> ().size()) > , enable_if_t < is_convertible_v < decltype (declval < T const &> ().empty()), bool > && noexcept (declval < T const &> ().empty()) > >> : true_type {}; // handy variable template template < typename T > inline constexpr auto is_foo_v = is_foo < T >:: value; template < typename T, typename = enable_if_t < is_foo_v < T >>> void func(T & t){}

And now we’re back to the simple case.

If we’re going to be making many concepts we can probably get rid of some of the boilerplate ( true_type , false_type stuff) by using the detection idiom.

We’ll continue on with a simple version of the detector:

template < typename Enable, template < typename ... > typename T, typename ... Args > struct detector : false_type { }; template < template < typename ... > typename T, typename ... Args > struct detector < void_t < T < Args... >> , T, Args... > : true_type { }; template < template < typename ... > typename T, typename ... Args > using is_detected = detector < void , T, Args... > ;

Thus the previous example would become:

template < typename T > using IsFoo = void_t < IsBar < T > , typename T :: difference_type, enable_if_t < is_default_constructible_v < T >> , enable_if_t < is_assignable_v < T, int >> , enable_if_t < is_same_v < decltype (declval < T &> ().foo(declval < int > (), declval < int > ())), T &>> , enable_if_t < is_same_v < decltype (declval < T const &> ().size()), typename T :: difference_type > && noexcept (declval < T const &> ().size()) > , enable_if_t < is_convertible_v < decltype (declval < T const &> ().empty()), bool > && noexcept (declval < T const &> ().empty()) > > ; template < typename T > using is_foo = is_detected < IsFoo, T > ; template < typename T > inline constexpr auto is_foo_v = is_foo < T >:: value; template < typename T, typename = enable_if_t < is_foo_v < T >>> void func(T & t){}

So we can see that most of this is very easy to use and even readable.

Expression SFINAE

This is done in the trailing return type of a function. It’s used for disabling functions if the expression doesn’t exist. It looks like:

template < typename T > auto f(T x, T y) -> decltype (x + y) { return x + y; }

If you’ll notice, there isn’t a need for declval , only expressions on variables that we’ve declared. This is an amazing property and we can use it for our own SFINAE purposes.

To check for a member function we can do something like:

template < typename T > auto IsFoo_h(T & t) -> decltype (t.foo(declval < int > (), declval < int > ())); // <- abstracted out into a helper expression sfinae function /// BOILERPLATE template < typename T > using IsFoo = decltype (IsFoo_h(declval < T &> ())); // decltype here is necessary because of the function call, declval is required because we need something to call the function on. template < typename T > using is_foo = is_detected < IsFoo, T > ; template < typename T > inline constexpr auto is_foo_v = is_foo < T >:: value; template < typename T, typename = IsFoo < T >> void func(T & t){}

We don’t need to define a body because we’re only using it for SFINAE. We’ll leave out the boilerplate for the rest of the examples as it doesn’t change.

So, this example was pretty bad, we’ve only got one member function in there and nothing else. We can of course extend it with our trusty void_t . We can also make use of the fact that this is a function and use the template and variable arguments. Thus:

template < typename T, typename = IsBar < T > , typename difference_type = typename T :: difference_type > auto IsFoo_h(T & t, T const & r, int i = {}, int j = {}) -> void_t < enable_if_t < is_default_constructible_v < T >> , enable_if_t < is_assignable_v < T, int >> , enable_if_t < is_same_v < decltype (t.foo(i, j)), T &>> , enable_if_t < is_same_v < decltype (r.size()), difference_type > && noexcept (r.size()) > , enable_if_t < is_convertible_v < decltype (r.empty()), bool > && noexcept (r.empty()) > > ; /// New boilerplate template < typename T > using IsFoo = decltype (IsFoo_h(declval < T &> (), declval < T &> ()));

We can also use the comma operator inside of a decltype :

template < typename T, typename = IsBar < T > , typename difference_type = typename T :: difference_type > auto IsFoo_h(T & t, T const & r, int i = {}, int j = {}) -> decltype ( T{}, t = i, enable_if_t < is_same_v < decltype (t.foo(i, j)), T &> , int > {}, enable_if_t < is_same_v < decltype (r.size()), difference_type > && noexcept (r.size()), int > {}, enable_if_t < is_convertible_v < decltype (r.empty()), bool > && noexcept (r.empty()), int > {} );

So which to use? They’re both equivalent, because we can just use a decltype(...) inside of void_t to achieve the same expressions. We’ll continue on with decltype only because we don’t have to write decltype so many times if we don’t care about the return value or noexcept .

Making it pretty

We’ll just make some macros. We can use the enable_if<...,int>{} technique from above.

We’ll also create an enable macro because we might want to SFINAE out bool expressions:

#define Enable(BOOL) std::enable_if_t<(BOOL), int>{} #define Conv(EXPR, TYPE) Enable((std::is_convertible_v<decltype(EXPR), TYPE>)) #define Same(EXPR, TYPE) Enable((std::is_same_v<decltype(EXPR), TYPE>)) #define Noexcept(EXPR) Enable(noexcept(EXPR)) // Might also want to test both return type and noexcept in one call #define NoexceptConv(EXPR, TYPE) Enable((std::is_convertible_v<decltype(EXPR), TYPE> && noexcept(EXPR))) #define NoexceptSame(EXPR, TYPE) Enable((std::is_same_v<decltype(EXPR), TYPE> && noexcept(EXPR))) // We're using Conv here so it lines up with Same // We'll copy the Conv and Same macros for Noexcept so we get the flexibility of removing {}

Unfortunately we can’t use expressions in Enable because we can’t use parameters directly if we want that bool. declval still works. So you can stick in a declval<X>().max_size() == 10 or sizeof(x) <= 8 there if you want.

You might think that noexcept(...) would work on it’s own, but this just produces a bool and won’t SFINAE out when it’s false . So we’ll have to Enable on that too.

These can be made to work for the void_t version by removing the {} from Enable . This will work for the decltype version if you add {} everytime you use a macro. Might also work better if you are only testing a single expression as you won’t need void_t nor decltype , you could just use the macro directly.

Our example becomes:

template < typename T, typename = IsBar < T > , typename difference_type = typename T :: difference_type > auto IsFoo_h(T & t, T const & r, int i = {}, int j = {}) -> decltype ( T{}, t = i, Same(t.foo(i, j), T & ), NoexceptSame(r.size(), difference_type), NoexceptConv(r.empty(), bool ) );

What about optionality?

Some members may be optionally provided and sensible defaults generated when not. The standard library provides sensible defaults using _traits types. e.g. pointer_traits, allocator_traits. We might also be inclined to provide our own traits for optional members to make our concepts easier for a user to satisfy.

In our concept we don’t even have to check for those optionals. Because they’re optional, they don’t have to be provided anyway and a sensible default will be given. But, if we allow for the user to specialize the _traits , we’ll probably want to check that against the concept.

Consider if, in our example, size() and difference_type are optional. We’ll have to provide a _traits class and test the concept using _traits :

template < typename T > struct foo_traits { /// Type template < typename R, typename = void > struct difference_type_provided_h : false_type { using type = int ; // our default }; template < typename R > struct difference_type_provided_h < R, void_t < typename R :: difference_type >> : true_type { using type = typename R :: difference_type; }; using difference_type_provided = difference_type_provided_h < T > ; static constexpr auto difference_type_provided_v = difference_type_provided :: value; using difference_type = typename difference_type_provided :: type; /// Function // We can use the exact same method to check for members template < typename R > static auto SizeProvided_h(R const & r) -> decltype (NoexceptSame(r.size(), difference_type)); template < typename R > using SizeProvided = decltype (SizeProvided_h(declval < R &> ())); using size_provided = is_detected < SizeProvided, T > ; static constexpr auto size_provided_v = size_provided :: value; static difference_type size (T const & t) noexcept { if constexpr (size_provided_v) { return t.size(); // call the provided function if it's there. } else { return 10 ; // our default } } }; template < typename T, typename = IsBar < T > , typename difference_type = typename foo_traits < T >:: difference_type > auto IsFoo_h(T & t, T const & r, int i = {}, int j = {}) -> decltype ( T{}, t = i, Same(t.foo(i, j), T & ), NoexceptSame(foo_traits < T >:: size(r), difference_type), NoexceptConv(r.empty(), bool ) );

We can’t use any tricks for the defaulted type and so we have to fallback to basic class SFINAE. This is because we can’t get at the ::type . Say we use a std::conditional in the typedef, it would result in an error if we try to use T::difference_type without it being provided. A macro can be used to test the provided type (I actually saw this somewhere in libstdc++), it goes something like:

#define NESTED_TYPE_PROVIDED(TYPE, ALT) \ template < typename R, typename = void > \ struct TYPE ##_provided_h : std::false_type \ { \ using type = ALT; \ }; \ template < typename R > \ struct TYPE ##_provided_h<R, std::void_t<typename R::TYPE>> : std::true_type \ { \ using type = typename R :: TYPE; \ }; // now we can do `using *_provided = *_provided_h<T>;` depending on the template parameter.

Caution

Even if you provide the expression, _trait could generate a default for you if you haven’t provided the right kind of expression. This might not be what you had intended. e.g. You provide a size() that is not noexcept , but your _trait requires noexcept or it’ll generate a default (you probably don’t want this).

Summary

With a few macros, a helper function for expression SFINAE, a traits class, and some boilerplate, we can generate some nice readable concepts.

#define Enable(BOOL) std::enable_if_t<(BOOL), int>{} #define Conv(EXPR, TYPE) Enable((std::is_convertible_v<decltype(EXPR), TYPE>)) #define Same(EXPR, TYPE) Enable((std::is_same_v<decltype(EXPR), TYPE>)) #define Noexcept(EXPR) Enable(noexcept(EXPR)) #define NoexceptConv(EXPR, TYPE) Enable((std::is_convertible_v<decltype(EXPR), TYPE> && noexcept(EXPR))) #define NoexceptSame(EXPR, TYPE) Enable((std::is_same_v<decltype(EXPR), TYPE> && noexcept(EXPR))) template < typename T > using IsBar = void ; template < typename T > struct foo_traits { /// Type template < typename R, typename = void > struct difference_type_provided_h : false_type { using type = int ; // our default }; template < typename R > struct difference_type_provided_h < R, void_t < typename R :: difference_type >> : true_type { using type = typename R :: difference_type; }; using difference_type_provided = difference_type_provided_h < T > ; static constexpr auto difference_type_provided_v = difference_type_provided :: value; using difference_type = typename difference_type_provided :: type; /// Function template < typename R > static auto SizeProvided_h(R const & r) -> decltype (NoexceptSame(r.size(), difference_type)); template < typename R > using SizeProvided = decltype (SizeProvided_h(declval < R &> ())); using size_provided = is_detected < SizeProvided, T > ; static constexpr auto size_provided_v = size_provided :: value; static difference_type size (T const & t) noexcept { if constexpr (size_provided_v) { return t.size(); } else { return 10 ; // our default } } }; template < typename T, typename = IsBar < T > , typename difference_type = typename foo_traits < T >:: difference_type > auto IsFoo_h(T & t, T const & r, int i = {}, int j = {}) -> decltype ( T{}, t = i, Same(t.foo(i, j), T & ), NoexceptSame(foo_traits < T >:: size(r), difference_type), NoexceptConv(r.empty(), bool ) ); template < typename T > using IsFoo = decltype (IsFoo_h(declval < T &> (), declval < T &> ())); template < typename T > using is_foo = is_detected < IsFoo, T > ; template < typename T > inline constexpr auto is_foo_v = is_foo < T >:: value; template < typename T, typename = IsFoo < T >> void func(T & t) {}

Notes

const and non const functions may be defined differently, so you’ll probably want to check both of those against the concept. This isn’t done in the examples. So instead of just NoexceptConv(r.empty(), bool) it would be NoexceptConv(t.empty(), bool), NoexceptConv(r.empty(), bool) , unless you’re planning to only use empty() with a const type.

Link to test code