I'm using Catch2 with TEST_CASE blocks from within I sometimes declare local temporary struct for convenience. These struct sometimes needs to be displayed, and to do so Catch2 suggests to implement the << operator with std::ostream. Unfortunately, this becomes quite complicated to implement with local-only struct because such operator can't be defined inline nor in the TEST_CASE block.
I thought of a possible solution which would be to define a template for << which would call toString() instead if that method exists:
#include <iostream>
#include <string>
template <typename T>
auto operator<<(std::ostream& out, const T& obj) -> decltype(obj.toString(), void(), out)
{
out << obj.toString();
return out;
}
struct A {
std::string toString() const {
return "A";
}
};
int main() {
std::cout << A() << std::endl;
return 0;
}
I have a few questions:
- Is the
decltypetrick modern C++ or can we achieve the same using<type_traits>instead? - Is there a way to require for the
toString()returned value to be astd::stringand thus disable the template substitution otherwise? - Is it guaranteed that a class with a concrete implementation of
operator<<will be prioritized over the template if it exists?
Also, I find this solution to be quite fragile (I get errors when compiling my overall project although this simple snippet works), and I think it can lead to errors because of its implicit nature. Unrelated classes may define toString() method without expecting it to be used in << template substitution.
I thought it might be cleaner to do this explicitly using a base class and then SFINAE:
#include <iostream>
#include <string>
#include <type_traits>
struct WithToString {};
template <typename T, typename = std::enable_if_t<std::is_base_of_v<WithToString, T>>>
std::ostream& operator<<(std::ostream& out, const T& obj)
{
out << obj.toString();
return out;
}
struct A : public WithToString {
std::string toString() const {
return "A";
}
};
int main() {
std::cout << A() << std::endl;
return 0;
}
The downside of this solution is that I can't define toString() as a virtual method in the base class otherwise it prevents aggregate initialization (which is super-useful for my test cases). Consequently, WithToString is just an empty struct which serves as a "marker" for std::enable_if. It does not bring any useful information by itself, and it requires documentation to be properly understood and used.
What are your thoughts on this second solution? Can this be improved somehow?
I'm targeting C++17 so I can't use <concepts> yet unfortunately. Also I would like to avoid using the <experimental> header (although I know it contains useful stuff for C++17).