Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to test for presence of an inner class in a class via SFINAE?

I'm trying to have a different template specialization for classes which have an inner class with a particular name present. I've taken a clue from here and tried the following:

#include <iostream>

template< typename T, typename Check = void > struct HasXYZ
{ static const bool value = false; };

template< typename T > struct HasXYZ< T, typename T::XYZ >
{ static const bool value = true; };

struct Foo
{
  class XYZ {};
};

struct FooWithTypedef
{
  typedef void XYZ;
};

int main()
{
  // The following line prints 1, as expected
  std::cout << HasXYZ< FooWithTypedef >::value << std::endl;
  // The following line prints 0. Why?
  std::cout << HasXYZ< Foo >::value << std::endl;

  return 0;
}

As you can see, if I test for a typedef-defined type in FooWithTypedef, it works. However, it does not work if the type is a genuine inner class. It also only works when the typedef-ed type in FooWithTypedef matches the default value of the second argument in the initial template declaration (which is void in my example). Could one explain what is going on here? How does the specialization process work here?

like image 881
dragonroot Avatar asked Aug 22 '12 06:08

dragonroot


1 Answers

Answer to the initial question

The template specialization you defined here:

template <typename T> struct HasXYZ <T,typename T::XYZ>
{ static const bool value = true; };

will take effect when somebody uses the data type HasXYZ<A,A::XYZ> for some data type A.

Note that, whatever A is, A::XYZ is a data type totally independent of A. Inner classes are data types in their own right. When you use A as the first template argument, there is absolutely no reason for the compiler to assume that you want to use something called A:XYZ as the second argument, even if an inner class of that name exists, and even if doing so would lead the compiler to a template specialization that matches the template arguments exactly. Template specializations are found based on the template arguments provided by the coder, not based on further possible template arguments.

Hence when you use HasXYZ<Foo>, it falls back to using the default template argument void for the second parameter.

Needless to say that if you were to use HasXYZ<Foo,Foo:XYZ> explicitly, you'd get the expected output. But that obviously is not what you intended.

I am afraid the only way to get what you need is std::enable_if (or something that works in a similar way).


Answer to the additional question (after update)

Consider the simplification below:

template <typename T, typename Check = void>
struct A
{ static const bool value = false; };

template <typename T>
struct A<T,void>
{ static const bool value = true; };

The primary definition specifies a default argument of void for the second template parameter. But the specialization (second definition above) defines what class A actually looks like if the second template parameter really is void.

What this means is that if you use, say, A<int> in your code, the default argument will be supplemented so you get A<int,void>, and then the compiler finds the most fitting template specialization, which is the second one above.

So, while default template arguments are defined as part of the primary template declaration, making use of them does not imply that the primary template definition is used. This is basically because default template arguments are part of the template declaration, not the template definition (*).

This why in your example, when typedef void XYZ is included in FooWithTypedef, the second template parameter defaults to void and then the most fitting specialization is found. This works even if in the template specialization the second argument is defined as T::XYZ instead of void. If these are the same at the time of evaluation, the template specialization will be selected (§14.4 "Type equivalence").

(*) I didn't find a statement in the Standard that actually says it so clearly. But there is §14.1/10, which describes the case where you have multiple declarations (but only one primary definition) of a template:

(§14.1/10) The set of default template-arguments available for use with a template declaration or definition is obtained by merging the default arguments from the definition (if in scope) and all declarations in scope in the same way default function arguments are (8.3.6). [ Example:

  template<class T1, class T2 = int> class A;
  template<class T1 = int, class T2> class A;

is equivalent to

  template<class T1 = int, class T2 = int> class A;

].

This suggests that the mechanism behind default template arguments is independent of that used to identify the most fitting specialization of the template.

In addition, there are two existing SO posts that refer to this mechanism as well:

This reply to Template specialization to use default type if class member typedef does not exist

Default values of template parameters in the class template specializations

like image 101
jogojapan Avatar answered Oct 16 '22 16:10

jogojapan