Проверка концепции C ++ против наследования - PullRequest
8 голосов
/ 07 марта 2009

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

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

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

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

Ответы [ 5 ]

6 голосов
/ 07 марта 2009

Я думаю о концепциях как о некоем мета-интерфейсе. Они классифицируют типы по своим способностям. Следующая версия 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, которая будет соответствовать простым массивам (чем-то, что называется концептуальная карта , которая определяет, как какой-то тип соответствует некоторому концепту) и другие, существующие стандартные контейнеры.

Причина, по которой я поднял этот вопрос, заключается в том, что у меня есть шаблонный контейнер, в котором сами контейнеры имеют иерархические отношения. Я хотел бы написать алгоритмы, которые используют эти контейнеры, не заботясь о том, что это за конкретный контейнер. Кроме того, некоторым алгоритмам было бы полезно знать, что тип шаблона удовлетворяет определенным понятиям (например, 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 для включения или отключения разных реализаций каждый раз.

1 голос
/ 07 марта 2009

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

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

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

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

0 голосов
/ 07 марта 2009

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

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;

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

Их можно объединить, определив полиморфный шаблон:

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

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

0 голосов
/ 07 марта 2009

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

template<typename T>
class ContainerBase{};

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

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

0 голосов
/ 07 марта 2009

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

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