Reputation: 5791
Consider a class Logger
which has a member function write()
overloaded for standard C++ types, and also has some convenience function-templates like writeLine()
which internally call write()
:
class Logger {
public:
void write(int x) { ... }
void write(double x) { ... }
...
template <typename T>
void writeLine(T x) { write(x); ... }
...
};
Consider further a subclass FooLogger
which adds additional write()
overloads for domain-specifc types (let's call two of them FooType1
and FooType2
):
class FooLogger : public Logger {
public:
using Logger::write;
void write(FooType1 x) { ... }
void write(FooType2 x) { ... }
...
};
(self-contained example program at Ideone)
FooLogger::write()
, when called directly, now supports any argument for which either of the two classes provides an overload.
However, FooLogger::writeLine()
only supports the argument types for which class Logger
has a write()
overload... it does not see the additional write()
overloads declared in class FooLogger
.
I want it to see them though, so that it can be called with those argument types as well!
I got it to work using the Curiously Recurring Template Pattern (CRTP):
template <typename TDerivedClass>
class AbstractLogger {
...
template <typename T>
void writeLine(T x) { static_cast<TDerivedClass*>(this)->write(x); ... }
};
class Logger : AbstractLogger {}
class FooLogger : public AbstractLogger<FooLogger> {
...
};
(self-contained example program at Ideone)
While it does the job, it came at the cost of increased code complexity and vebosity:
static_cast
dance wherever appropriate when adding more code to the class in the future!)AbstractLogger
and Logger
into two classes..cpp
file) - even the ones that do not need to do the static_cast
thing.Considering the above, I'm seeking insight from people with C++ experience:
Upvotes: 4
Views: 521
Reputation: 5999
Why not use free functions, e.g., operator<<
, defined on your type and the logger's stream output type, or just functions that are called if visible? For an example of how to do this: googletest is written so that it all of the assertions can be customized this way by you providing serialization methods. See Teaching Googletest How To Print Your Values and then you can look in the implementation to see how they do it.
(Notice that googletest has too methods: you can provide a PrintTo()
method in your class or you can overload operator<<
, with PrintTo()
preferred if both are available. This has the advantage that you can serialize to logging differently than serializing to typical output streams (e.g., you already have an operator<<
for your class that doesn't do what you want for logs).
(The magic is all contained in gtest-printer.h - see class UniversalPrinter
at line 685 for the trigger.)
This also sas the advantage that it is very easy to add any class/struct/object to be logged properly without even going to the bother of extending the logging class. Furthermore ... what happens if someone extends the logger class (i.e., derives from it) to serialize class AAA
, and in a different piece of code there is a different derivation to serialize class BBB
and then finally you write some code where you'd like to log both AAA
s and BBB
s? The derived class approach doesn't work so well there ...
Upvotes: 2
Reputation: 217255
How about the other way:
template <typename ...Ts>
class Logger : private Ts...
{
public:
using Ts::write...;
void write(int x) { /*...*/ }
void write(double x) { /*...*/ }
// ...
template <typename T>
void writeLine(T x) { write(x); /*...*/ }
// ...
};
class FooWriter
{
public:
void write(FooType1 x) { /*...*/ }
void write(FooType2 x) { /*...*/ }
};
using FooLogger = Logger<FooWriter>;
And then use any of (or their aliases):
Logger<>
or Logger<FooWriter>
or Logger<FooWriter, BarWriter>
...
Upvotes: 5