6

I have a basic understanding of SFINAE, e.g. how enable_if works. I recently came across this answer, and I've spent over an hour trying to understand how it actually works to no avail.

The goal of this code is to overload a function based on whether a class has a specific member in it. Here's the copied code, it uses C++11:

template <typename T> struct Model
{
    vector<T> vertices;

    void transform( Matrix m )
    {
        for(auto &&vertex : vertices)
        {
          vertex.pos = m * vertex.pos;
          modifyNormal(vertex, m, special_());
        }
    }

private:

    struct general_ {};
    struct special_ : general_ {};
    template<typename> struct int_ { typedef int type; };

    template<typename Lhs, typename Rhs,
             typename int_<decltype(Lhs::normal)>::type = 0>
    void modifyNormal(Lhs &&lhs, Rhs &&rhs, special_) {
       lhs.normal = rhs * lhs.normal;
    }

    template<typename Lhs, typename Rhs>
    void modifyNormal(Lhs &&lhs, Rhs &&rhs, general_) {
       // do nothing
    }
};

For the life of me, I can't wrap my head around how this mechanism works. Specifically, what does typename int_<decltype(Lhs::normal)>::type = 0 help us do, and why do we need an extra type (special_/general_) in this method.

Community
  • 1
  • 1
Phonon
  • 12,549
  • 13
  • 64
  • 114
  • To me it looks that the example is not valid; the forwarding reference will not fire `decltype(Lhs::normal)` if `Lhs` is an lvalue. It should be `decltype(std::declval().normal)` – Piotr Skotnicki Mar 24 '16 at 07:05
  • @PiotrSkotnicki The example runs, I tested it. – Phonon Mar 24 '16 at 07:18
  • It compiles, but did you check if it gives a correct result? – Piotr Skotnicki Mar 24 '16 at 07:19
  • @PiotrSkotnicki Yes, it runs and produces results as expected. – Phonon Mar 24 '16 at 07:24
  • @Phonon mind posting your working version in http://ideone.com/? because for me it doesn't give the correct result if called with a lvalue. – Gam Mar 24 '16 at 07:27
  • 4
    @Phonon well, compare [this](http://coliru.stacked-crooked.com/a/ca5db73e5d318e74) and [this](http://coliru.stacked-crooked.com/a/ded4f8d0549034f4), instead of arguing with me – Piotr Skotnicki Mar 24 '16 at 07:29
  • @PiotrSkotnicki Why does Lhs::normal work on rvalues and not on lvalues tho? I have no clue. – Gam Mar 24 '16 at 07:37
  • 1
    @Phantom because a forwarding reference becomes an lvalue reference for lvalues, so, e.g. `A&::normal` is never valid (see the ampersand) – Piotr Skotnicki Mar 24 '16 at 07:38
  • @PiotrSkotnicki I guess I missed that detail when learning about forwarding references.. For lvalues, T becomes T&, and the parameter becomes T& + && = T&&&, and collapses into T&. While for Rvalues, T is still T, and T + && = T&&. Right...? xD – Gam Mar 24 '16 at 07:50
  • @Phantom yes, and additionally, if someone explicitly specifies `T` in a template argument list as `T&&` (e.g. `modifyNormal`) then once again, T&& + && collapses into T&& – Piotr Skotnicki Mar 24 '16 at 07:52

3 Answers3

3

why do we need an extra type (special_/general_) in this method

These are used only for the purpose of allowing the modifyNormal function to be overloaded with different implementations. What is special about them is that special_ uses the IS-A relationship since it inherits from general_. Furthermore, the transform function always calls the modifyNormal overload which takes the special_ type, see the next part.

what does typename int_<decltype(Lhs::normal)>::type = 0 help us do

This is a template argument with a default value. The default value is present so that the transform function doesn't have to specify it, which is important because the other modifyNormal function doesn't have this template parameter. Furthermore, this template parameter is only added to invoke SFINAE.

http://en.cppreference.com/w/cpp/language/sfinae

When substituting the deduced type for the template parameter fails, the specialization is discarded from the overload set instead of causing a compile error.

So if a failure occurs the modifyNormal function taking the special_ type is removed from the set of overloads to consider. This only leaves the modifyNormal function taking a general_ type and since special_ IS-A general_ type everything still works.

If a substitution failure does not occur then the modifyNormal function using the special_ type will be used since it's a better match.


Note: The general_ type is a struct so the inheritance is public by default, allowing the IS-A relationship without using the public keyword.


Edit:

Can you comment on why we use the elaborate typename int_<decltype(Lhs::normal)>::type mechanism in the first place?

As stated above this is used to trigger the SFINAE behavior. However, it's not very elaborate when you break it down. At it's heart it wants to instantiate a instance of the int_ struct for some type T and it has a type data type defined:

int_<T>::type

Since this is being used in a template the typename keyword needs to be added, see When is the “typename” keyword necessary?.

typename int_<T>::type

Lastly, what is the actual type used to instantiate the int_ struct? That's determined by decltype(Lhs::normal), which reports the type for Lhs::normal. If type Lhs type has a normal data member then everything succeeds. However, if it doesn't then there is a substitution failure and the importance of this is explained above.

Community
  • 1
  • 1
James Adkison
  • 9,412
  • 2
  • 29
  • 43
  • Can you comment on why we use the elaborate `typename int_::type` mechanism in the first place? Isn't there a simpler way to make it fail? – Phonon Mar 24 '16 at 07:24
  • @Phonon I added some more information to try an clarify the details for you. – James Adkison Mar 24 '16 at 12:42
1

special_/general_ are types used to let the compiler distinguish in between the two methods modifyNormal. Note that the third argument is not used. The general implementation does nothing, but in special case it modifies the normal. Also note that special_ is derived from general_. That means that if the specialized version of modifyNormal is not defined (SFINAE) the general case applies; if the specialized version exists, then it will be chosen (it is more specific).

Now there is a switch in the definition of modifyNormal; if the type (first argument of the template) has no member named normal then the template fails (SFINAE and do not complain about it, this is the trick about SFINAE) and it will be the other definition of modifyNormal that will apply (the general case). If the type defines a member named normal then the third argument of the template can be resolved to an additional third argument template (an int defaulted equals to 0). This third argument has no purpose to the function, only for SFINAE (the pattern applies on it).

Jean-Baptiste Yunès
  • 34,548
  • 4
  • 48
  • 69
1

The types general and special are used to force the compiler to choose the first function (that is a better match) as a first try while trying to resolve the call.
Note that the call is:

modifyNormal(vertex, m, special_());

Anyway, because general inherits from special, both are valid and the second one will be chosen if the first fails during type substitution.

So, it's like to say - let's have a go as if that method actually exists, but keep calm because we have a catch-all callback that does nothing.

Why could it fail?
That's where int_ takes its part in the game.
If the decltype (let me say) gives an error because the member method normal is missed in Lhs, int_ cannot be specialized and the substitution actually fails, but thanks to SFINAE, this failure is not an error as long as there exists another substitution that can be tried (and it exists in our case, the method having general as an argument, that still is a less precise match for the original call).

skypjack
  • 49,335
  • 19
  • 95
  • 187