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.
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.
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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With