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
decltype
trick 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::string
and 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).
CodePudding user response:
You can think of both methods as "operator<<
on all types with some property".
The first property is "has a toString()
" method (and will work in C 11 even. This is still SFINAE, in this case the substitutions are in the return type). You can make it check that toString()
returns a std::string
with a different style of SFINAE:
template <typename T, std::enable_if_t<
std::is_same_v<std::decay_t<decltype(std::declval<const T&>().toString())>, std::string>,
int> = 0>
std::ostream& operator<<(std::ostream& out, const T& obj)
{
out << obj.toString();
return out;
}
And a non-template operator<<
will always be chosen before this template. A more "specialized" template will also be chosen before this one. The rules for overload resolution are a bit complex, but they can be found here: https://en.cppreference.com/w/cpp/language/overload_resolution#Best_viable_function
The second property is "derives from WithToString
". As you guessed, this one is more "explicit", and it is harder to accidentally/unexpectedly use the operator<<
.
You can actually define the operator inline, with a friend function:
struct A {
std::string toString() const {
return "A";
}
friend std::ostream& operator<<(std::ostream& os, const A& a) {
return os << a.toString();
}
};
And you could also have this friend declaration in WithToString
, making it a self-documenting mixin
template<typename T> // (crtp class)
struct OutputFromToStringMixin {
friend std::ostream& operator<<(std::ostream& os, const T& obj) {
return os << obj.toString();
}
};
struct A : OutputFromToStringMixin<A> {
std::string toString() const {
return "A";
}
};