10
\$\begingroup\$

I am trying to learn what SFINAE is. I believe I understand it quite well; however, I do not have a lot of practice in the subject. I mean I would find it difficult to list all the cases in which SFINAE should or could be used, or it would be difficult for me to immediately understand what a given sample of an SFINAE code should do. However, I believe I understand the core concept well enough to use it effectively.

Nonetheless looking through examples of SFINAE usage I came to the few conclusions.

One of which is: it is extremely pointless to use a chain of std::enable_if_t, std::void_t, std::is_same, etc, etc, etc, to define a single type using type = int *. IMHO (of a "newbie" with untrained eyes) such chains make the code less (extremely less) readable.

The second conclusion was: wouldn't it be useful to have the ability to check few conditions at the same time?

So I created this alias: (well "created" is waaay too big word... I just wrote 4 lines of code...)

template<typename T, typename ...>
using sfinae = T;

template<typename T, typename ...>
using SFINAE = T;

Usage examples

// 1) within class declaration:
template <typename T, 
    sfinae<bool,
        std::enable_if_t<std::is_integral_v<T>>,
        std::enable_if_t<std::is_arithmetic_v<T>>
    > = true /* provide default option, bc reasons */>
class foo;

// 2) within function declaration:
template <typename T, 
    sfinae<bool,
        std::enable_if_t<std::is_integral_v<T>>,
        std::enable_if_t<std::is_arithmetic_v<T>>
    > = true /* provide default option, bc reasons */>
void foo;

// 3) as a function return type:
template <typename T> 
sfinae<T,
    std::enable_if_t<std::is_integral_v<T>>,
    std::enable_if_t<std::is_arithmetic_v<T>>
> const & foo(T const & val);

// 4) To define other SFINAE-conditions as aliases:
template <typename T> 
using check_xyz = sfinae<void,
    std::enable_if_t<std::is_integral_v<T>>,
    std::enable_if_t<std::is_arithmetic_v<T>>
>;

template <typename T, typename = check_xyz<T>>
class foo;

And my main concern/question is: would a professional c++ developer deem sfinae</*...*/> (as I proposed it) useful or extremely unnecessary? Are there any changes that could be introduced to the idea?

\$\endgroup\$
  • \$\begingroup\$ Please turn all ... into actual code. We review only complete snippets. \$\endgroup\$ – t3chb0t Dec 19 '18 at 17:43
  • 7
    \$\begingroup\$ @t3chb0t, but the actual code is template<typename T, typename ...> using sfinae = T; and template<typename T, typename ...> using SFINAE = T; everything else is just a usage examples... Also because of the keywords "immediate context" (which can be found in cpp draw, the sfinae section. Usage of this aliases is limited declarations only. However... I could delete definitions (it would by erase all ... by an extension). \$\endgroup\$ – cukier9a7b5 Dec 19 '18 at 17:47
  • \$\begingroup\$ Lacks concrete context: Code Review requires concrete code from a project, with sufficient context for reviewers to understand how that code is used. Pseudocode, stub code, hypothetical code, obfuscated code, and generic best practices are outside the scope of this site. Please take a look at the help center. \$\endgroup\$ – Mast Dec 19 '18 at 17:48
  • \$\begingroup\$ I know, inside templates yes, but there are many ... elsewhere and I now admit that I didn't see the // usage examples: comment. I'd be better if it was a heading so that it's better visible... let me edit this... \$\endgroup\$ – t3chb0t Dec 19 '18 at 17:50
  • 1
    \$\begingroup\$ I think your quesiton is fine after all. Not much code but the rest is just examples that weren't easy to notice. I'll retract my close-vote now... @Mast? \$\endgroup\$ – t3chb0t Dec 19 '18 at 17:52
14
\$\begingroup\$

I just wrote four lines of code...

template<typename T, typename ...>
using sfinae = T;

template<typename T, typename ...>
using SFINAE = T;

You should have written just two lines of code — either the first two or the last two. Not both. "There's more than one way to [spell] it" is the Perl way, not the C++ way. ;) (At least not intentionally! Not intentionally pre-C++2a! Now we're getting both std::filesystem and std::fs, that's a less airtight position.)


As you show, your thing can be used for

template<class T, 
    sfinae<bool,
        std::enable_if_t<std::is_integral_v<T>>,
        std::enable_if_t<std::is_arithmetic_v<T>>
    > = true>
void foo();

However, this is pretty verbose; surely we'd rather write std::enable_if_t<std::is_integral_v<T> && std::is_arithmetic_v<T>> than std::enable_if_t<std::is_integral_v<T>>, std::enable_if_t<std::is_arithmetic_v<T>>.

So personally I have a different alias that accomplishes the same goal: bool_if_t.

template<bool B>
using bool_if_t = std::enable_if_t<B, bool>;

template<class T, 
    bool_if_t<
        std::is_integral_v<T> &&
        std::is_arithmetic_v<T>
    > = true>
void foo();

This has the advantage that the inner expression is written using normal C++ syntax rules. We don't have to remember some arbitrary rule like "x, y means both x and y must be true"; we just write x && y. And if we want to enable this function when either x or y is true, we don't have to invent a new primitive; we just write bool_if_t<x || y>. bool_if_t is much more composable than your sfinae<Ts...>, because bool_if_t can exploit the existing expression syntax of C++.


Recommended viewing: "A Soupçon of SFINAE" (Arthur O'Dwyer, CppCon 2017). (Yes, that's me.)

You picked examples that don't really show off the power of sfinae, by the way. Consider this example:

// 3) as a function return type:
template <typename T> 
sfinae<T,
    std::enable_if_t<std::is_integral_v<T>>,
    std::enable_if_t<std::is_arithmetic_v<T>>
> const & foo(T const & val);

You take a "value-space" boolean (is_integral_v<T>), lift it up into "SFINAE-space" with enable_if_t, and then apply a logical AND operation in SFINAE-space using your sfinae alias.

template <typename T> 
std::enable_if_t<
    std::is_integral_v<T> &&
    std::is_arithmetic_v<T>, T
> const & foo2(T const & val);

Here I take the same "value-space" boolean, perform the logical AND in value space where we have a dedicated operator && for maximum expressiveness; and I lift it up into SFINAE-space after doing the AND. This is clearer and also probably more efficient in terms of compile time.

Where your version helps is when your values start in SFINAE-space!

template<class T> 
sfinae<T,
    decltype(std::declval<T>() + std::declval<T>()),
    T*,
    T&
> const & foo3(T const & val);

If we were trying to replicate this code's behavior with the standard tools, we'd say something like

template<class, class=void> struct has_plus : std::false_type {};
template<class T> struct has_plus<T, decltype(void(std::declval<T>() + std::declval<T>()))> : std::true_type {};
template<class T> inline constexpr bool has_plus_v = has_plus<T>::value;

template<class T> 
std::enable_if_t<
    has_plus_v<T> &&
    not std::is_reference_v<T> &&
    not std::is_void_v<T>, T
> const & foo4(T const & val);

That is, we start with a value in SFINAE-space (decltype(std::declval<T>() + std::declval<T>())), lift it into value-space (has_plus_v<T>), do the logical AND in value-space, and lift the result back into SFINAE-space.

Whereas with your foo3, you start in SFINAE-space, do the logical AND via sfinae<...> without leaving SFINAE-space, and then you're done. Much simpler! But harder to read, I think.


A simple way to improve readability is to pick a good name. sfinae_space_and<Ts...> might be clearer. Can you think of a way to write sfinae_space_or<Ts...>? How about sfinae_space_not<T>? Does it make sense, maintainability-wise, to provide one without the others?

\$\endgroup\$
  • 5
    \$\begingroup\$ Incredible how much one can write about four (two) lines of code. Very informative ;-) \$\endgroup\$ – t3chb0t Dec 19 '18 at 18:42
  • \$\begingroup\$ What's the use of bool_if_t = std::enable_if_t<B, bool>;? IMO it hides the intent behind it by removing the informative "enable_if" part. \$\endgroup\$ – Flamefire Dec 20 '18 at 11:56
  • \$\begingroup\$ @Flamefire: I see your point, but OTOH, the "intent" is usually obvious from context. Significantly, bool_if_t<B>=true is 8 characters shorter than enable_if_t<B, bool>=true. Perhaps most significantly, it eliminates a bikeshed point: if all you have is the low-level enable_if_t, then you'll see people writing enable_if_t<B, XXX>={} for many different values of XXX (bool, int, etc). The slightly-higher-level abstraction eliminates that needless variation. (Murphy's Law: if you don't want people fiddling with the knobs, you have to remove the knobs.) \$\endgroup\$ – Quuxplusone Dec 20 '18 at 16:28
  • \$\begingroup\$ Full disclosure: enable_if_t<B, int>=0 is 4 characters shorter than enable_if_t<B, bool>=true, and int_if_t<B>=0 would be 4 characters shorter than bool_if_t<B>=true. Why did I pick bool?... I don't know. :P Maybe I should switch to int_if_t as my primitive. \$\endgroup\$ – Quuxplusone Dec 20 '18 at 16:30
  • 1
    \$\begingroup\$ @cukier9a7b5: "I was actually trying to implement sfinae_space_not and sfinae_space_or ... Unfortunately [I failed] ... I definitely think that, maintainability-wise, it is a must to provide at least syntax for not, and, or cases." Your thoughts parallel mine! :) I'm about 95% sure that it is impossible to write either sfinae_not or sfinae_or. So, by the laws of syllogism, as you stated: you shouldn't provide sfinae_and, either. \$\endgroup\$ – Quuxplusone Dec 21 '18 at 22:23
9
\$\begingroup\$

A bit dated...

First of all, let me say that since C++20 much of SFINAE will be unnecessary. Concepts have SFINAE enabled by default, have much nicer syntax, and have greater reuse capabilities.

Code Review

Personally I don't see much improvement over original one. What I would prefer instead of

template <typename T, typename ... >
...

is

template <typename T, bool ... Conditionals>
... //perform folding

Even then, people sometimes have non-trivial logic like AND-ing and OR-ing the results of conditionals.

Much of the complexity lies in that conditional statements, which could be moved away by using something like template variable:

template <typename T>
constexpr inline bool supports_xxx = /*is_same, trivial, etc*/;

and then people could just use

template <typename T, typename = std::enable_if_t<supports_xxx<T>>>
...

Example

// 1) within class declaration:
template <typename T, 
    sfinae<bool,
        std::enable_if_t<std::is_integral_v<T>>,
        std::enable_if_t<std::is_arithmetic_v<T>>
    > = true /* provide default option, bc reasons */>
class foo;

is better written as

template <typename T>
constexpr inline bool is_int_arithmetic_v = std::is_integral_v<T> 
                                            && std::is_arithmetic_v<T>;

template <typename T, 
          typename = std::enable_if_t<is_int_arithmetic_v<T>>>
class foo;

Do note the inline, it will help users of the code to deal with ODR issues.

\$\endgroup\$
  • \$\begingroup\$ typename = void_t<things...> doesn't work if you want multiple mutually exclusive templates. bool_if_t<things...> = true works in that case. See youtube.com/watch?v=ybaE9qlhHvw&t=37m00s . \$\endgroup\$ – Quuxplusone Dec 19 '18 at 18:41
  • \$\begingroup\$ @Quuxplusone, shouldn't specialization paired with some sort of tag dispatch be used in that case? I believe it also scales better in cases where more than 2 mutually exclusive cases are present. But yeah, my advice is not a good one to make. \$\endgroup\$ – Incomputable Dec 19 '18 at 18:43
  • \$\begingroup\$ "Shouldn't" is a strong word. ;) I agree that tag dispatch is usually more readable and should be preferred (or if constexpr in C++17). But sometimes tag dispatch can't be used; see the video's example of a conditionally explicit constructor. \$\endgroup\$ – Quuxplusone Dec 19 '18 at 18:54
  • 2
    \$\begingroup\$ Review review: It is "Dated" "since C++20" because it "will be unecessary". Sounds like a plot from Back to the Future IV ;) Anyways, learning SFINAE may still be worthwhile while writing library code that should be backwards compatible and/or useful within older codebases. And compilers still not to catch up a lot 'til they support fully C++20. \$\endgroup\$ – phresnel Dec 20 '18 at 9:14
  • 1
    \$\begingroup\$ since C++20 Still a year off! Since Concepts were initially proposed for C++11 C++14 and C++17 but dropped before the standard was published I am not going to hold my breath until I see them in the ratified standard. \$\endgroup\$ – Martin York Dec 20 '18 at 11:36
2
\$\begingroup\$
template <typename T, 
  sfinae<bool,
    std::enable_if_t<std::is_integral_v<T>>,
    std::enable_if_t<std::is_arithmetic_v<T>>
  > = true /* provide default option, bc reasons */>
class foo;

vs

template <typename T, 
  std::enable_if_t<
    std::is_integral_v<T>
    && std::is_arithmetic_v<T>,
    bool
  > = true
class foo;

This version doesn't use your new sfinae template, and uses fewer characters, and seems no less functional.

What more in C++20 we'll have named concepts (and failing that) requires clauses. That will make much SFINAE obsolete.

What more, there are fancier ways to do SFINAE, especially in

There is this:

template<template<class...>class, class...>
struct can_apply;

which tests if a template can be applied to a list of types.

With constexpr lambdas that can appear in template non type parameter argument calculations, we can do:

template<class F>
constexpr auto apply_test(F &&);

which returns an object that, when evaluated on some arguments, returns true_type if you can invoke F on it, and false_type otherwise.

template<class T,
  std::enable_if_t<
    apply_test([](auto a, auto b)RETURNS(a+b))( std::declval<T>(), std::declval<T>() ),
    bool
  > = true
>
struct foo;

here we test if a type T can be added to itself. (I also use the somewhat ubiquitous RETURNS macro)

Or, more cleanly:

template<class T>
auto foo( T const& lhs, T const& rhs )
requires test_apply(std::plus<T>{})(lhs, rhs)

assuming sufficiently SFINAE friendly std::plus<T>.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.