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:
- Are the codes above legal? Or are they ill-formed?
- If the codes are legal, what causes the inconsistent behaviors for different overloads?
- 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;
}