Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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:

  • Is CRTP the right tool for this job?
  • Is there another way to solve this?
like image 750
smls Avatar asked Mar 04 '23 18:03

smls


1 Answers

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>...

like image 96
Jarod42 Avatar answered Apr 26 '23 17:04

Jarod42