Search code examples
c++templatesc++11dynamic-casttemplate-aliases

C++ dynamic downcasting to class template having template template parameter being a class template or an alias template


I hope the title makes sense. I probably miss vocabulary to express it correctly.

Well, an exemple will probably be more clear.

Problem for me is: dynamic downcasting returns 0 at run time in some of the following cases (written in comments). I'd like to know if it's a correct behaviour (using C++11), also why, and what can I do to make it work. Apparently, Templated and A::A_templated are treated as different classes, despite being defined as identical by using alias "using". Problem doesn't appear for simple typedef alias.

template <class T>
class Templated {};

class A {
    public :
    typedef int A_Type;
    template <class T>
    using A_Templated = Templated<T>;
};

class Test_base {
    public :
    Test_base() {}
    virtual void foo()=0;
};

template <class T>
class Test_Type : public Test_base {
    public :
    Test_Type() {}
    void foo() {}
};

template < template <class T> class TT >
class Test_Templated : public Test_base {
    public :
    Test_Templated() {}
    void foo() {}
};

int main() {
    Test_base* test;

    test = new Test_Type<int>;
    std::cout << dynamic_cast< Test_Type<int>* >(test) << std::endl;//-->ok
    std::cout << dynamic_cast< Test_Type<A::A_Type>* >(test) << std::endl;//-->ok

    test = new Test_Templated<Templated>;
    std::cout << dynamic_cast< Test_Templated<Templated>* >(test) << std::endl;//-->ok
    std::cout << dynamic_cast< Test_Templated<A::A_Templated>* >(test) << std::endl;//--> returns 0 !

    test = new Test_Templated<A::A_Templated>;
    std::cout << dynamic_cast< Test_Templated<A::A_Templated>* >(test) << std::endl;//-->ok
    std::cout << dynamic_cast< Test_Templated<Templated>* >(test) << std::endl;//--> returns 0 !


}

I propose another way to see the problem, this is probably more clear. I'm facing it after trying to avoid the example above. The following example's basically says what Bogdan pointed out. I find very frustrating the fact the compiler can't resolve Templated with Templated_alias. I'm wondering if a compilation option exists, which can sort of force type resolving through template aliases.

template <class T>
class Templated {};

template <class T>
using Templated_alias = Templated<T>;

template < template <class T> class TT >
class B;

template <>
class B<Templated> {
    public :
    void foo(Templated<int> _arg) {}
};

int main() {
    B<Templated> b1;
    b1.foo(Templated<int>());
    b1.foo(Templated_alias<int>());//compiles => Templated_alias<int> is equivalent to Templated<int>
    B<Templated_alias> b2;//Compilation error: Implicit instantiation of undefined template B<Templated_alias>
    //which means: Templated_alias is not equivalent to Templated
}

Thanks to Bogdan's trick, and after some little nose-bleeding, I managed to find some kind of solution. The idea is to build a class in charge of 'filtering' potential aliases of template classes. It needs one specification per template class needed to be 'filtered'. Main drawback of the method is that filtering thus needs to be used everywhere template classes are used as template parameters in order to be consistent.

//Classes to be dealt with

template <class T>
class Templated {};

template <class T>
class Templated2 {};

template <class T>
using Templated_alias = Templated<T>;

class A_base {
    virtual void foo()=0;
};

template <template <class T> class TT>
class A : public A_base {
    void foo() {}
};

//Here starts the trick definition

template<template<class> class TT1, template<class> class TT2>
using is_same_template_t = typename std::is_same<TT1<int>, TT2<int> >::type;

//Template Template aliasing
template < template <class T> class TT >
class TT_aliasing {
    public :
    template <class T>
    using Class_T = TT<T>;
};

//Template Template Alias Filtering
template < template <class T> class TT, class = std::true_type>
class TT_AF {
    public :
    template <class T>
    using Class_T = TT<T>;
};

template < template <class T> class TT >
class TT_AF<TT, is_same_template_t<TT, Templated> > : public TT_aliasing<Templated> {};

int main() {

    A_base* a;
    a = new A< TT_AF<Templated>::Class_T >();
    std::cout << dynamic_cast< A< TT_AF<Templated>::Class_T >* >(a) << std::endl;
    std::cout << dynamic_cast< A< TT_AF<Templated_alias>::Class_T >* >(a) << std::endl;
    std::cout << dynamic_cast< A< TT_AF<Templated2>::Class_T >* >(a) << std::endl;

    std::cout << "---------------" << std::endl;

    a = new A< TT_AF<Templated_alias>::Class_T >();
    std::cout << dynamic_cast< A< TT_AF<Templated>::Class_T >* >(a) << std::endl;
    std::cout << dynamic_cast< A< TT_AF<Templated_alias>::Class_T >* >(a) << std::endl;
    std::cout << dynamic_cast< A< TT_AF<Templated2>::Class_T >* >(a) << std::endl;

    std::cout << "---------------" << std::endl;

    a = new A< TT_AF<Templated2>::Class_T >();
    std::cout << dynamic_cast< A< TT_AF<Templated>::Class_T >* >(a) << std::endl;
    std::cout << dynamic_cast< A< TT_AF<Templated_alias>::Class_T >* >(a) << std::endl;
    std::cout << dynamic_cast< A< TT_AF<Templated2>::Class_T >* >(a) << std::endl;

    A< TT_AF<Templated>::Class_T > a1;
    A< TT_AF<Templated_alias>::Class_T > a2;
    a1 = a2;
    A< TT_AF<Templated2>::Class_T > a3;
    //a1 = a3;//no viable overloaded '='

}

Output gives:

0x600000014ba0
0x600000014ba0
0x0
---------------
0x600000014bb0
0x600000014bb0
0x0
---------------
0x0
0x0
0x600000014bc0

After using the above trick. I ran into different problems. Can't be absolutely sure it's related but it's very likely. Compiler seems to struggle to correctly build the 'dynamic table'. I asked for this problem at C++ what can make type_info::hash_code differs for two (supposedly) same objects May be my bad, but for now I wouldn't recommend using the trick with Clang 3.1 .


Solution

  • Clang's behaviour is correct.

    A::A_Type is equivalent to int according to [7.1.3p1] in the standard:

    [...] Within the scope of its declaration, a typedef-name is syntactically equivalent to a keyword and names the type associated with the identifier in the way described in Clause 8. A typedef-name is thus a synonym for another type. A typedef-name does not introduce a new type the way a class declaration (9.1) or enum declaration does.

    A::A_Templated<int> is equivalent to Templated<int> according to [14.5.7p2]:

    When a template-id refers to the specialization of an alias template, it is equivalent to the associated type obtained by substitution of its template-arguments for the template-parameters in the type-id of the alias template.

    However, A::A_Templated is not equivalent to Templated, according to [14.5.7p1]:

    [...] The name of the alias template is a template-name.

    This means that A::A_Templated and Templated are two different templates, so Test_Templated<A::A_Templated> and Test_Templated<Templated> are different specializations of Test_Templated, thus the casts that return null pointers are correct in doing so.

    GCC 5.1.0 doesn't handle this correctly. Clang 3.6.0 and MSVC 14 RC handle it correctly.


    All references are to working draft N4431.

    Note that there is an active Core Working Group Issue regarding this behaviour - Issue 1286. The author says that the intention is to introduce standard wording to make such cases work the way you expected, that is, make the alias template equivalent to the one referenced in the type-id. There's a note from May 2015 in there, indicating that the issue is receiving attention, but it's not there yet.


    In terms of "making it work", it's difficult to give solutions without knowing what your practical needs are, but I'd try to make Test_Templated depend on specializations of Templated, rather than the template itself, that is, declare it like

    template<class T>
    class Test_Templated : public Test_base { /* ... */ };
    

    and use it like

    test = new Test_Templated<Templated<int>>;
    std::cout << dynamic_cast< Test_Templated<Templated<int>>* >(test) << std::endl; //ok
    std::cout << dynamic_cast< Test_Templated<A::A_Templated<int>>* >(test) << std::endl; //also ok
    

    You could wrap this by adding a level of indirection, if that helps in any way:

    template<template<class> class TT, class T> using Make_Test_Templated = Test_Templated<TT<T>>;
    

    and then use it like this:

    test = new Make_Test_Templated<A::A_Templated, long>;
    std::cout << dynamic_cast< Make_Test_Templated<A::A_Templated, long>* >(test) << std::endl; //ok
    std::cout << dynamic_cast< Make_Test_Templated<Templated, long>* >(test) << std::endl; //also ok
    

    Anyway, I think the key is to try to use the fact that the specializations are equivalent.


    Alright, based on your latest update, here's a hack addressing the problem in your second code sample: change the explicit specialization B<Templated> to a partial specialization that only matches if given a template that generates the same specialization as Templated when instantiated with a certain argument (let's say int for this example).

    How's that for a confusing sentence? Sorry. Here's what your code sample becomes with the above changes:

    #include <iostream>
    #include <type_traits>
    
    template<class> class Templated { };
    template<class T> using Templated_alias = Templated<T>;
    template<class> class Templated2 { };
    
    // Helper trait
    template<template<class> class TT1, template<class> class TT2>
    using is_same_template_t = typename std::is_same<TT1<int>, TT2<int>>::type;
    
    template<template<class> class, class = std::true_type> class B;
    template<template<class> class TT> class B<TT, is_same_template_t<TT, Templated>>
    {
    public:
       void foo(Templated<int>) { std::cout << "B<Templated>::foo\n"; }
    };
    
    int main() {
       B<Templated> b1;
       b1.foo(Templated<int>());
       b1.foo(Templated_alias<int>());
       B<Templated_alias> b2; // Works fine now, and so do the next two lines.
       b2.foo(Templated<int>());
       b2.foo(Templated_alias<int>());
       // B<Templated2> b22; // Error trying to instantiate the primary template B.
    }
    

    Note that you have to make sure is_same_template_t is only used to check templates that can be instantiated with an int argument (change int to whatever you need, of course). If you want to make it more generic, you can also include the type on which the templates need to be instantiated in the trait's parameter list, like this:

    template<template<class> class TT1, template<class> class TT2, class T>
    using is_same_template_t = typename std::is_same<TT1<T>, TT2<T>>::type;
    

    and use it like this:

    is_same_template_t<TT, Templated, int>