1

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.)
drel
  • 165
  • 3
  • 8
  • 2
    I can’t seem to find a proper duplicate, but this is dependent ADL, which is surely well discussed somewhere. – Davis Herring Oct 26 '21 at 21:24
  • 1
    If trying to figure out the behavior, I recommend not using Visual C++ for that purpose, since for backward compatibility with legacy code a lot of Visual C++ versions processed templates with single-phase lookup instead of the two-phase lookup required by the C++ standard. It didn't even try to support the correct behavior [until 2017 and even then it defaulted to the old behavior](https://devblogs.microsoft.com/cppblog/two-phase-name-lookup-support-comes-to-msvc/) – Ben Voigt Oct 26 '21 at 21:48
  • Required reading: https://stackoverflow.com/a/6273465/103167 – Ben Voigt Oct 26 '21 at 21:53

1 Answers1

1

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;
}
drel
  • 165
  • 3
  • 8