C++ Templates polymorphism

只谈情不闲聊 提交于 2019-11-26 17:30:25
Luc Touraille

I think the exact terminology for what you need is "template covariance", meaning that if B inherits from A, then somehow T<B> inherits from T<A>. This is not the case in C++, nor it is with Java and C# generics*.

There is a good reason to avoid template covariance: this will simply remove all type safety in the template class. Let me explain with the following example:

//Assume the following class hierarchy
class Fruit {...};

class Apple : public Fruit {...};

class Orange : public Fruit {...};

//Now I will use these types to instantiate a class template, namely std::vector
int main()
{
    std::vector<Apple> apple_vec;
    apple_vec.push_back(Apple()); //no problem here

    //If templates were covariant, the following would be legal
    std::vector<Fruit> & fruit_vec = apple_vec;

    //push_back would expect a Fruit, so I could pass it an Orange
    fruit_vec.push_back(Orange()); 

    //Oh no! I just added an orange in my apple basket!
}

Consequently, you should consider T<A> and T<B> as completely unrelated types, regardless of the relation between A and B.

So how could you solve the issue you're facing? In Java and C#, you could use respectively bounded wildcards and constraints:

//Java code
Bar(Container<? extends Interface) {...}

//C# code
Bar<T>(Container<T> container) where T : Interface {...}

The next C++ Standard (known as C++1x (formerly C++0x)) initially contained an even more powerful mechanism named Concepts, that would have let developers enforce syntaxic and/or semantic requirements on template parameters, but was unfortunately postponed to a later date. However, Boost has a Concept Check library that may interest you.

Nevertheless, concepts might be a little overkill for the problem you encounter, an using a simple static assert as proposed by @gf is probably the best solution.

* Update: Since .Net Framework 4, it is possible to mark generic parameters has being covariant or contravariant.

There are two problems here: default constructions have the form MyClass c;; with parentheses it looks like a function declaration to the compiler.

The other problem is that Container<Interface> is simply a different type then Container<Foo> - you could do the following instead to actually get polymorphism:

Bar::Bar(const Container<Interface*>&) {}

Container<Interface*> container;
container.push_back(new Foo);
Bar* temp = new Bar(container);

Or of course you could make Bar or its constructor a template as Kornel has shown.

If you actually want some type-safe compile-time polymorphism, you could use Boost.TypeTraits is_base_of or some equivalent:

template<class T>
Bar::Bar(const Container<T>& c) {
    BOOST_STATIC_ASSERT((boost::is_base_of<Interface, T>::value));
    // ... will give a compile time error if T doesn't 
    // inherit from Interface
}

No. Imagine that the container parameter is "hardcoded" into the class it defines (and that is actually how it works). Hence the container type is Container_Foo, that is not compatible with Container_Interface.

What you might try however is this:

template<class T>
Bar(const Container<T> & bar){
...
}

Yet you loose direct type checking that way.

Actually the STL way (probably more effective and generic) would be to do

template<class InputIterator>
Bar(InputIterator begin, InputIterator end){
...
}

... but I assume you don't have iterators implemented in the container.

It is possible to create an inheritance tree for containers, reflecting the inheritance tree of the data. If you have the following data:

class Interface {
public:
    virtual ~Interface()
        {}
    virtual void print() = 0;
};

class Number : public Interface {
public:
    Number(int value) : x( value )
        {}
    int get() const
        { return x; }
    void print()
        { std::printf( "%d\n", get() ); };
private:
    int x;
};

class String : public Interface {
public:
    String(const std::string & value) : x( value )
        {}
    const std::string &get() const
        { return x; }
    void print()
        { std::printf( "%s\n", get().c_str() ); }
private:
    std::string x;
};

You could also have the following containers:

class GenericContainer {
public:
    GenericContainer()
        {}
    ~GenericContainer()
        { v.clear(); }

    virtual void add(Interface &obj)
        { v.push_back( &obj ); }
    Interface &get(unsigned int i)
        { return *v[ i ]; }
    unsigned int size() const
        { return v.size(); }
private:
    std::vector<Interface *> v;
};

class NumericContainer : public GenericContainer {
public:
    virtual void add(Number &obj)
        { GenericContainer::add( obj ); }
    Number &get(unsigned int i)
        { return (Number &) GenericContainer::get( i ); }
};

class TextContainer : public GenericContainer {
public:
    virtual void add(String &obj)
        { GenericContainer::add( obj ); }
    String &get(unsigned int i)
        { return (String &) GenericContainer::get( i ); }
};

This is not the best performing code; it is just to give an idea. The only problem with this approach is that every time you add a new Data class, you have to also create a new Container. Apart from that, you have polymorphism "working again". You can be specific or general:

void print(GenericContainer & x)
{
    for(unsigned int i = 0; i < x.size(); ++i) {
        x.get( i ).print();
    }
}

void printNumbers(NumericContainer & x)
{
    for(unsigned int i = 0; i < x.size(); ++i) {
        printf( "Number: " );
        x.get( i ).print();
    }
}

int main()
{
    TextContainer strContainer;
    NumericContainer numContainer;
    Number n( 345 );
    String s( "Hello" );

    numContainer.add( n );
    strContainer.add( s );

    print( strContainer );
    print( numContainer );
    printNumbers( numContainer );
}

I propose the following workaround, which employs a template function. Although the example use Qt's QList, nothing prevents the solution from being straightforwardly transposed to any other container.

template <class D, class B> // D (Derived) inherits from B (Base)
QList<B> toBaseList(QList<D> derivedList)
{
    QList<B> baseList;
    for (int i = 0; i < derivedList.size(); ++i) {
        baseList.append(derivedList[i]);
    }
    return baseList;
}

Pros:

  • general
  • type-safe
  • fairly efficient if the items are pointers or some other cheaply copy-constructible elements (such as implicitly shared Qt classes)

Cons:

  • requires the creation of a new container, as opposed to enabling the reuse of the original one
  • implies some memory and processor overhead both to create and to populate the new container, which depend heavily on the cost of the copy-constructor
#include <iostream>
#include <sstream>
#include <map>
#include <vector>

struct Base { int b = 111; };
struct Derived: public Base { };

struct ObjectStringizer {
    template <typename T>
    static std::string to_string(const T& t) {
        return helper<T>()(t);
    }

    template <typename T, typename = void>
    struct helper {
        std::string operator()(const T& t) {
            std::ostringstream oss;
            oss << t;
            return oss.str();
        }
    };

    template <typename T>
    struct helper<T, typename std::enable_if<std::is_base_of<Base, T>::value>::type> {
        std::string operator()(const T& base) {
            return to_string(base.b);
        }
    };

    template <typename T>
    struct helper<std::vector<T>> {
        std::string operator()(const std::vector<T>& v) {
            std::ostringstream oss;
            for (size_t i = 0, sz = v.size(); i < sz; ++i) {
                oss << (i ? "," : "") << to_string(v[i]);
            }
            return "[" + oss.str() + "]";
        }
    };

    template <typename Key, typename Value>
    struct helper<std::map<Key, Value>> {
        std::string operator()(const std::map<Key, Value>& m) {
            std::ostringstream oss;
            for (auto iter = m.begin(), iter_end = m.end(); iter_end != iter; ++iter) {
                oss << (m.begin() != iter ? "," : "") << to_string(iter->first) << ":" << to_string(iter->second);
            }
            return "{" + oss.str() + "}";
        }
    };
};

int main(int argc, char* argv[]) {
    std::cout << ObjectStringizer::to_string("hello ") << ObjectStringizer::to_string(std::string("world")) << std::endl;
    std::cout << ObjectStringizer::to_string(Derived()) << std::endl;
    std::cout << ObjectStringizer::to_string(std::vector<int>{3, 5, 7, 9}) << std::endl;
    std::cout << ObjectStringizer::to_string(std::map<int, std::string>{{1, "one"}, {2, "two"}}) << std::endl;
    return 0;
}

container is a container of Foo objects not a container of Interface objects

And it cannot be polymorphic either, pointers to things can be ,but not the objects themselvs. How big would the slots in the container have to be for container if you could put anything derived from interface in it

you need

 container<Interface*>

or better

 container<shared_ptr<Interface> >
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!