Частично специализированные структуры и шаблон перегруженной функции

Как известно, шаблоны функций не могут быть частично специализированы на С++. Когда вы концептуально пытаетесь достичь этого, есть два возможных решения, которые вы можете использовать. Одним из них является использование структур со статической функцией, необязательно обернутой с помощью функции шаблона, например:

template <class T, class U>
struct BarHelper
{
    static void BarHelp(T t, const U& u)
    {
        std::cerr << "bar general\n";
    }
};

template <class T>
struct BarHelper<T, double>
{
    static void BarHelp(T t, const double& u)
    {
        std::cerr << "bar specialized\n";
    }
};
template <class T, class U>
void bar(T t, const U& u)
{
    BarHelper<T, U>::BarHelp(t, u);
};

bar здесь необязательно, вы можете, если хотите, просто использовать элемент struct static непосредственно (хотя вам придется затем явно указать все аргументы).

Другой подход - просто перегрузить функциональные шаблоны:

template <class T, class U>
void func(T t, const U& u)
{
    std::cerr << "func general\n";

}
template <class T>
void func(T t, const double& u)
{
    std::cerr << "func specialized\n";
}

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

Есть ли причины предпочесть частично специализированные структуры? Насколько общие причины? То есть который должен быть вашим "дефолтом"? Означает ли это, если вы: а) планируете самостоятельно реализовать все специализации, а b) это используется как точка настройки, где пользователи могут вводить собственное поведение?

У Herb Sutter есть знаменитое сообщение в блоге об исключении специализации шаблона функций. В нем он также рекомендует (почти ближе к концу) предпочитать частично специализированные структуры для перегруженных шаблонов функций, но он, похоже, не дает никаких конкретных причин: http://www.gotw.ca/publications/mill17.htm.

Moral # 2: Если вы пишете шаблон базы функций, предпочитайте писать его как шаблон одной функции, который никогда не должен быть специализированным или перегруженным

(выделено курсивом).

Ответ 1

Сначала перечислим варианты создания нескольких вариантов одного и того же метода шаблонов:

Специализация функции шаблона: НЕ является опцией, поскольку функции шаблона не могут быть частично специализированными. (Смотрите здесь SO-темы на здесь, здесь и здесь).

  • Простая перегрузка: это может работать, , поскольку вопрос упоминает и демонстрирует.
    Однако это не всегда хорошо работает, как мы увидим ниже.

  • Использование частичной специализации класса функтора: это простая альтернатива для отсутствия специализированной функции шаблона.

  • Использование std::enable_if вместе с перегрузкой функций шаблона: этот подход можно выбрать, когда простая перегрузка шаблона не работает, см. ниже.

EDIT: добавление опции @Nir 4

  1. Использование функциональных параметров, основанных на шаблоне: Этот подход, предложенный Nir в комментариях и представленный ниже, позволяет перегружать функцию шаблона, но требует некоторого громоздкого синтаксиса на стороне вызывающего абонента, см. ниже.

--- КОНЕЦ ИЗМЕНЕНИЯ ---

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

Рассмотрим следующее:

template <typename T, T val1, T val2>
void isSame1() {
    cout << "val1: " << val1 << ", val2: " << val2 << " are "
         << (val1==val2?" ":"NOT ") << "the same" << endl;
}

Хотя val1 и val2 являются KNOWN при компиляции, нет возможности частичной специализации случая, когда мы KNOW во время компиляции, чтобы они были одинаковыми. Перегрузка функций в этом случае не помогает, перегрузка для случая невозможна для двух параметров шаблона непигового типа.

С помощью частичной специализации классов мы можем сделать:

template <typename T, T val1, T val2>
struct IsSameHelper {
    static void isSame() {
        cout << "val1: " << val1 << ", val2: " << val2 << " are NOT the same" << endl;
    }
};

// partial specialization
template <typename T, T val>
struct IsSameHelper<T, val, val> {
    static void isSame() {
        cout << "val1: " << val << ", val2: " << val << " are the same" << endl;
    }
};

template <typename T, T val1, T val2>
void isSame2() {
    IsSameHelper<T, val1, val2>::isSame();
}

Или, альтернативно, с std::enable_if мы можем сделать:

template<typename T, T val1, T val2>
struct is_same_value : std::false_type {};

template<typename T, T val>
struct is_same_value<T, val, val> : std::true_type {};

template <typename T, T val1, T val2>
typename std::enable_if<!is_same_value<T, val1, val2>::value, void>::type isSame3() { 
    cout << "val1: " << val1 << ", val2: " << val2 << " are NOT the same" << endl;
}

template <typename T, T val1, T val2>
typename std::enable_if<is_same_value<T, val1, val2>::value, void>::type isSame3() {
    cout << "val1: " << val1 << ", val2: " << val2 << " are the same" << endl;
}

Основной для всех вышеперечисленных вариантов будет выглядеть так:

int global1 = 3;
int global2 = 3;

//======================================================
// M A I N
//======================================================
int main() {
    isSame1<int, 3, 4>();
    isSame1<int, 3, 3>();
    isSame1<int*, &global1, &global1>();
    isSame1<int*, &global1, &global2>();

    isSame2<int, 3, 4>();
    isSame2<int, 3, 3>();
    isSame2<int*, &global1, &global1>();
    isSame2<int*, &global1, &global2>();

    isSame3<int, 3, 4>();
    isSame3<int, 3, 3>();
    isSame3<int*, &global1, &global1>();
    isSame3<int*, &global1, &global2>();
}

EDIT: добавление опции @Nir 4

template <class T, T v> struct foo{
    static constexpr T val = v;
};

// in a .cpp
template <class T, T v>
constexpr T foo<T, v>::val; // required for non-integral / non-enum types

template <class T, T v1, T v2> void isSame4(foo<T, v1> f1, foo<T, v2> f2) {
    cout << "val1: " << f1.val << ", val2: " << f2.val << " are NOT the same" << endl;
}

template <class T, T v> void isSame4(foo<T, v> f1, foo<T, v> f2) {
    cout << "val1: " << f1.val << ", val2: " << f2.val << " are the same" << endl;
}

Основной для этого параметра будет выглядеть:

int global1 = 3;
int global2 = 3;

//======================================================
// M A I N
//======================================================
int main() {
    isSame4(foo<int, 4>(), foo<int, 3>());
    isSame4(foo<int, 3>(), foo<int, 3>());
    isSame4(foo<int*, &global1>(), foo<int*, &global1>());
    isSame4(foo<int*, &global1>(), foo<int*, &global2>());
}

Я не вижу никакого преимущества в синтаксисе 4-го варианта. Но можно думать иначе...

Обратите внимание на необходимость .cpp файла в опции 4, для объявления T foo::val, во всех остальных вариантах все подходит для файлов .h.

--- КОНЕЦ ИЗМЕНЕНИЯ ---


Подводя итог:

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

См. код: http://coliru.stacked-crooked.com/a/65891b9a6d89e982