1

I have a work distribution scheme where every unit does some book-keeping and management and pass the task to next one in order chain such as; Lets say have 3 classes: Boss, Manager, Worker

class Boss
{
    void do_async(Request req, std::function<void(Result)> callback)
    {
        //Find eligible manager etc.
        manager.do_async(Boss_request(req,...),std::bind(&Boss::callback,this,callback,std::placeholders::_1));

    }
    void callback(std::function<void(Result)> main_callback,Boss_result res)
    {
        //some book keeping
        main_callback(res.main_part);
    }
};

class Manager
{
    void do_async(Boss_request req, std::function<void(Boss_result)> boss_callback)
    {
        //Find eligible worker etc. add some data to request
        worker.do_async(Manager_request(req,...),std::bind(&Manager::callback,this,boss_callback,std::placeholders::_1));
    }

    void callback(std::function<void(Boss_result)> boss_callback,Manager_result res)
    {
        //some book keeping
        boss_callback(res.boss_part);
    }
};

class Worker
{
    void do_async(Manager_request req, std::function<void(Manager_result)> manager_callback)
    {
        //Do job async
        work_async(Worker_task(req,...),std::bind(&Worker::callback,this,manager_callback,std::placeholders::_1));
    }

    void callback(std::function<void(Manager_result)> manager_callback,Worker_result res)
    {
        //some book keeping
        manager_callback(res.manager_part);
    }
};

As you can see I am extensively using std::bind, std::function and std::placeholder. Does this approach has any advantages/disadvantages? if not preferable, so what is the better way to do it? Would using lambda functions be possible/preferable(as performance or code-quality) in this state?

Edit: Why do I need asynchronous access in every level, instead of only first level? Because between each classes there is a many-to-many relationship. I have couple of layers of processing units (Boss or Manager or Worker) which can order anyone in next layer. When a unit orders the job to next one in line. It must be free immediately to take new orders from above.

I haven't used directly lambda because callbacks can be a little bit large and may make it harder to read. But code-quality can be sacrificed if there is any significant performance penalty.

ifyalciner
  • 1,050
  • 1
  • 10
  • 20
  • You should have a look to promise.future scheme, that would probably prevent you frome writing all this boilerplate. – OznOg Oct 30 '17 at 09:38
  • there is no reason to use `bind`. Use lambda instead: https://stackoverflow.com/questions/17363003/why-use-stdbind-over-lambdas-in-c14 – bolov Oct 30 '17 at 11:45
  • You seem to be doing continuation passing style, but putting the continuation in an inconsistent spot. – Yakk - Adam Nevraumont Oct 30 '17 at 12:04

1 Answers1

3

What you are doing here is piping data around. Embrace the pipe.

namespace chain {
  template<class T, class Base=std::function<void(T)>>
  struct sink:Base{
    using Base::operator();
    using Base::Base;
  };

  template<class T, class F>
  sink<T> make_sink( F&& f ) {
    return {std::forward<F>(f)};
  }

  template<class T>
  using source=sink<sink<T>>;

  template<class T, class F>
  source<T> make_source( F&& f ) {
    return {std::forward<F>(f)};
  }
  template<class T>
  source<std::decay_t<T>> simple_source( T&& t ) {
    return [t=std::forward<T>(t)]( auto&& sink ) {
      return sink( t );
    };
  }
  template<class In, class Out>
  using pipe = std::function< void(source<In>, sink<Out>) >;

  template<class In, class Out>
  sink<In> operator|( pipe<In, Out> p, sink<Out> s ) {
    return [p,s]( In in ) {
      p( [&]( auto&& sink ){ sink(std::forward<In>(in)); }, s );
    };
  }
  template<class In, class Out>
  source<Out> operator|( source<In> s, pipe<Out> p ) {
    return [s,p]( auto&& sink ) {
      p( s, decltype(sink)(sink) );
    };
  }
  template<class T>
  std::function<void()> operator|( source<T> in, sink<T> out ) {
    return [in, out]{ in(out); };
  }
  template<class In, class Mid, class Out>
  pipe<In, Out> operator|( pipe<In, Mid> a, pipe<Mid, Out> b ) {
    return [a,b]( source<In> src, sink<Out> dest ) {
      b( src|a, dest  );
      // or a( src, b|dest );
      // but I find pipe|sink -> sink to be less pleasing an implementation
    };
  }
}//namespace

Then write these:

pipe<Request, Result> Boss::work_pipe();

pipe<Boss_request, Boss_result> Manager::work_pipe();
pipe<Boss_request, Manager_request> Manager::process_request();
pipe<Manager_request, Manager_result> Manager::do_request();
pipe<Manager_result, Boss_results> Manager::format_result();

pipe<Manager_request, Manager_result> Worker::work_pipe();

and similar for Worker and Boss.

pipe<Request, Result> Boss::work_pipe() {
  return process_request() | do_request() | format_result();
}
pipe<Boss_request, Boss_result> Manager::work_pipe() {
  return process_request() | do_request() | format_result();
}
pipe<Manager_request, Manager_result> Worker::work_pipe() {
  return process_request() | do_request() | format_result();
}

then:

pipe<Manager_request, Manager_result> Manager::do_request() {
  return [this]( source<Manager_request> src, sink<Manager_result> dest ) {
    // find worker
    worker.do_request( src, dest );
  };
}
pipe<Manager_output, Boss_result> Manager::format_result() {
  return [this]( source<Manager_output> src, sink<Boss_result> dest ) {
    src([&]( Manager_output from_worker ) {
      // some book keeping
      dest( from_worker.boss_part );
    });
  };
}

now, I made sources "sinks for sinks", because it permits a source (or a pipe) to generate 1, 0, or many messages from one invocation. I find this useful in many cases, but it does make writing pipes a bit stranger.

You can also write this in without using std::function at all, by simply applying "i am a sink" and "i am a source" and "i am a pipe" tags to lambdas (via composition, like override) then blindly hooking things up with | and hoping their type are compatible.

To do_sync, you just do this:

void Boss::do_async( Request req, sink<Result> r ) {
  work_async( simple_source(req) | work_pipe() | r );
}

ie, the entire computation can be bundled up and moved around. This moves the threading work to the top.

If you need the async thread implementation to be at the bottom, you can pipe up the earlier work and pass it down.

void Boss::do_async( source<Request> req, sink<Result> r ) {
  find_manager().do_async( req|process_request(), format_result()|r );
}
void Manager::do_async( source<Boss_request> req, sink<Boss_result> r ) {
  find_worker().do_async( req|process_request(), format_result()|r );
}
void Worker::do_async( source<Manager_request> req, sink<Manager_result> r ) {
  work_async( req|process_request()|do_request()|format_result()|r );
}

because of how the sink/source/pipes compose, you can choose what parts of the composition you pass down and which parts you pass up.

The std::function-less version:

namespace chain {
  struct pipe_tag{};
  struct sink_tag{};
  struct source_tag{};

  template<class T, class=void>
  struct is_source:std::is_base_of<source_tag, T>{};
  template<class T, class=void>
  struct is_sink:std::is_base_of<sink_tag, T>{};
  template<class T, class=void>
  struct is_pipe:std::is_base_of<pipe_tag, T>{};

  template<class F, class Tag>
  struct tagged_func_t: F, Tag {
    using F::operator();
    using F::F;
    tagged_func_t(F&& f):F(std::move(f)) {}
  };
  template<class R, class...Args, class Tag>
  struct tagged_func_t<R(*)(Args...), Tag>: Tag {
    using fptr = R(*)(Args...);
    fptr f;
    R operator()(Args...args)const{
      return f( std::forward<Args>(args)... );
    }
    tagged_func_t(fptr fin):f(fin) {}
  };

  template<class Tag, class F>
  tagged_func_t< std::decay_t<F>, Tag >
  tag_func( F&& f ) { return {std::forward<F>(f)}; }

  template<class F>
  auto as_pipe( F&& f ) { return tag_func<pipe_tag>(std::forward<F>(f)); }
  template<class F>
  auto as_sink( F&& f ) { return tag_func<sink_tag>(std::forward<F>(f)); }
  template<class F>
  auto as_source( F&& f ) { return tag_func<source_tag>(std::forward<F>(f)); }

  template<class T>
  auto simple_source( T&& t ) {
    return as_source([t=std::forward<T>(t)]( auto&& sink ) {
      return sink( t );
    });
  }

  template<class Pipe, class Sink,
    std::enable_if_t< is_pipe<Pipe>{} && is_sink<Sink>{}, bool> = true
  >
  auto operator|( Pipe p, Sink s ) {
    return as_sink([p,s]( auto&& in ) {
      p( [&]( auto&& sink ){ sink(decltype(in)(in)); }, s );
    });
  }
  template<class Source, class Pipe,
    std::enable_if_t< is_pipe<Pipe>{} && is_source<Source>{}, bool> = true
  >
  auto operator|( Source s, Pipe p ) {
    return as_source([s,p]( auto&& sink ) {
      p( s, decltype(sink)(sink) );
    });
  }
  template<class Source, class Sink,
    std::enable_if_t< is_sink<Sink>{} && is_source<Source>{}, bool> = true
  >
  auto operator|( Source in, Sink out ) {
    return [in, out]{ in(out); };
  }
  template<class PipeA, class PipeB,
    std::enable_if_t< is_pipe<PipeA>{} && is_pipe<PipeB>{}, bool> = true
  >
  auto operator|( PipeA a, PipeB b ) {
    return as_pipe([a,b]( auto&& src, auto&& dest ) {
      b( src|a, dest  );
      // or a( src, b|dest );
      // but I find pipe|sink -> sink to be less pleasing an implementation
    });
  }

  template<class T>
  using sink_t = tagged_func_t< std::function<void(T)>, sink_tag >;
  template<class T>
  using source_t = tagged_func_t< std::function<void(sink_t<T>)>, source_tag >;
  template<class In, class Out>
  using pipe_t = tagged_func_t< std::function<void(source_t<In>, sink_t<Out>)>, pipe_tag >;
}

which does fewer type checks, but gets rid of type erasure overhead.

The sink_t, source_t and pipe_t typedefs are useful when you need to type-erase them.

"Hello world" example using the non-type erasure version.

Yakk - Adam Nevraumont
  • 235,777
  • 25
  • 285
  • 465
  • 1
    @ifyalciner Thanks. I wrote it when annoyed by having to write layers of processing for string substitution and replacement and tried to figure out how to memory allocations while still permitting 1 to {0, infinity} replacements. The idea that a source is a sink of sink (instead of something returning a T) leads to some neat properties. I'm still messing around with how to get multiplexing working, and am uncertain if this model is appropriate for it. – Yakk - Adam Nevraumont Oct 30 '17 at 19:07