Полиморфизм шаблонов С++
У меня есть эта структура классов.
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);
Что не так? Не являются ли шаблоны полиморфными?
Ответы
Ответ 1
Я думаю, что точная терминология для того, что вам нужно, это "шаблонная ковариация", что означает, что если B наследует от A, то как-то T<B>
наследуется от T<A>
. Это не относится к С++ и не относится к Java и С# 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 и С# вы можете использовать соответственно ограниченные подстановочные знаки и ограничения:
//Java code
Bar(Container<? extends Interface) {...}
//C# code
Bar<T>(Container<T> container) where T : Interface {...}
Следующий стандарт С++ (известный как С++ 1x (ранее С++ 0x)) первоначально содержал еще более мощный механизм с именем Concepts, которые позволили бы разработчикам применять синтаксические и/или семантические требования к параметрам шаблона, но, к сожалению, были перенесены на более позднюю дату. Однако Boost имеет концепцию Check library, которая может вас заинтересовать.
Тем не менее, концепции могут быть немного переполнены проблемой, с которой вы сталкиваетесь, использование простого static assert, предложенное @gf, вероятно, является лучшим решением.
* Обновление. Начиная с .Net Framework 4, можно отметить, что общие параметры ковариантные или контравариантные.к югу >
Ответ 2
Здесь есть две проблемы: конструкции по умолчанию имеют форму 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
}
Ответ 3
Нет. Представьте, что параметр контейнера "жестко закодирован" в класс, который он определяет (и на самом деле это работает). Следовательно, тип контейнера Container_Foo
, который несовместим с Container_Interface
.
Однако вы можете попробовать следующее:
template<class T>
Bar(const Container<T> & bar){
...
}
Тем не менее, вы теряете непосредственный контроль типа таким образом.
На самом деле способ STL (вероятно, более эффективный и общий) состоял бы в том, чтобы делать
template<class InputIterator>
Bar(InputIterator begin, InputIterator end){
...
}
... но я предполагаю, что у вас нет итераторов, реализованных в контейнере.
Ответ 4
Можно создать дерево наследования для контейнеров, отражающее дерево наследования данных. Если у вас есть следующие данные:
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 ); }
};
Это не лучший код; это просто дать идею. Единственная проблема с этим подходом заключается в том, что каждый раз, когда вы добавляете новый класс Data, вы также должны создавать новый контейнер. Кроме того, у вас есть полиморфизм "снова работает". Вы можете быть конкретным или общим:
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 );
}
Ответ 5
Я предлагаю следующее обходное решение, в котором используется функция шаблона. Хотя в примере используется Qt QList, ничто не мешает правильному переносу решения в любой другой контейнер.
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)
Минусы:
- требует создания нового контейнера, в отличие от повторного использования исходного
- подразумевает некоторые издержки памяти и процессора как для создания, так и для заполнения нового контейнера, которые в значительной степени зависят от стоимости конструктора-копии
Ответ 6
контейнер - это контейнер объектов Foo, а не контейнер объектов интерфейса
И это тоже не может быть полиморфным, указатели на вещи могут быть, но не объекты themselvs. Насколько велики слоты в контейнере должны быть для контейнера, если вы могли бы положить что-нибудь из интерфейса в нем
вам нужно
container<Interface*>
или лучше
container<shared_ptr<Interface> >