Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is the following template specialization code non-standard or a bug in VS-C++?

The following code compiles in GCC (I used ideone, which uses gcc-4.3.4) but does not compile in Visual Studio. Is it standard code and a bug in Visual C++ 2008 and 2010 (I tried it in both) or non-standard and GCC is happy to compile it?

namespace cool
{
  template <bool, typename = void> struct enable_if {};
  template <typename T> struct enable_if<true, T> { typedef T type; };

  template <typename T0, typename T1> struct is_same { enum { value = false }; };
  template <typename T> struct is_same<T, T> { enum { value = true }; };
}

struct BasePolicy {};
struct BasePolicy2 {};
struct Faz {};

template <typename Policy,
typename = typename cool::enable_if<cool::is_same<BasePolicy, Policy>::value || cool::is_same<BasePolicy2, Policy>::value>::type >
struct Foo;

template <typename Policy>
struct Foo<Policy> {
  Foo();
};

template <typename Policy>
Foo<Policy>::Foo() {
}

int main()
{
  Foo<BasePolicy2> fb;
  // Foo<Faz> fb1;
}

Error 1 error C2039: '{ctor}' : is not a member of 'Foo' main.cpp 25

Note that the problem is the out-of-line definition of Foo's constructor. If you define it in the class, then Visual-C++ is happy:

template <typename Policy>
struct Foo<Policy> {
  Foo() {}
};

Also, the following code compiles on both (note that the the || and the logic after it is missing):

namespace cool
{
  template <bool, typename = void> struct enable_if {};
  template <typename T> struct enable_if<true, T> { typedef T type; };

  template <typename T0, typename T1> struct is_same { enum { value = false }; };
  template <typename T> struct is_same<T, T> { enum { value = true }; };
}

struct BasePolicy {};
struct BasePolicy2 {};
struct Faz {};

template <typename Policy,
  typename = typename cool::enable_if<cool::is_same<BasePolicy, Policy>::value>::type >
struct Foo;

template <typename Policy>
struct Foo<Policy> {
  Foo();
};

template <typename Policy>
Foo<Policy>::Foo() {
}

int main()
{
  Foo<BasePolicy> fb;
  // Foo<Faz> fb1;
}

Credit where credit is due, this is a slightly modified version given to me by Dietmar Kühl)

like image 539
Samaursa Avatar asked Mar 12 '12 15:03

Samaursa


1 Answers

Visual C++ 2008/2010 is at fault. But it can be worked around - in more than one way.

Let's consider the type of Foo<BasePolicy2> fb.

This declaration defaults the 2nd template parameter of template Foo<> as first declared. So explicitly its type is:

/*1*/ Foo<BasePolicy2,cool::enable_if<
            cool::is_same<BasePolicy, BasePolicy2>::value ||
            cool::is_same<BasePolicy2,BasePolicy2>::value
        >::type
    >

If you are already satisfied that /*1*/ boils down to:

/*2*/ Foo<BasePolicy2,void>

then you can meet us again at The Rendezvous, below.

Well, we can see that the type:

/*3/ cool::enable_if<
        cool::is_same<BasePolicy, BasePolicy2>::value ||
        cool::is_same<BasePolicy2,BasePolicy2>::value
    >

resolves to:

/*4/ cool::enable_if<some_boolean_consant>

Next, let's check on how template enable_if<> is defined. In namespace cool we've got:

/*5/ template <bool, typename = void> struct enable_if {};
/*6/ template <typename T> struct enable_if<true, T> { typedef T type; };

so /*4*/ in its turn is defaulting the 2nd template parameter of template enable_if<>, and the type of that default is void.

Ok, then /*6*/ specializes template enable_if<> with respect to its second template parameter whenever its first bool parameter has the value true, and it says in that case, enable_if<> shall export a typedef type that has the type of the 2nd template parameter. If the first bool parameter is false, then that typedef just won't exist and our compiler will barf.

Alright, we know that if /*4*/ will compile at all, then some_boolean_consant == true, and the exported type type is that of the defaulted 2nd template parameter of enable_if<>. Which is void.

Now've deduced the type of:

/*7*/   cool::enable_if<
            cool::is_same<BasePolicy, BasePolicy2>::value ||
            cool::is_same<BasePolicy2,BasePolicy2>::value
        >::type

It is void. So /*1*/ boils down to /*2*/, and that is the defaulted type of Foo<BasePolicy2>.

Which is as it should be, but if you are doubtful of this conclusion, then just add this to the program at global scope and compile:

typedef cool::enable_if<
            cool::is_same<BasePolicy, BasePolicy2>::value ||
            cool::is_same<BasePolicy2,BasePolicy2>::value
        >::type WhatType;
WhatType what_am_i;

The compiler will say:

'what_am_i' : illegal use of type 'void'

or words to that effect.

The Rendezvous

The knowledge that /*1/ = /*2/ gives us some leverage on the Visual C++ compilation error that the question is about:

Error 1 error C2039: '{ctor}' : is not a member of 'Foo' main.cpp 25

The error is complaining that the constructor giving rise to it is not in fact a constructor declared by the type it must belong to, i.e. that Foo<Policy>::Foo() is not a constructor of Foo<Policy>.

Now the definition of Foo<Policy> defaults the 2nd template parameter of its initial declaration, which we know has to be void. So the question presents itself: Is the compiler in fact honouring that defaulted 2nd template parameter? - i.e. does it recognize that the template definition of Foo<Policy> is a template definition of Foo<Policy,void>?

The answer is No. For if we simply change the definition of the template and its constructor to explicitly specify the default 2nd parameter:

template <typename Policy>
struct Foo<Policy,void> {
    Foo();
};

template <typename Policy>
Foo<Policy,void>::Foo() {
}

then the program compiles clean with Visual C++.

Is Visual C++ possibly sticking up for its own - debatable - convictions about Standard C++ by raising this error? Or is it just broken? It's just broken. Because if we try it on this simpler but essentially the same program it has no problem:

/* Simple Case */

template<typename X, typename Y = void>
struct A;

template<typename X>
struct A<X> {
    A();
};

template<typename X>
A<X>::A(){};

int main()
{
    A<int> aint;
    return 0;
}

This shows that it's that mouthful of template metaprogramming /*3*/ that is causing the indigestion, and as the questioner points out, if that mouthful is experimentally simplified by deleting the || operation, all is well (except that our cool:: logic is broken, of course).

The Workarounds?

We have already seen one. Just make the void template parameter explicit in the definitions of Foo<Policy> and Foo<Folicy>::Foo(). You can leave now if that's all you want to know.

But that one itches. We're applying the fix at the level of /* Simple Case */ when we know the bug is not general at that level. It's down in the obscurity of the compiler's template metaprogramming works, so a workaround that doesn't itch would at least be confined to namespace cool.

Here is a rule of caution about template metaprogramming (TMP) helper templates that I hope the progress of compilers will soon let me forget: Don't let the blighters instantiate in flight. Such templates exist only to facilitate compiletime logic. As long as the compiler can perform that logic just by recursively defining types it is likely to stay in its comfort zone (at least as long as its instantiation-depth holds out). If it is obliged to instantiate intermediate types in the furtherance of compiletime logic then its weirdest peccadilloes are apt to surface.

If you code your TMP helper classes in such a way that compiletime logic can only be accomplished by taking the values of static or enumerated constants that they expose, then you compel the compiler to instantiate all the time, because only thus do those static or enumerated constants acquire values.

Case in point, class /*3*/ with its mysteriously toxic || operation. The TMP helper classes of namespace cool do their meta-logic on boolean constants - unnecessarily.

The cautious approach to TMP helper classes is to define them so that logical operations can all be simulated, instantiation-free, by recursive type definitions, with constants being exported - lest you ultimately need them - only when the all the compiletime logic is successfully landed. A cautious re-write of the contents of namespace cool might look something like this:

namespace cool
{
    template<bool val>
    struct truth_type
    {
        static const bool value = false;
    };

    template<>
    struct truth_type<true>
    {
        static const bool value = true;
    };

    typedef truth_type<true> true_type;
    typedef truth_type<false> false_type; 

    template<class lhs,class rhs>
    struct or_type
    {
        typedef false_type type;
    };

    template<class lhs>
    struct or_type<lhs,true_type>
    {
        typedef true_type type;
    };

    template<class rhs>
    struct or_type<true_type,rhs>
    {
        typedef true_type type;
    };

    template <typename T, typename = void> struct enable_if {};
    template <typename T> struct enable_if<true_type, T> { typedef T type; };

    template <typename T0, typename T1> struct is_same {
        typedef false_type type;
    };
    template <typename T> struct is_same<T, T> {
        typedef true_type type;
    };
}

and the corresponding declaration of template Foo<> would look like this:

template <typename Policy,
typename = typename cool::enable_if<
    typename cool::or_type<
        typename cool::is_same<BasePolicy, Policy>::type,
        typename cool::is_same<BasePolicy2, Policy>::type
    >::type
>::type>
struct Foo;

This way, none of our cool:: things is ever instantiated until the whole shebang is instantiated: we deal solely in types for all the meta-logic. If these changes are made to the program then the itchy workaround is not required. And GCC is happy with it also.

like image 163
Mike Kinghan Avatar answered Sep 27 '22 17:09

Mike Kinghan