C ++ Шаблоны полиморфизм - PullRequest
       16

C ++ Шаблоны полиморфизм

28 голосов
/ 05 февраля 2010

У меня есть такая структура классов.

class Interface{
...
}

class Foo : public Interface{
...
}

template <class T>
class Container{
...
}

И у меня есть этот конструктор другого класса Bar.

Bar(const Container<Interface> & bar){
...
}

Когда я вызываю конструктор таким образом, я получаю ошибку "нет соответствующей функции"

Container<Foo> container ();

Bar * temp = new Bar(container);

Что не так? Разве шаблоны не полиморфны?

Ответы [ 6 ]

38 голосов
/ 05 февраля 2010

Я думаю, что точная терминология для того, что вам нужно, это "ковариация шаблона", означающая, что если B наследуется от A, то каким-то образом T<B> наследуется от T<A>. Это не так в C ++, а также в случае Java и C # generics *.

Есть веская причина избегать ковариации шаблона: это просто удалит все типы безопасности в классе шаблона. Позвольте мне объяснить на следующем примере:

//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!
}

Следовательно, вы должны рассматривать T<A> и T<B> как совершенно не связанные типы, независимо от отношения между A и B.

Так как вы могли решить проблему, с которой вы столкнулись? В Java и C # можно использовать соответственно ограниченные подстановочные знаки и ограничения :

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

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

Следующий стандарт C ++ (известный как C ++ 1x (ранее C ++ 0x)) изначально содержал еще более мощный механизм с именем Concepts , который позволил бы разработчикам обеспечивать соблюдение синтаксических и / или семантических требований. по параметрам шаблона, но, к сожалению, был отложен на более поздний срок. Однако в Boost есть библиотека Concept Check , которая может вас заинтересовать.

Тем не менее, концепции могут быть немного излишними для проблемы, с которой вы сталкиваетесь, использование простого статического утверждения, предложенного @ gf , вероятно, является лучшим решением.

* Обновление: начиная с .Net Framework 4, можно отметить, что общие параметры имеют ковариантный или контравариантный .

11 голосов
/ 05 февраля 2010

Здесь есть две проблемы: конструкции по умолчанию имеют вид MyClass c;; в скобках это выглядит как объявление функции для компилятора.

Другая проблема в том, что Container<Interface> - это просто другой тип, чем Container<Foo> - вместо этого вы могли бы сделать следующее, чтобы фактически получить полиморфизм:

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

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

Или, конечно, вы можете сделать Bar или его конструктор шаблоном, как показал Корнел.

Если вам действительно нужен какой-то типизированный полиморфизм времени компиляции, вы можете использовать Boost.TypeTraits is_base_of или какой-то эквивалент:

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
}
5 голосов
/ 05 февраля 2010

Нет. Представьте, что параметр контейнера «жестко закодирован» в определяемый им класс (и именно так он и работает) Следовательно, тип контейнера Container_Foo, который не совместим с Container_Interface.

Однако вы можете попробовать следующее:

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

Тем не менее, вы теряете прямую проверку типов таким образом.

На самом деле способ STL (возможно, более эффективный и общий) состоит в

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

... но я предполагаю, что в контейнере не реализованы итераторы.

2 голосов
/ 05 июля 2011

Я предлагаю следующий обходной путь, который использует шаблонную функцию. Хотя в примере используется QList Qt, ничто не мешает прямому переносу решения в любой другой контейнер.

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;
}

Плюсы:

  • вообще
  • типобезопасный
  • довольно эффективно, если элементы являются указателями или некоторыми другими дешевыми конструктивными элементами (такими как неявно разделяемые классы Qt)

Минусы:

  • требует создания нового контейнера, в отличие от возможности повторного использования исходного
  • подразумевает некоторую нагрузку на память и процессор как для создания, так и для заполнения нового контейнера, которые сильно зависят от стоимости конструктора копирования
2 голосов
/ 05 февраля 2010

Можно создать дерево наследования для контейнеров, отражающее дерево наследования данных.Если у вас есть следующие данные:

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;
};

У вас также могут быть следующие контейнеры:

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 ); }
};

Это не самый эффективный код;это просто, чтобы дать идею.Единственная проблема с этим подходом состоит в том, что каждый раз, когда вы добавляете новый класс данных, вы также должны создавать новый контейнер.Кроме того, у вас есть полиморфизм "снова работает".Вы можете быть конкретным или общим:

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 );
}
0 голосов
/ 05 февраля 2010

контейнер - это контейнер объектов Foo, а не контейнер объектов интерфейса

И это также не может быть полиморфным, могут быть указатели на вещи, но не объекты сами по себе. Насколько большими должны быть слоты в контейнере для контейнера, если вы можете поместить в него что-нибудь, полученное из интерфейса

вам нужно

 container<Interface*>

или лучше

 container<shared_ptr<Interface> >
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...