使用函数重载将行为注入模板函数的规则

Rules of injecting behavior to templated function using function overloading

在编写模板化库时,有时希望在函数模板定义之后实现行为。例如,我正在考虑实现为

的记录器库中的 log 函数
template< typename T >
void log(const T& t) {
    std::cout << "[log] " << t << std::endl;
}

以后如果我想在我自己的类型上使用 log,我会实现 std::ostream& operator<<(std::ostream&, CustomType) 并希望 log 函数自动在 CustomType 上运行。

但是,我很不确定这个模式是否符合标准。为了了解编译器如何处理它,我编写了以下最小示例。

#include<iostream>

// In some library...
void foo(double)         { std::cout << "double" << std::endl; }
template< typename T>
void doFoo(T x) {
    foo(x);
}

// In some codes using the library...
struct MyClass {};
template< typename T > struct MyClassT {};
namespace my { struct Class {}; }

void foo(MyClass)        { std::cout << "MyClass" << std::endl; }
void foo(MyClassT<int>)  { std::cout << "MyClassT<int>" << std::endl; }
void foo(my::Class)      { std::cout << "my::Class" << std::endl; }
void foo(int)            { std::cout << "int" << std::endl; }


int main() {

    doFoo(1.0);               // okay, prints "double".
    doFoo(MyClass{});         // okay, prints "MyClass".
    doFoo(MyClassT<int>{});   // okay, prints "MyClassT<int>".
    doFoo(42);                // not okay, prints "double". int seems to have been converted to double.
    // doFoo(my::Class{});    // compile error, cannot convert my::Class to int.

    return 0;
}

我希望通过重载 foo 函数注入到 doFoo 函数模板。结果看起来很不一致,因为它适用于自定义(模板化)类型,但不适用于命名空间或内置类型中的自定义类型。对于编译器 MSVC(与 Visual Studio 16.10.1 捆绑)以及 gcc 9.3.0.

,结果相同

我现在很困惑什么才是正确的行为。我猜这与实例化的位置有关。我的问题是:

  1. 以上代码合法吗?或者它们格式不正确?
  2. 如果代码是合法的,是什么导致了不同重载的不一致行为?
  3. 如果代码是非法的,什么是注入库模板的好替代方法? (我正在考虑将 functions/functors 显式传递给我的 doFoo 函数,就像 <algorithm> 所做的那样。)

If the codes are illegal, what would be a good alternative to injecting library templates? (I'm thinking of passing functions/functors explicitly to my doFoo function, like what <algorithm> is doing.)

您可以将仿函数与默认特征类型结合使用以获得“两全其美”。这基本上就是标准库中无序容器使用 std::hash.

的方式

它允许通过专门化特征或显式传递仿函数来进行注入。

#include<iostream>

// In some library...
template <typename T>
struct lib_trait;

template<>
struct lib_trait<double> {
    void operator()(double) const         { std::cout << "double" << std::endl; }
};

template<typename T, typename CbT=lib_trait<T>>
void doFoo(T x, const CbT& cb={}) {
    cb(x);
}

// In some codes using the library...
struct MyClass {};
template< typename T > struct MyClassT {};
namespace my { struct Class {}; }

template<>
struct lib_trait<MyClass> {
    void operator()(MyClass) const        { std::cout << "MyClass" << std::endl; }
};

template<>
struct lib_trait<MyClassT<int>> {
    void operator()(MyClassT<int>) const  { std::cout << "MyClassT<int>" << std::endl; }
};

template<>
struct lib_trait<int> {
    void operator()(int) const            { std::cout << "int" << std::endl; }
};

int main() {

    // Leverage default argument to get the same syntax.
    doFoo(1.0);               // okay, prints "double".

    // Handled by specializations defined later.
    doFoo(MyClass{});         // okay, prints "MyClass".
    doFoo(MyClassT<int>{});   // okay, prints "MyClassT<int>".
    doFoo(42);                // okay, prints "int".

    // Pass in an explicit functor.
    doFoo(my::Class{}, [](const auto&){});

    return 0;
}