4

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?
smls
  • 5,738
  • 24
  • 29

2 Answers2

5

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

Jarod42
  • 203,559
  • 14
  • 181
  • 302
2

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

user3738870
  • 1,415
  • 2
  • 12
  • 24
davidbak
  • 5,775
  • 3
  • 34
  • 50
  • Note that free function as `PrintTo` can also lead to subtle bugs, with ADL and ODR. See [googletest-printto-not-getting-called-for-a-class](https://stackoverflow.com/questions/24673515/googletest-printto-not-getting-called-for-a-class). Each way has its pros and cons. – Jarod42 Jan 09 '19 at 23:05
  • @Jarod42 - well it turns out, after I refreshed myself on the documentation - that for GoogleTest `PrintTo` isn't a free function, it's a member function. But yes, there could be problems as a free function. I myself would make it a free function _in a specific namespace_ since there's no problem extending any namespace (except `std`) whenever you want. Would you see a problem with that? – davidbak Jan 09 '19 at 23:09
  • @Jarod42 - also in your linked answer (nice one BTW) I think that code he was having difficulty with wasn't due to PrintTo being free - it was with violating ODR - which could happen regardless. As we're on the subject, BTW, does any IDE diagnose that? (As the IDE typically has a full-program view or at least wide-program-view even if the compiler does not.) – davidbak Jan 09 '19 at 23:13
  • My point is that ODR violation is easy to have with free functions for multiple overload as we have to guaranty that the same overloads are seen in different point of instantiation. Detecting that is hard: linker might compare the "same" function in different obj to see if they are really identical, but with inlined code, it seems it is too late... – Jarod42 Jan 09 '19 at 23:19
  • @Jarod42 - and I guess my point would be the OP asked for alternatives in addition to getting his approach to work, and I think this alternative has advantages - such as, being lighter weight (IMO). Also, I rarely find ODR problems in practice (only once or twice in someone else's code, ever, though it certainly was confusing) but YMMV! – davidbak Jan 09 '19 at 23:21