Наследование против специализации - PullRequest
8 голосов
/ 07 апреля 2009

Учитывая следующие два сценария использования (именно так, как вы их видите, то есть конечный пользователь будет заинтересован только в использовании Vector2_t и Vector3_t):

[1] Наследование:

template<typename T, size_t N> struct VectorBase
{
};

template<typename T> struct Vector2 : VectorBase<T, 2>
{
};

template<typename T> struct Vector3 : VectorBase<T, 3>
{
};

typedef Vector2<float> Vector2_t;
typedef Vector3<float> Vector3_t;

[2] Специализация:

template<typename T, size_t N> struct Vector
{
};

template<typename T> struct Vector<T, 2>
{
};

template<typename T> struct Vector<T, 3>
{
};

typedef Vector<float, 2> Vector2_t;
typedef Vector<float, 3> Vector3_t;

Я не могу определиться с тем, какое решение лучше. Очевидным преимуществом наследования является повторное использование кода в производных классах; Возможный недостаток - производительность (больший размер, пользователи могут передавать по значению и т. д.). Специализация, кажется, избегает всего этого, но за счет того, что мне приходится повторяться несколько раз.

Какие еще преимущества / недостатки я упустил, и, по вашему мнению, какой маршрут мне выбрать?

Ответы [ 4 ]

12 голосов
/ 07 апреля 2009

То, что вы в конечном итоге хотите, я думаю, это иметь тип пользователя

Vector<T, N>

И в зависимости от N пользователь получит несколько разных вещей. Первый не выполнит этого, а второй - по цене дублирования кода.

Что вы можете сделать, это инвертировать наследство:

template<typename T, size_t N> struct VectorBase 
{
};

template<typename T> struct VectorBase<T, 2>
{
};

template<typename T> struct VectorBase<T, 3>
{
};

template<typename T, size_t N> struct Vector : VectorBase<T, N>
{
};

И реализовать несколько функций, которые зависят только от N, являющегося некоторым конкретным значением в соответствующем базовом классе. Вы можете добавить в них защищенный деструктор, чтобы пользователи не могли удалять экземпляры Vector через указатели на VectorBase (обычно они даже не должны иметь возможность именовать VectorBase: поместить эти базы в некоторое пространство имен реализации, например detail ).

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

Еще одна идея состоит в том, чтобы использовать только один класс, а затем enable_if (используя boost::enable_if), чтобы включить или отключить их для определенных значений N, или использовать такой преобразователь типа int, который намного * 1021 проще *

struct anyi { };
template<size_t N> struct i2t : anyi { };

template<typename T, size_t N> struct Vector
{
    // forward to the "real" function
    void some_special_function() { some_special_function(i2t<N>()); }

private:
    // case for N == 2
    void some_special_function(i2t<2>) {
        ...
    }

    // case for N == 3
    void some_special_function(i2t<3>) {
        ...
    }

    // general case
    void some_special_function(anyi) {
        ...
    }
};

Таким образом, он полностью прозрачен для пользователя Vector. Это также не добавит дополнительных затрат пространства для компиляторов, выполняющих пустую оптимизацию базового класса (довольно часто).

4 голосов
/ 07 апреля 2009

Использование наследования и частного наследования. И не используйте никаких виртуальных функций. Так как с частным наследованием у вас нет is-a, никто не сможет использовать указатель baseclas на производный подкласс, и вы не получите проблемы среза при передаче значения по значению.

Это дает вам лучшее из обоих миров (и именно так большинство библиотек реализуют многие из классов STL).

С http://www.hackcraft.net/cpp/templateInheritance/ (обсуждается std :: vector, а не ваш V класс ector):

vector<T*> объявлен имеющим частная база vector<void*>. Все функции, которые помещают новый элемент в вектор, такой как push_back(), вызовите эквивалентная функция на этой частной базе, так внутренне наш vector<T*> использует vector<void*> для хранения. Все функции которые возвращают элемент из вектора, такой как front(), выполните static_cast на результат вызова эквивалентной функции на частной базе. Так как единственный способ получить указатель на vector<void*> (кроме намеренно опасные трюки) через интерфейс, предлагаемый vector<T*> это безопасно статически привести void* обратно к T* (или void*& обратно на T*& и т. д.).

В общем, если STL делает это так, это похоже на достойную модель для подражания.

0 голосов
/ 29 июля 2009

Наследование должно использоваться только для модели "is-a". Специализация была бы более чистой альтернативой. Если вам нужно или вы хотите использовать наследование по какой-либо причине, по крайней мере сделайте его частным или защищенным наследованием, чтобы вы не наследовали публично от класса с открытым не виртуальным деструктором.

Да, ребята с метапрограммированием шаблонов всегда делают

 struct something : something_else {};

Но эти something являются метафункциями и не предназначены для использования в качестве типов.

0 голосов
/ 07 апреля 2009

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

...