Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SFINAE Based on Class Member Existence/Absence

Tags:

c++

c++11

sfinae

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.

like image 663
Phonon Avatar asked Mar 24 '16 06:03

Phonon


2 Answers

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.

like image 98
James Adkison Avatar answered Oct 10 '22 03:10

James Adkison


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).

like image 31
Jean-Baptiste Yunès Avatar answered Oct 10 '22 04:10

Jean-Baptiste Yunès