2009-03-06 2 views
7

Какова связь между использованием виртуальных функций и механизмами наследования C++ по сравнению с использованием шаблонов и что-то вроде концепций повышения?Проверка концепции на C++ vs inheritance

Похоже, что существует вполне перекрытие того, что возможно. А именно, представляется возможным достичь полиморфного поведения при любом подходе. Итак, когда имеет смысл отдавать предпочтение одному другому?

Причина, по которой я это делаю, заключается в том, что у меня есть шаблон с шаблонами, где сами контейнеры имеют иерархические отношения. Я хотел бы написать алгоритмы, которые используют эти контейнеры, не заботясь о том, какой конкретный контейнер он есть. Кроме того, некоторые алгоритмы выиграют от знания того, что тип шаблона удовлетворял определенным понятиям (например, Comparable).

Итак, с одной стороны, я хочу, чтобы контейнеры вели себя полиморфно. С другой стороны, мне все еще нужно использовать понятия, если я хочу правильно реализовать некоторые алгоритмы. Что делает младший разработчик?

ответ

6

Я думаю о концепциях как о мета-интерфейсе. Они классифицируют типы после своих способностей. Следующая версия C++ предоставляет собственные концепции. Я не понял этого, пока не натолкнулся на концепции C++ 1x и как они позволяют объединять разные, но несвязанные типы. Представьте, что у вас есть интерфейс Range. Вы можете моделировать это двумя способами. Одним из них является соотношение подтип:

class Range { 
    virtual Iterator * begin() = 0; 
    virtual Iterator * end() = 0; 

    virtual size_t size() = 0; 
}; 

Конечно, каждый класс, производный от, который реализует интерфейс Range и может использоваться с функциями. Но теперь вы видите, что он ограничен. Как насчет массива? Это тоже диапазон!

T t[N]; 

begin() => t 
end() => t + size() 
size() => N 

К сожалению, вы не можете получить массив из этого класса Range, реализующего этот интерфейс.Вам нужен дополнительный метод (перегрузка). А как насчет сторонних контейнеров? Пользователь вашей библиотеки может захотеть использовать свои контейнеры вместе с вашими функциями. Но он не может изменить определение своих контейнеров. Здесь понятия вступают в игру:

auto concept Range<typename T> { 
    typename iterator; 
    iterator T::begin(); 
    iterator T::end(); 
    size_t T::size(); 
} 

Теперь, вы говорите что-то о поддерживаемых операциях некоторого типа, которые могут быть выполнены, если T имеют соответствующие функции-членов. В вашей библиотеке вы должны написать функцию generic. Это позволяет принимать любые типы , пока поддерживает необходимые операции:

template<Range R> 
void assign(R const& r) { 
    ... iterate from r.begin() to r.end(). 
} 

Это отличный вид взаимозаменяемости. Любой тип подходит к законопроекту, который придерживается концепции, а не только те типы, которые активно реализуют некоторый интерфейс. Следующий стандарт C++ продвигается дальше: он определяет концепцию Container, которая будет соответствовать простым массивам (по какой-то причине концепция map, которая определяет, как какой-то тип подходит для какой-то концепции) и другие, существующие стандартные контейнеры.

Причина, по которой я это делаю, заключается в том, что у меня есть шаблон с шаблонами, где сами контейнеры имеют иерархические отношения. Я хотел бы написать алгоритмы, которые используют эти контейнеры, не заботясь о том, какой конкретный контейнер он есть. Кроме того, некоторые алгоритмы выиграют от знания того, что тип шаблона удовлетворял определенным понятиям (например, Comparable).

На самом деле вы можете сделать оба с шаблонами. Вы можете продолжать использовать иерархические отношения для совместного использования кода, а затем писать алгоритмы в общем виде. Например, чтобы сообщить, что ваш контейнер сопоставим. Это как стандартный произвольный доступ вперед/назад/выход/вход категория итераторов реализованы:

// tag types for the comparator cagetory 
struct not_comparable { }; 
struct basic_comparable : not_comparable { }; 

template<typename T> 
class MyVector : public BasicContainer<T> { 
    typedef basic_comparable comparator_kind; 
}; 

/* Container concept */ 
T::comparator_kind: comparator category 

Это разумный простой способ сделать это, на самом деле. Теперь вы можете вызвать функцию, и она перейдет к правильной реализации.

template<typename Container> 
void takesAdvantage(Container const& c) { 
    takesAdvantageOfCompare(c, typename Container::comparator_kind()); 
} 

// implementation for basic_comparable containers 
template<typename Container> 
void takesAdvantage(Container const& c, basic_comparable) { 
    ... 
} 

// implementation for not_comparable containers 
template<typename Container> 
void takesAdvantage(Container const& c, not_comparable) { 
    ... 
} 

На самом деле существуют различные методы, которые могут быть использованы для его реализации. Другой способ - использовать boost::enable_if для включения или отключения различных реализаций каждый раз.

+0

C++ 1x? Означает ли это, что они отказались от выпуска нового стандарта в этом десятилетии или вы говорите о будущей разработке на C++? – jpalecek

+0

http://www.research.att.com/~bs/C++0xFAQ.html#concepts – jmucchiello

+0

jpalecek, они хотят выпустить его в 2010 году. У меня есть привычка называть его C++ 1x :) –

0

Если решение может быть принято во время компиляции, используйте шаблоны. В противном случае используйте наследование и виртуальные функции.

1

Да, полиморфное поведение возможно с обоими механизмами. Фактически, оба являются , называемый также полиморфизмом.

Виртуальные функции дают вам динамический полиморфизм (потому что он решен во время выполнения), в то время как шаблоны дают вам статический полиморфизм (все решено во время компиляции).

И это должно отвечать на вопрос о том, какой из них предпочтительнее. По возможности предпочитайте переводить работу во время компиляции. Поэтому, когда вам это удастся, используйте шаблоны, чтобы решить ваши потребности в полиморфизме. И когда это невозможно (потому что вам нужно использовать информацию типа времени выполнения, поскольку точные типы не известны во время компиляции), вернитесь к динамическому полиморфизму.

(Конечно, могут быть другие причины, чтобы предпочесть тот или иной. В частности, шаблоны требуют, чтобы вы переместили много кода в файлы заголовков, которые могут быть или не быть проблемой, а скорость компиляции имеет тенденцию страдать, которые также могут или не могут быть проблемой.)

0

В данном конкретном случае вы можете сделать что-то вроде

template<typename T> 
class ContainerBase{}; 

template<typename T> 
class ContainerDerived : public ContainerBase<T> {}; 

Поскольку каждый типа «Контейнера» является уникальным для каждого типа шаблона, нет никаких функций членов причины каждый тип контейнера не может быть специализирован на чертах шаблонного типа.

0

В качестве простого примера разницы между компиляцией и время выполнения полиморфизма рассмотрит следующий код:

template<typename tType> 
struct compileTimePolymorphism 
{ }; 

// compile time polymorphism, 
// you can describe a behavior on some object type 
// through the template, but you cannot interchange 
// the templates 
compileTimePolymorphism<int> l_intTemplate; 
compileTimePolymorphism<float> l_floatTemplate; 
compileTimePolymorphism *l_templatePointer; // ???? impossible 

struct A {}; 
struct B : public A{}; 
struct C : public A{}; 

// runtime polymorphism 
// you can interchange objects of different type 
// by treating them like the parent 
B l_B; 
C l_C: 
A *l_A = &l_B; 
l_A = &l_C; 

Compile время полиморфизм является хорошим решением, когда поведение одного объекта зависит от какого-либо другого объект. Полиморфизм времени выполнения необходим, когда поведение объекта необходимо изменить.

Оба могут быть объединены путем определения шаблона, который является полиморфным:

template<typename tType> 
struct myContainer : public tType 
{}; 

Тогда возникает вопрос, где поведение вашего контейнера необходимо изменить (во время выполнения полиморфизм), и где поведение зависит от объектов он содержит (полиморфизм времени компиляции).