smls
smls

Reputation: 5791

Is the Curiously Recurring Template Pattern (CRTP) the right solution here?

Scenario

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)

Problem

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!

Current solution

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:

  1. It made the implementation of the base class significantly harder to read (see the Ideone link), and harder to maintain (mustn't forget to do the static_cast dance wherever appropriate when adding more code to the class in the future!)
  2. It required separating AbstractLogger and Logger into two classes.
  3. Because the base-class is now a class template, the implementations of all its member functions must now be included in the header (rather than the .cpp file) - even the ones that do not need to do the static_cast thing.

Question

Considering the above, I'm seeking insight from people with C++ experience:

Upvotes: 4

Views: 521

Answers (2)

davidbak
davidbak

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 AAAs and BBBs? The derived class approach doesn't work so well there ...

Upvotes: 2

Jarod42
Jarod42

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

Related Questions