Lecture 8: Curiously Recurring Template Pattern (CRTP), Mixins, Dependency Injection/Inversion, and the Pimpl Idiom

Date: 2023-09-12

1. Curiously Recurring Template Pattern (CRTP)

What is CRTP?

CRTP is a C++ idiom that uses inheritance and templates to enable static (compile-time) polymorphism. It's "curiously recurring" because the derived class appears as a template parameter in the base class.

Basic Syntax

template <typename Derived>
class Base {
public:
    void interface() {
        static_cast<Derived*>(this)->implementation();
    }
};

class Derived : public Base<Derived> {
public:
    void implementation() {
        // Actual implementation here
    }
};

How it Works

  1. Static Polymorphism: Unlike dynamic polymorphism, which uses virtual functions, CRTP allows method calls to be resolved at compile-time.

  2. Code Reuse: CRTP helps in sharing common code across different classes without incurring the runtime cost of virtual function calls.

Pros and Cons

Pros

  1. Performance: Faster than dynamic polymorphism as it avoids virtual table lookups.
  2. Code Reuse: Allows for more straightforward code sharing between classes.

Cons

  1. Complexity: Introduces additional complexity in the codebase.
  2. Type Safety: Type errors are possible if you misuse the CRTP.

Use Cases

  1. Smart Pointers: Often used in the design of smart pointers like std::shared_ptr.
  2. Expression Templates: Useful in library design, especially for optimizing operations in mathematical libraries.

Examples

Basic CRTP

template <class Derived>
class Animal {
public:
    void makeSound() {
        static_cast<Derived*>(this)->sound();
    }
};

class Dog : public Animal<Dog> {
public:
    void sound() {
        // Woof Woof
    }
};

Here, Animal is the base class templated on Derived. The derived class Dog inherits from Animal parameterized with Dog itself.


2. Mixins

What are Mixins?

Mixins are classes designed to provide optional functionalities to other classes. Unlike traditional inheritance, where the derived class is a subtype of the base class, mixins offer a form of horizontal inheritance. The main goal is to distribute reusable pieces of code.

Basic Syntax

class Mixin1 {
public:
    void action1() {
        // Implementation of action1
    }
};

class Mixin2 {
public:
    void action2() {
        // Implementation of action2
    }
};

template <typename Base>
class Combined : public Base, public Mixin1, public Mixin2 {
    // Inherits both Base and Mixins
};

How it Works

  1. Composition: Mixins add functionality to classes through multiple inheritance.

  2. Flexibility: You can combine multiple mixins to create a class with the exact set of features you need.

Pros and Cons

Pros

  1. Code Reuse: Facilitates the sharing of functionality across unrelated class hierarchies.
  2. Flexibility: Easy to assemble complex classes from simpler, well-defined pieces.

Cons

  1. Complexity: Managing multiple inheritance can be complicated.
  2. Name Clashes: With many mixins, you may encounter member name collisions.

Use Cases

  1. Logging: A logging mixin could provide standardized logging operations to various classes.
  2. Observability: A mixin could add event publishing features to a class.

Examples

Logging Mixin

class LoggingMixin {
public:
    void log(const std::string& message) {
        // Log the message
    }
};

class MyClass : public LoggingMixin {
public:
    void doSomething() {
        log("Doing something");
        // Actual work here
    }
};

Here, MyClass inherits from LoggingMixin, gaining the ability to log messages via the log() method.


3. Dependency Injection/Inversion

What is Dependency Injection/Inversion?

Dependency Injection is a design pattern that decouples the client from the construction of its dependencies, making the system more modular and easier to test. Dependency Inversion, a related principle, is the concept of depending on abstractions (interfaces) rather than concrete implementations.

Basic Syntax

// Interface
class Database {
public:
    virtual void query() = 0;
};

// Concrete Implementation
class MySQLDatabase : public Database {
public:
    void query() override {
        // MySQL query implementation
    }
};

// Client Code
class Application {
private:
    Database* db;
public:
    Application(Database* db) : db(db) {}
    void runQuery() {
        db->query();
    }
};

How it Works

  1. Decoupling: Dependencies are injected into the client as parameters or setters, so the client doesn't need to construct them.

  2. Testability: Makes it easier to insert mock implementations for testing.

  3. Flexibility: Clients can easily switch between different implementations of a dependency.

Pros and Cons

Pros

  1. Modularity: Easier to change or add new features.
  2. Testability: Simplifies the testing process.
  3. Maintainability: Improves the structure and organization of code.

Cons

  1. Complexity: Adds an extra layer of abstraction, which can make the code harder to understand.
  2. Initialization Overhead: Dependencies must be created and injected, which may lead to a longer initialization phase.

Use Cases

  1. Configuration: For creating configurable systems where the user can swap components.
  2. Plugin Architecture: Useful for systems that need to support plugins or modular features.

Examples

Simple Dependency Injection

class FileWriter {
public:
    virtual void write(const std::string& message) = 0;
};

class DiskFileWriter : public FileWriter {
public:
    void write(const std::string& message) override {
        // Write to disk
    }
};

class Logger {
private:
    FileWriter* writer;
public:
    Logger(FileWriter* writer) : writer(writer) {}
    void log(const std::string& message) {
        writer->write(message);
    }
};

In this example, the Logger class is decoupled from the DiskFileWriter class, making it easy to test or change the underlying file writing mechanism.


4. Pimpl Idiom

What is the Pimpl Idiom?

The Pimpl (Pointer to Implementation) Idiom is a technique in C++ for hiding the internal details of a class. It's commonly used to reduce compilation times and to maintain binary compatibility between different versions of a library.

Basic Syntax

// header file
class MyClass {
public:
    MyClass();
    ~MyClass();
    void someMethod();
private:
    class Impl;
    Impl* pimpl;
};

// source file
class MyClass::Impl {
public:
    void someMethod() {
        // Implementation here
    }
};

MyClass::MyClass() : pimpl(new Impl) {}
MyClass::~MyClass() { delete pimpl; }
void MyClass::someMethod() { pimpl->someMethod(); }

How it Works

  1. Encapsulation: The actual implementation of the class is hidden in the source file, leaving just an interface in the header file.

  2. Reduced Compilation Time: Changes to the implementation don't require recompiling all code that includes the header.

Pros and Cons

Pros

  1. Faster Compilation: Less code in headers leads to quicker compilation times.
  2. Binary Compatibility: Allows you to change the implementation without affecting binary compatibility.

Cons

  1. Memory Overhead: Each instance of the class now requires dynamic memory allocation for its implementation.
  2. Indirection: The extra level of indirection can have a slight performance impact.

Use Cases

  1. Libraries: Ideal for library developers who want to maintain binary compatibility across versions.
  2. Large Projects: Useful in large codebases to minimize compilation times when classes change.

Examples

Basic Pimpl Idiom

// MyClass.h
class MyClass {
public:
    MyClass();
    ~MyClass();
    void doSomething();
private:
    class Impl;
    Impl* pImpl;
};

// MyClass.cpp
class MyClass::Impl {
public:
    void doSomething() {
        // Implementation
    }
};

MyClass::MyClass() : pImpl(new Impl) {}
MyClass::~MyClass() { delete pImpl; }
void MyClass::doSomething() { pImpl->doSomething(); }

In this example, the MyClass interface remains the same, but its implementation details are hidden, allowing for more freedom in changing the implementation.