[C++] Breaking Dependencies With Templates

When it comes to design patterns, interfaces and separating dependencies in a codebase, the first result is often a virtual interface/base class and a concrete implementation:

A simple example: We want to send a text notification to a GUI, to display it to a user

And this would be the classic example of an adapter. In the concrete implementation we have the plattform specific code.

A lot of design pattern resources, where you'd find coding examples you'd find something like this:

struct notification_interface 
{
    virtual ~notification_interface() = default;
    virtual void notify_user(const std::string& msg) = 0;
};
struct concrete_notification : public notification_interface
{
    void notify_user(const std::string& msg) override
    {
        // ... 
    }
};

struct worker
{
    // ... 
    void do_some_work()
    {
        // ...
        m_notification->notify_user("some results ... ");
    }
    std::unique_ptr<notification_interface> m_notification;
};

And now, imagine we'd have any engine which uses notification_interface, you can use dynamic polymorphism to get the desired implementation.

Now there are some things to consider:

  1. Most likely we don't need a dynamic interface here. notify_user would have a plattform specific implementation dependeing on where we are running.
  2. We can't use a stack allocation
  3. We probably need a factory method to create concrete
  4. We literally created now our own dependency, because on every target plattform we have to use a concrete implementation (we also could use a default implementation in notification_interface but this can create some situations where it isn't really clear which implementation we use)
  5. If we have unittests we always have to provide a dummy implementation or a mock to compile our tests.
 

Lets go ahead and solve the problems.
First of all we make worker templated, with a parame

template<typename Notification>
struct worker
{
    worker(Notification notification) : m_notification(std::move(notification)){}
    // ... 
    void do_some_work()
    {
        // ...
        m_notification("some results ... ");
    }
    Notification m_notification;
};

And now we can pass anything callable which accepts a string to the constructor. Here are three options:

// we can define a free function 
void notify_me(const std::string& s)
{
    std::cout << s << '\n';
}

// we can create a type with overloads the () operator
struct notifier
{
    void operator()(const std::string& s) const
    {
        std::cout << s << '\n';
    }
};

int main()
{
    // or we can define a lambda
    worker w1([](const std::string& s){ 
        std::cout << s << '\n';
    });
    w1.do_some_work();

    // here we pass the free function to worker
    worker w2(notify_me);
    w2.do_some_work();

    // or we pass our notifier type to worker
    worker w3(notifier{});
    w3.do_some_work();
}
And I really like this idea, because if we want to write unittests for worker, where we need to get rid of any platform specific implementation, we can just use a lambda. Compared to have a default implementation or a specific test interface if we'd go the classic way...
// I'm used to use googletest ... 
TEST(any, test) 
{
    worker w1([](const std::string& s){ 
        // you can also check the arguments in the call here ... 
        std::cout << s << '\n';
    });
    w1.do_some_work();
    // some test implementation and assertion ... 
}

If this would be available for any clients, like we develop a library which are used by other programmers, the client can always check with the override keyword that the correct functions are implemented. But we can also use some template meta programming to have checks at compile time, that the correct signatures are used:

// our primary template for the check 
template<typename, typename T>
struct is_notifiable {
    static_assert(
        std::integral_constant<T, false>::value);
};

// the specialization which does the actual check 
template<typename C, typename Ret, typename... Args>
struct is_notifiable<C, Ret(Args...)> {
private:
    template<typename T>
    static constexpr auto check(T*) -> typename std::is_same<
        decltype( std::declval<T>()( std::declval<Args>()... ) ),
        Ret    
    >::type;  

    template<typename>
    static constexpr std::false_type check(...);

    typedef decltype(check<C>(0)) type;

public:
    static constexpr bool value = type::value;
};


template<typename Notification>
struct worker
{
    // and now we do a static_assert to check the function 
    // the signature void(const std::string&)
    static_assert(is_notifiable<Notification, void(const std::string&)>::value);
    // as before ... 
};

int main()
{
    // ...

    // without const this would be invalid now:
    // error: static assertion failed due to requirement 'is_notifiable ... 
    worker w4([](std::string& s){ 
        std::cout << s << '\n';
    });
    w4.do_some_work();
}

As another option we can use C++20's concepts here. Then we'd make the template meta programming easier:

// first we define the concept, which isn't so much code as before
template <typename Func>
concept C_Notification = requires(Func func) {
  { func(std::declval<const std::string&>()) } -> std::same_as<void>;
};

// and then we apply the concept for instance here 
template<C_Notification Notification>
void notification_concept(Notification n)
{
    n("something else here ...");
}

int main()
{
    notification_concept(notify_me); // OK 
    // notification_concept([](int i){}); // not ok 
}

Note: The concept only checks if this is valid code, so a signature with a non-const std::string passed by value would be valid too. I haven't used the concepts in depth yet, so if anyone knows how to check the types/arguments explicitly, you can leave a comment below.

 

Conclusion

And now you can document your interface and can make sure clients implement the interface correctly. The compilation will always fail if the interface is incomplete or wrong.

So as I started to use templates and some meta programming it seemed a bit complicated to me, but once you get used to it, you really see that it makes your life easier. We can put our code fast into test, don't need to implement any fakes or mocks. We just pass in a lambda with the desired implementation in the test. We can also add checks into the lambda which adds more flexibility to it.

There is C++ Software Design by Klaus Iglberger which is one of the best C++ books which I have read. He also had conference talks on this topic, so just look this up on Youtube where you can get more insides.

The example which I did in this article is here on compiler explorer.

I hope that helped and thats it for now.

Best Thomas

Previous
Previous

[C++] A find-Function to Append .and_then(..).or_else(..) in C++

Next
Next

[C++] A std::tuple To Concatenate Validation Criteria