Is there a smart way to implement an observer pattern completely static (at compile time)

Viewed 696

EDIT:

  • I want to implement an subject - observer pattern such that I can call notify<SUBJECT>() from anywhere and many update<SUBJECT>() fuctions get called
  • A subject is a struct type. There can be several of these subjects
  • I want to be able to put an observer update function anywhere in the code (in different compilation units)
  • The notify function needs to known which update functions to call at compile-time
  • It would be good if the notify functions can be connected to the update functions automatically

I am trying to implement a kind of Observer pattern, the notification mechanism. I currently use a run-time implementation that allows one to attach itself to a vector. However, all "attach" calls are made during startup so it can also be done (and I want in to be done) at compile-time.

  • My first idea was without the int template parameter which leads to redefinition error from the linker. It would be nice if you can tell the linker to keep all of them and call all of them, but I'm pretty sure that this cannot be done.
  • Now I use weak linkage and an int template parameter to make the methods unique. However, that is not as comfortable as I want it. Having to remember all the numbers across the whole codebase. At least, I get hard errors when I use the number twice.
  • I'm not so deep into template metaprogramming, but I think it gets complicated when it comes to different compilation units (but I can also switch to a single compilation unit)
  • I'd like to avoid boost but C++17 is perfectly fine.

This is the common header:

struct Test {
    int value;
};

namespace Observer
{
    template<typename T, int N>
    void update(decltype(T::value));

    /*
     * This ìs a function dummy which can be overloaded with the
     * real implementation. The linker overrides them and kicks out
     * the unoverridden functions since they do nothing.
     */
    template<typename T, int N>
    void __attribute__((weak)) update(decltype(T::value)) {}
}

template<typename T>
void notify(decltype(T::value) value)
{
    Observer::update<T, 1>(value, value != old);
    Observer::update<T, 2>(value, value != old);
    Observer::update<T, 3>(value, value != old);
}

Let's say we have two compile units:

template<>
void Observer::update<Test, 1>(decltype(Test::value) value)
{
    std::cout << "update: " << value << std::endl;
}
template<>
void Observer::update<Test, 2>(decltype(Test::value) value)
{
    std::cout << "update: " << value << std::endl;
}

I need to get rid of the numeric iteration in different compile units but can still have multiple implementations of the update function of the same type. All "connected" at compile-time to be called at run-time.

2 Answers

I'm not sure I understand your question completely, but if your restriction on observers having the same type is not a hard restriction, you can do some meta programming magic to iterate a list of types and call a static update() on each of them, without the manual unrolling.

Here's an example:

#include <iostream>
#include <tuple>
#include <utility>

// A list of types
template<typename... Ts> class observers{
    template<std::size_t I, typename...>
    struct at_impl{};

    template<std::size_t I, typename T, typename... Others>
    struct at_impl<I, T, Others...> {
        using type = typename at_impl<I-1, Others...>::type;
    };

    template<typename T, typename... Others>
    struct at_impl<0, T, Others...> {
        using type = T;
    };

public:

    // A way of getting the nth-type listed
    template<std::size_t I>
    using at = typename at_impl<I, Ts...>::type;

    constexpr inline static std::size_t size = sizeof...(Ts);
};

// Our notification function will iterate a list of observers, and call update() on each one.

template<typename ObserverList, std::size_t I = 0>
std::enable_if_t< ObserverList::size <= I, void>
notify_impl(int v) {}

template<typename ObserverList, std::size_t I = 0>
std::enable_if_t< I < ObserverList::size, void> 
notify_impl(int v) {
    using T = typename ObserverList::template at<I>;
    T::update(v);
    notify_impl<ObserverList, I+1>(v);
}

template<typename ObserverList>
void notify(int v) {
    notify_impl<ObserverList, 0>(v);
}

// Let's define some observers...

struct FirstObserver {
    static void update(int v){std::cout << "first observer notified with value = " << v << '\n';}
};

struct SecondObserver {
    static void update(int v){std::cout << "second observer notified with value = " << v << '\n';}
};

struct ThirdObserver {
    static void update(int v){std::cout << "third observer notified with value = " << v << '\n';}
};

// ... and put those observers on a list.
using ObserverList = observers<FirstObserver, SecondObserver, ThirdObserver>;

int main() {
    notify<ObserverList>(0);
}

Output:

first observer notified with value = 0
second observer notified with value = 0
third observer notified with value = 0

See it live!


However, if you just want to call Observer::update iteratively, you can just implement notify as follows:

template<typename T, std::size_t I, std::size_t MAX>
std::enable_if_t< MAX <= I, void>
notify_impl(decltype(T::value)) {}

template<typename T, std::size_t I, std::size_t MAX>
std::enable_if_t< I < MAX, void> 
notify_impl(decltype(T::value) v) {
    Observer::update<T, I>(v);
    notify_impl<T, I+1, MAX>(v);
}

template<typename T>
void notify(decltype(T::value) v) {
    // Assuming we have 3 observers
    notify_impl<T, 0, 3>(v);
}

If you are worrying about what happens on different translation units, you just need to remember that templates are code generators: Ultimately, the code generated will be the same as if you did the unrolled loop by hand, and, if that works, this should work as well.

Live example

Maybe something like this:

#include <iostream>
#include <array>
#include <functional>

template<typename Container, typename T>
void notify( Container& observers, T value )
{
   for ( auto& observer : observers )
      observer( value );
}

// updateX can be defined in another unit but exposed in a header
void update1( int i )
{
   std::cout << "observer 1:" << i << std::endl;
}

void update2( int i )
{
   std::cout << "observer 2:" << i << std::endl;
}

const std::array<std::function<void(int)>, 2> observers {
   [](int i) { update1( i ); },
   [](int i) { update2( i ); }
};

int main()
{
   notify( observers, 3 );
   return 0;
}

The list of calls to the obsevers might be unrolled and inlined by the optimizer.

Related