Home > Enterprise >  Rules of injecting behavior to templated function using function overloading
Rules of injecting behavior to templated function using function overloading

Time:10-27

When writing templated libraries, sometimes it is desirable to implement behavior later than the definition of a function template. For example, I'm thinking of a log function in a logger library that is implemented as

template< typename T >
void log(const T& t) {
    std::cout << "[log] " << t << std::endl;
}

Later if I want to use log on my own type, I would implement std::ostream& operator<<(std::ostream&, CustomType) and hope that log function works on CustomType automatically.

However, I'm very unsure whether this pattern is conformant to the standard. To see how compilers treat it, I wrote the following minimal example.

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

where I hope to inject to doFoo function template by overloading the foo function. The results seem very inconsistent, because it works for custom (templated) types, but not for custom types in namespaces or built-in types. The results are the same for compilers MSVC (bundled with Visual Studio 16.10.1), as well as gcc 9.3.0.

I'm now very confused about what should be the correct behavior. I guess it has something to do with the location of instantiation. My questions are:

  1. Are the codes above legal? Or are they ill-formed?
  2. If the codes are legal, what causes the inconsistent behaviors for different overloads?
  3. 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.)

CodePudding user response:

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.)

You can combine functors with a default trait type to get a "best of both worlds". That's essentially how the unordered containers in the standard library use std::hash.

It allows injection either via specializing the trait or passing a functor explicitly.

#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;
}
  • Related