5

Before the introduction of concepts and constraints, there are several ways to simulate this compile-time check. Take a "order()" function for example: (how to implement LessThanComparable without concepts or constraints is another story)

  • Use static_assert

    template <typename T, typename U>
    void order(T& a, U& b)
    {
        static_assert(LessThanComparable<U,T>, "oh this is not epic");
        if (b < a)
        {
            using std::swap;
            swap(a, b);
        }
    }
    

    This approach won't work for function overloading.

  • Use typename = enable_if

    template <typename T, typename U,
        typename = std::enable_if_t<LessThanComparable<U,T>>>>
    void order(T& a, U& b)
    {
        if (b < a)
        {
            using std::swap;
            swap(a, b);
        }
    }
    

    What if an over-"intelligent" guy specifies a third parameter by hand?

  • Use enable_if in the function prototype:

    template <typename T, typename U>
    std::enable_if_t<LessThanComparable<U,T>>, void> order(T& a, U& b)
    {
        if (b < a)
        {
            using std::swap;
            swap(a, b);
        }
    }
    

    Sometimes also doesn't work in function overloading.

  • Use enable_if as the type of a dummy non-type template parameter

    template <typename T, typename U,
        std::enable_if_t<LessThanComparable<U,T>>, void*> = nullptr> // or int = 0
    void order(T& a, U& b)
    {
        if (b < a)
        {
            using std::swap;
            swap(a, b);
        }
    }
    

    I saw this before, and I can't think of any drawbacks.

  • And many other variants.

Which ones are preferable or recommended? What is the benefits and drawbacks? Any help is appreciated.

max66
  • 65,235
  • 10
  • 71
  • 111
L. F.
  • 19,445
  • 8
  • 48
  • 82
  • The usual way is using _type traits_. – πάντα ῥεῖ Dec 25 '18 at 13:11
  • 1
    @πάνταῥεῖ You mean the usual way to implement the `LessThanComparable`? – L. F. Dec 25 '18 at 13:12
  • 1
    No I mean in general. Type traits are used to verify concepts and constraints of template parameters. – πάντα ῥεῖ Dec 25 '18 at 13:13
  • 1
    @πάνταῥεῖ Yes, they are very useful in metaprogramming – L. F. Dec 25 '18 at 13:14
  • Related: https://stackoverflow.com/questions/20181702/which-type-traits-cannot-be-implemented-without-compiler-hooks – πάντα ῥεῖ Dec 25 '18 at 13:15
  • 2
    There is a slight drawback to non-type template parameter approach. If you wish to separate definition from declaration (e.g. for a template member function) you'll have to duplicate the "constraint". – r3mus n0x Dec 25 '18 at 13:35
  • Related to [why-should-i-avoid-stdenable-if-in-function-signatures](https://stackoverflow.com/questions/14600201/why-should-i-avoid-stdenable-if-in-function-signatures) with one drawback listed in [my answer](https://stackoverflow.com/a/49532681/2684539) for the preferred approach. – Jarod42 Dec 25 '18 at 22:35

3 Answers3

5

It's a complicated topic and is not easy give an answer at your question.

Anyway, some observations/suggestions, without any pretense to being exhaustive.

(1) the static_assert() way

static_assert(LessThanComparable<U,T>, "oh this is not epic");

is a good solution if you want a function that works only with some types and give an error (a clear error, because you can choose the error message) if called with wrong types.

But usually is the wrong solution when you want an alternative. Isn't a SFINAE solution. So calling the function with arguments of wrong types gives an error and doesn't permit that another function is used in substitution.

(2) You're right about the

typename = std::enable_if_t</* some test */>>

solution. An user can explicit a third parameter by hand. Joking, I say that this solution can be "hijacked".

But isn't the only drawback of this solution.

Suppose you have two complementary foo() functions that have to be enabled/disabled via SFINAE; the first one when a test is true, the second one when the same test if false.

You can think that the following solution is dangerous (can be hijacked) but can work

/* true case */
template <typename T, typename = std::enable_if_t<true == some_test_v<T>>>
void foo (T t)
 { /* something with t */ }

/* false case */
template <typename T, typename = std::enable_if_t<false == some_test_v<T>>>
void foo (T t)
 { /* something with t */ }

Wrong: this solution simply doesn't works because you're enabling/disabling not the second typename but only the default value for the second typename. So you're not completely enabling/disabling the functions and the compiler have to consider two functions with the same signature (the signature of a function doesn't depends from default values); so you have a collision and obtain an error.

The following solutions, SFINAE over the returned type

std::enable_if_t<LessThanComparable<U,T>, void> order(T& a, U& b)

(also without void, that is the default type

std::enable_if_t<LessThanComparable<U,T>> order(T& a, U& b)

) or over the second type (following the Yakk's suggestion about a not-standard allowed void *)

template <typename T, typename U,
          std::enable_if_t<LessThanComparable<U,T>>, bool> = true> 

are (IMHO) good solutions because both of they avoid the hijacking risk and are compatible with two complementary functions with the same name and signature.

I suggest a third possible solution (no hijack-able, complementary compatible) that is the addition of a third defaulted value with SFINAE enabled/disabled type: something as

template <typename T, typename U>
void order(T& a, U& b, std::enable_if_t<LessThanComparable<U,T>> * = nullptr)

Another possible solution avoid at all SFINAE but use tag-dispatching; something as

template <typename T, typename U>
void order_helper (T & a, U & b, std::true_type const &)
 { if (b < a) { std::swap(a, b); } }

// something different if LessThanComparable is false ?
template <typename T, typename U>
void order_helper (T & a, U & b, std::false_type const &)
 { /* ???? */ }

template <typename T, typename U>
void order (T & a, U & b)
 { order_helper(a, b, LessThanComparable<U,T>{}); }

This in case LessThanComplarable inherit from std::true_type when the condition is true, from std::false_type when the condition is false.

Otherwise, if LessThanComparable gives only a boolean value, the call to order_helper() can be

order_helper(a, b, std::integral_constant<bool, LessThanComparable<U,T>>{});

(3) if you can use C++17, there is the if constexpr way that can avoid a lot of overloading

template <typename T, typename U>
void order(T& a, U& b)
{
   if constexpr ( LessThanComparable<U, T> )
    { 
      if ( b < a )
         std::swap(a, b);
    }
   else
    {
      // what else ?
    }
}
max66
  • 65,235
  • 10
  • 71
  • 111
2

Non-type template parameters of type void* are not allowed in at least some versions of the standard; I'd use bool with value =true.

Otherwise, use that.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Thank you, I did not know you cannot use `void*` as a non-type template parameter. – L. F. Dec 26 '18 at 09:25
  • Anyway, [this page](https://en.cppreference.com/w/cpp/language/template_parameters#Non-type_template_parameter) seems to suggest that a pointer to object or function is allowed as a non-type template parameter in every standard from before C++11 to C++20. **Is `void*` considered a "pointer to object"?** – L. F. Jan 23 '19 at 13:13
  • @l.f. I'd ask a language lawyer tagged question here, or read the standard; my concern is that I have experienced otherwise reasonable compilers refusing void ptr arguments, which are easy to avoid, so I just avoid them. – Yakk - Adam Nevraumont Jan 23 '19 at 13:25
2

You should look how range-v3 library emulates concepts https://github.com/ericniebler/range-v3/blob/master/include/range/v3/range_concepts.hpp

There is also a way to use template aliases to achieve something similar to concepts Using alias templates for sfinae: does the language allow it?

And, you have missed decltype variants on your list:

template <typename T, typename U>
auto order(T& a, U& b) -> decltype(void(b < a))
{
    if (b < a)
    {
        using std::swap;
        swap(a, b);
    }
}

template <typename T, typename U,
    typename = decltype(void(b < a))>
void order(T& a, U& b)
{
    if (b < a)
    {
        using std::swap;
        swap(a, b);
    }
}
Nikita Kniazev
  • 3,728
  • 2
  • 16
  • 30
  • `decltype` is mostly the way to write the traits. In the similar way `std::void_t` would allow SFINAE on type instead of boolean, but doesn't really change the OP's variants. – Jarod42 Dec 25 '18 at 22:39
  • I think `decltype` better belongs to the implementation of those concept-simulators like `LessThanComparable`. Also a variant, though. – L. F. Dec 26 '18 at 09:27