Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why Must Specializing Argument be void?

So yet another question in this saga. Guillaume Racicot has been good enough to provide me with yet another workaround so this is the code I'll be basing this question off of:

struct vec
{
    double x;
    double y;
    double z;
};

namespace details
{
template <typename T>
using subscript_function = double(*)(const T&);

template <typename T>
constexpr double X(const T& param) { return param.x; }

template <typename T>
constexpr double Y(const T& param) { return param.y; }

template <typename T>
constexpr double Z(const T& param) { return param.z; }
}

template <typename T, typename = void>
constexpr details::subscript_function<T> my_temp[] = { &details::X<T>, &details::Y<T> };

template <typename T>
constexpr details::subscript_function<T> my_temp<T, enable_if_t<is_floating_point_v<decltype(details::X(T()))>, T>>[] = { &details::X<T>, &details::Y<T>, &details::Z<T> };


int main() {
    vec foo = { 1.0, 2.0, 3.0 };

    for(const auto i : my_temp<decltype(foo)>) {
        cout << (*i)(foo) << endl;
    }
}

The problem seems to arise in my specialization when I return something other than void. In the code above for example, enable_if_t<is_floating_point_v<decltype(details::X(T()))>, T> prevents specialization, while simply removing the last argument and allowing enable_if to return void allows specialization.

I think this points to my misunderstanding of what is really happening here. Why must the specialized type always be void for this to work?

Live Example

like image 344
Jonathan Mee Avatar asked Jun 19 '19 12:06

Jonathan Mee


2 Answers

Not sure to understand what you don't understand but...

If you write

template <typename T, typename = void>
constexpr details::subscript_function<T> my_temp[] = { &details::X<T>, &details::Y<T> };

template <typename T>
constexpr details::subscript_function<T> my_temp<T, enable_if_t<is_floating_point_v<decltype(details::X(T()))>, T>>[] = { &details::X<T>, &details::Y<T>, &details::Z<T> };

you have a first, main, template variable with two templates: a type and a type with a default (void).

The second template variable is enabled when std::enable_if_t is void.

What's happen when you write

for(const auto i : my_temp<decltype(foo)>) 

?

The compiler:

1) find my_temp<decltype(foo)> that has a single template parameter

2) look for a matching my_temp template variable

3) find only a my_temp with two template parameters but the second has a default, so

4) decide that my_temp<decltype(foo)> can be only my_temp<decltype(foo), void> (or my_temp<vec, void>, if you prefer)

5) see that the main my_temp matches

6) see that the my_temp specialization doesn't matches because

enable_if_t<is_floating_point_v<decltype(details::X(T()))>, T>

is T (that is vec), so could match only my_temp<vec, vec> that is different from my_temp<vec, void>.

7) choose the only template variable available: the main one.

If you want that the specialization is enabled by

enable_if_t<is_floating_point_v<decltype(details::X(T()))>, T>

you should use T

// ..............................V   T! not void
template <typename T, typename = T>
constexpr details::subscript_function<T> my_temp[] = { &details::X<T>, &details::Y<T> };

as default for second template type in the main template variable.

Off Topic suggestion: better use std::declval inside the std::is_floating_point_v test; I suggest

std::enable_if_t<std::is_floating_point_v<decltype(details::X(std::declval<T>()))>>
like image 76
max66 Avatar answered Sep 29 '22 12:09

max66


How template specialization works:

There is a primary specialization. This one basically defines the arguments and defaults.

template <typename T, typename = void>

This is the template part of your primary specialization. It takes one type, then another type that defaults to void.

This is the "interface" of your template.

template <typename T>
[...] <T, enable_if_t<is_floating_point_v<decltype(details::X(T()))>, T>> [...]

here is a secondary specialization.

In this case, the template <typename T> is fundamentally different. In the primary specialization, it defined an interface; here, it defines "variables" that are used below.

Then we have the part where we do the pattern matching. This is after the name of the template (variable in this case). Reformatted for sanity:

<
  T,
  enable_if_t
  <
    is_floating_point_v
    <
      decltype
      (
        details::X(T())
      )
    >,
    T
  >
>

now we can see the structure. There are two arguments, matching the two arguments in the primary specialization.

The first one is T. Now, this matches the name in the primary specialization, but that means nothing. It is like calling a function make_point(int x, int y) with variables x,y -- it could be y,x or m,n and make_point doesn't care.

We introduced a completely new variable T in this specialization. Then we bound it to the first argument.

The second argument is complex. Complex enough that it is in a "non-deduced context". Typically, template specialization arguments are deduced from the arguments passed to template as defined in the primary specialization; non-deduced arguments are not.

If we do some_template< Foo >, matching a type T against Foo gets ... Foo. Pretty easy pattern match. Fancier pattern matches are permitted, like a specialization that takes a T*; this fails to match against some_template<int>, but matches against some_template<int*> with T=int.

Non-deduced arguments do not participate in this game. Instead, the arguments that do match are plugged in, and the resulting type is generated. And if and only if that matches the type passed to the template in that slot does the specialization match.

So lets examine what happens we pass vec as the first argument to my_temp

First we go to the primary specialization

template<typename T, typename=void>
my_temp

now my_temp<vec> has a default argument. It becomes my_temp<vec,void>.

We then examine each other specialization to see if any of them match; if none do, we stay as the primary specialization.

The other specialization is:

template<typename T>
[...] my_temp<
  T,
  enable_if_t
  <
    is_floating_point_v
    <
      decltype
      (
        details::X(T())
      )
    >,
    T
  >
>[...]

with [...] for stuff that doesn't matter.

Ok, the first argument is bound to T. Well, the first argument is vec, so that is easy. We substitute:

template<typename T>
[...] my_temp<
  vec,
  enable_if_t
  <
    is_floating_point_v
    <
      decltype
      (
        details::X(vec())
      )
    >,
    vec
  >
>[...]

then evaluate:

template<typename T>
[...] my_temp<
  vec,
  enable_if_t
  <
    is_floating_point_v
    <
      double
    >,
    vec
  >
>[...]

and more:

template<typename T>
[...] my_temp<
  vec,
  enable_if_t
  <
    true,
    vec
  >
>[...]

and more:

template<typename T>
[...] my_temp<
  vec,
  vec
>[...]

ok, remember we where trying to match against my_temp<vec,void>. But this specialization evaluated to my_temp<vec,vec>, and those don't match. Rejected.

Remove the ,T from enable_if, or make it ,void (same thing), and the last line of the above argument becomes my_temp<vec,void> matches my_temp<vec,void>, and the secondary specialization is chosen over the primary one.


It is confusing. The same syntax means fundamentally different things in primary specialization and secondary ones. You have to understand pattern matching of template arguments and non-deduced contexts.

And what you usually get is someone using it like a magic black box that you copy.

The magic black box -- the patterns -- are useful because they mean you don't have to think about the details of how you got there. But understanding pattern matching of template arguments, deduced and non-deduced contexts, and the differences between primary and secondary specializations is key to get why the black box works.

like image 32
Yakk - Adam Nevraumont Avatar answered Sep 29 '22 12:09

Yakk - Adam Nevraumont