The way you demonstrated in the question is the most basic way, which is also found in various C++ books. Personally I may not prefer in my production code, mainly because:
- Have to write the boilerplate code for
friend operator<<
for each and every class.
- When adding new class members, you may have to update the methods as well individually.
I would recommend following way since C++14:
Library
// Add `is_iterable` trait as defined in https://stackoverflow.com/a/53967057/514235
template<typename Derived>
struct ostream
{
static std::function<std::ostream&(std::ostream&, const Derived&)> s_fOstream;
static auto& Output (std::ostream& os, const char value[]) { return os << value; }
static auto& Output (std::ostream& os, const std::string& value) { return os << value; }
template<typename T>
static
std::enable_if_t<is_iterable<T>::value, std::ostream&>
Output (std::ostream& os, const T& collection)
{
os << "{";
for(const auto& value : collection)
os << value << ", ";
return os << "}";
}
template<typename T>
static
std::enable_if_t<not is_iterable<T>::value, std::ostream&>
Output (std::ostream& os, const T& value) { return os << value; }
template<typename T, typename... Args>
static
void Attach (const T& separator, const char names[], const Args&... args)
{
static auto ExecuteOnlyOneTime = s_fOstream =
[&separator, names, args...] (std::ostream& os, const Derived& derived) -> std::ostream&
{
os << "(" << names << ") =" << separator << "(" << separator;
int unused[] = { (Output(os, (derived.*args)) << separator, 0) ... }; (void) unused;
return os << ")";
};
}
friend std::ostream& operator<< (std::ostream& os, const Derived& derived)
{
return s_fOstream(os, derived);
}
};
template<typename Derived>
std::function<std::ostream&(std::ostream&, const Derived&)> ostream<Derived>::s_fOstream;
Usage
Inherit the above class for those classes for whom you want the operator<<
facility. Automatically friend
will get included into those class's definition via base ostream
. So no extra work. e.g.
class MyClass : public ostream<MyClass> {...};
Preferably in their constructors, you may Attach()
the member variables which are to be printed. e.g.
// Use better displaying with `NAMED` macro
// Note that, content of `Attach()` will effectively execute only once per class
MyClass () { MyClass::Attach("\n----\n", &MyClass::x, &MyClass::y); }
Example
From what you shared,
#include"Util_ostream.hpp"
template<typename T>
class Example : public ostream<Example<T>> // .... change 1
{
public:
Example(const T &_first_ele, const T &_second_ele) : first_(_first_ele), second_(_second_ele)
{
Example::Attach(" ", &Example::first_, &Example::second_); // .... change 2
}
private:
T first_;
T second_;
};
This approach has a pointer access per every print of the variable instead of direct. This negligible indirection should never be a bottle-neck in a code from the performance perspective.
Demo is slight more complex for practical purpose.
Requirements
- The intention here is to improve readability and uniformity of printing the variables
- Every printable class should have their separate
ostream<T>
regardless of inheritance
- An object should have
operator<<
defined or ostream<T>
inherited to be able to compile
Facilities
This is now shaping up as a good library component. Below are add-on facilities, I have added so far.
- Using
ATTACH()
macro, we can also print variable in certain way; Variable printing can always be customised as per need by modifying the library code
- If the base class is printable, then we can simply pass a typecasted
this
; Rest will be taken care
- Containers with
std::begin/end
compatibility are now supported, which includes vector
as well as map
The code shown in the beginning is shorter for the quick understanding purpose. Those who are further interested may click on the demo link above.