9

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?

dragonroot
  • 5,653
  • 3
  • 38
  • 63

3 Answers3

4

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

Community
  • 1
  • 1
jogojapan
  • 68,383
  • 11
  • 101
  • 131
  • 1
    Your answers seems to be the right answer, but there's something I'm still missing here I think. I've edited the question to be more specific. Could you edit yours to clarify why a typedefed type is dependent, but an inner class isn't? – dragonroot Aug 22 '12 at 22:21
  • 1
    @dragonroot: I have added an answer to the `typedef` part of the question. – jogojapan Aug 23 '12 at 02:54
2

Here is another version that detects the presence of the inner class :

#include <iostream>

template< typename T >
struct HasXYZ
{
  typedef char                 yes;
  typedef struct{ char d[2]; } no;

  template<typename T1>
  static yes test( typename T1::XYZ * );
  template<typename T1>
  static no test(...);

  static const bool value = ( sizeof( test<T>(0) ) == sizeof( yes ) );
};

struct Foo
{
  class XYZ {};
};
struct Bar
{
  class ABC {};
};

int main()
{
  std::cout << std::boolalpha << HasXYZ< Foo >::value << std::endl;
  std::cout << std::boolalpha << HasXYZ< Bar >::value << std::endl;
}
BЈовић
  • 62,405
  • 41
  • 173
  • 273
1

A::XYZ would need to void to have the partial specialization selected, which can never be the case for a class type. One way to make it work is using a fake dependant void typename:

template<class T>
struct void_{ typedef void type; };

template<class T, class = void>
struct has_XYZ{ static bool const value = false; };

template<class T>
struct has_XYZ<T, typename void_<typename T::XYZ>::type>{
  static bool const value = true;
};

For an explanation on how this works, see this question.

Community
  • 1
  • 1
Xeo
  • 129,499
  • 52
  • 291
  • 397
  • Oh, so it must match `void` which was given in the initial template declaration? Now I am lost. How does this whole specialization work in this case, where I have two args initially, the second being defaulted to `void`, and a specialization where the second arg still matches `void`? – dragonroot Aug 22 '12 at 22:59
  • @dragonroot: Like I said, read the linked question. If `XYZ` exists inside of `T`, the `void_<...>` part will not SFINAE out and will yield `void`, which will make that specific partial specialization match all instantiations where the second parameter is `void`, which, thanks to the default, should always be the case. Now if `XYZ` does not exist, the whole partial spec basically won't match and the base template will be selected. – Xeo Aug 22 '12 at 23:01
  • Yes, but my question here was, why did it have to be `void`? If XYZ is typedeffed to `int`, it would not work. Why? I've updated the question (again) to ask this, too. – dragonroot Aug 22 '12 at 23:10
  • @dragonroot: With the code I posted, it doesn't matter, since the `void` is provided by the little helper struct. For you code, yes, it matters. if `T::XYZ` exists and is `void`, the partial specialization will read `has_XYZ`. The default argument will then be applied whenever you use `has_XYZ` which will cause the partial specialization to be selected. If `T::XYZ` were `int`, the partial specialization would read `has_XYZ`, and the default `void` argument wouldn't match it anymore. – Xeo Aug 22 '12 at 23:12
  • Why is that partial specialization only selected if the second argument value matches the default value of the initial template? I guess my question is more about how the whole specialization selection works. Reading the standard would be too heavyweight, and I don't really know where else to look. – dragonroot Aug 22 '12 at 23:16
  • @dragonroot: You tell the compiler to use the partial spec if the second parameter is `void`, that's what `has_XYZ` means. And by default, the second argument *is* `void`. – Xeo Aug 22 '12 at 23:31