Частичная типовая спецификация бесплатных функций - лучшие практики

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

template <class T, int N>
T mul(const T& x) { return x * N; }

template <class T>
T mul<T, 0>(const T& x) { return T(0); }

// error: function template partial specialization ‘mul<T, 0>’ is not allowed

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

template <class T, int N>
struct mul_impl
{
    static T fun(const T& x) { return x * N; }
};

template <class T>
struct mul_impl<T, 0>
{
    static T fun(const T& x) { return T(0); }
};

template <class T, int N>
T mul(const T& x)
{
    return mul_impl<T, N>::fun(x);
}

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


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

Ответ 1

Как говорит litb, ADL превосходит, где он может работать, что в основном, когда параметры шаблона могут быть выведены из параметров вызова:

#include <iostream>

namespace arithmetic {
    template <class T, class S>
    T mul(const T& x, const S& y) { return x * y; }
}

namespace ns {
    class Identity {};

    // this is how we write a special mul
    template <class T>
    T mul(const T& x, const Identity&) {
        std::cout << "ADL works!\n";
        return x;
    }

    // this is just for illustration, so that the default mul compiles
    int operator*(int x, const Identity&) {
        std::cout << "No ADL!\n";
        return x;
    }
}

int main() {
    using arithmetic::mul;
    std::cout << mul(3, ns::Identity()) << "\n";
    std::cout << arithmetic::mul(5, ns::Identity());
}

Вывод:

ADL works!
3
No ADL!
5

Перегрузка + ADL достигает того, чего вы бы достигли, частично специализировав шаблон функции arithmetic::mul для S = ns::Identity. Но он полагается на вызывающего абонента, чтобы вызвать его таким образом, который позволяет ADL, поэтому вы никогда не вызываете std::swap явно.

Итак, вопрос в том, что вы ожидаете от пользователей вашей библиотеки частичной специализации ваших шаблонов функций? Если они собираются специализировать их для типов (как это обычно бывает с шаблонами алгоритмов), используйте ADL. Если они собираются специализировать их для целочисленных параметров шаблона, как в вашем примере, то, я думаю, вам нужно делегировать класс. Но обычно я не ожидаю, что третья сторона определит, что делать умножение на 3 - моя библиотека будет делать все целые числа. Я мог бы разумно ожидать, что третья сторона определит, что будет делать умножение на octonion.

Подумайте об этом, может показаться, что возведение в степень могло бы стать лучшим примером для меня, так как мой arithmetic::mul схож схож с operator*, поэтому нет реальной специализации mul в моем примере. Затем я бы специализировал/ADL-перегрузку для первого параметра, так как "Идентичность в силе чего-либо - это Identity". Надеюсь, вы поняли эту идею.

Я думаю, что есть недостаток ADL - он эффективно выравнивает пространства имен. Если я хочу использовать ADL для "реализации" как arithmetic::sub, так и sandwich::sub для моего класса, тогда у меня могут быть проблемы. Я не знаю, что эксперты скажут об этом.

Под этим я подразумеваю:

namespace arithmetic {
    // subtraction, returns the difference of lhs and rhs
    template<typename T>
    const T sub(const T&lhs, const T&rhs) { return lhs - rhs; }
}

namespace sandwich {
    // sandwich factory, returns a baguette containing lhs and rhs
    template<typename SandwichFilling>
    const Baguette sub(const SandwichFilling&lhs, const SandwichFilling&rhs) { 
      // does something or other 
    }
}

Теперь у меня есть тип ns::HeapOfHam. Я хочу использовать ADL std:: swap для написания собственной реализации арифметики:: sub:

namespace ns {
    HeapOfHam sub(const HeapOfHam &lhs, const HeapOfHam &rhs) {
        assert(lhs.size >= rhs.size && "No such thing as negative ham!");
        return HeapOfHam(lhs.size - rhs.size);
    }
}

Я также хочу использовать ADL std:: swap для написания собственной реализации sandwich:: sub:

namespace ns {
    const sandwich::Baguette sub(const HeapOfHam &lhs, const HeapOfHam &rhs) {
        // create a baguette, and put *two* heaps of ham in it, more efficiently
        // than the default implementation could because of some special
        // property of heaps of ham.
    }
}

Подождите минуту. Я не могу этого сделать, не так ли? Две разные функции в разных пространствах имен с одинаковыми параметрами и разными типами возврата: обычно это не проблема, для каких пространств имен. Но я не могу ADL-ify их обоих. Возможно, я пропустил что-то действительно очевидное.

Btw, в этом случае я мог бы полностью специализировать каждый из arithmetic::sub и sandwich::sub. Абоненты будут using того или другого, и получат правильную функцию. Однако исходный вопрос говорит о частичной специализации, поэтому можем ли мы сделать вид, что специализация - это не вариант, без меня, фактически, что HeapOfHam является шаблоном класса?

Ответ 2

Если вы пишете библиотеку, которая будет использоваться в другом месте или другими людьми, создайте вещь struct/class. Это больше кода, но пользователи вашей библиотеки (возможно, будущее вас!) Будут благодарны вам. ЕСЛИ это один код использования, потеря частичной специализации не повредит вам.