C++ circular template dependency between Producer and Consumer in callback

Viewed 122

I am designing two classes which encapsulate a producer and consumer. One class does single-thread processing, the other (not shown here) two threads.

The producer and consumer are passed to each 'threading class' by template parameters.

*this is passed to the producer to enable a callback upon each message:

template<class PRODUCER, class CONSUMER>
class SingleThread
{
    SingleThread() : _prod(*this){}

    void processMessage(const char* message, int len)
    {
        int d = _cons.doSomething(message, len);
        // Code omitted
    }

    PRODUCER<SingleThread<>> _prod;  //  Cyclical dependency
    CONSUMER _cons;
};

Each producer receives the threading class as a callback via template parameter:

template<class THREADING_CALLBACK>
class Producer
{
    Producer(THREADING_CALLBACK& cb) : _cb(cb){}

    // A lot of code omitted

    void receiveMessage(const char* message, int len)
    {
        _cb.processMessage(message, len);
    }

    THREADING_CALLBACK& _cb;
};

but I have a problem with circular template dependency. SingleThread takes Producer as a template parameter but Producer requires SingleThread:

SingleThread<Producer<SingleThread<Producer<....>, Consumer>, Consumer>

How should I (re)structure this design?

3 Answers

You can omit the template arguments:

template<class THREADING_CALLBACK>
class Producer
{
public:
    Producer(THREADING_CALLBACK& cb) : _cb(cb){}

    void receiveMessage(const char* message, int len)
    {
        _cb.processMessage(message, len);
    }

    THREADING_CALLBACK& _cb;
};

template<template <class> class PRODUCER, class CONSUMER>
class SingleThread
{
public:
    SingleThread() : _prod(*this){}

    void processMessage(const char* message, int len)
    {
        // Code omitted
    }

    PRODUCER<SingleThread> _prod;
    CONSUMER _cons;
};

You probably dont have requirement that producer knows about callback more than it looks like functor.

using THREADING_CALLBACK = std::function<void(const char* message, int len)>;

This way, Producer does not need to take THREADING_CALLBACK as template parameter.

class Producer
{
  public:
  Producer(THREADING_CALLBACK& cb) : _cb(cb){}

  void receiveMessage(const char* message, int len)
  {
    _cb(message, len);
  }

  THREADING_CALLBACK _cb;
};

As a framing challenge, as written the producer has no business knowing the implementation of the threading callback.

The callback is just a sink for some data.

Second, make it a value-type rather than a reference; it can internally do reference semantics if it likes.

template<class DATA_SINK>
class Producer
{
public:
    Producer(DATA_SINK cb) : _cb(cb){}

    void receiveMessage(const char* message, int len)
    {
        _cb(message, len);
    }

    DATA_SINK _cb;
};

we now see that the interface of DATA_SINK is void(C-style string, length), aka std::string_view.

    void receiveMessage(std::string_view msg)
    {
        _cb(msg);
    }

now, the question is, how much overhead is there in an indirect call here, compared to your workloads?

class Producer
{
public:
    Producer(std::function<void(std::string_view)> cb) : _cb(cb){}

    void receiveMessage(const char* message, int len)
    {
        _cb(message, len);
    }

    std::function<void(std::string_view)> _cb;
};

we now have decoupled the Producer from what consumes its data. SingleThread, because it owns the producer, still gets the template argumetn.

template<class PRODUCER, class CONSUMER>
class SingleThread
{
    SingleThread() : _prod([this](std::string_view sv){
      processMessage(sv);
    }){}

    void processMessage(std::string_view sv)
    {
        int d = _cons.doSomething(sv);
        // Code omitted
    }

    PRODUCER _prod;
    CONSUMER _cons;
};

dependency is now removed. We can continue to tear apart dependencies in a similar way if we want.

You'll see above that I embedded the "pointer semantics" of the data-sink in the producer into the callback I installed;

    [this](std::string_view sv){
      processMessage(sv);
    }

here I store a pointer to the state of the callback, while Producer stores an actual value.

A side benefit to this strategy is that it produces a nice point where we can mock the API that Producer communicates with.

The cost of bouncing through a std::function is not zero, but it isn't high unless you are doing something like calling it for every pixel on a HD image at 60 FPS.

Related