Вопрос

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

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

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

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

Это было полезно?

Решение

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

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 для включения или отключения различных реализаций каждый раз.

Другие советы

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

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

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

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

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

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

template<typename T>
class ContainerBase{};

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top