joboccara/pipes

[discussion] Implementing `drop_last` for pipes

kunaltyagi opened this issue · 2 comments

If a pipe is allowed to store the values locally in a circular queue, it can accumulate the first N values in it, and then release the rest.

Assuming an interface like Boost.CircularBuffer for the circular queue

class drop_last
{
  circular_buffer<std::any> queue_;

public:
  template <typename... Values, typename TailPipeline>
  void onReceive(Values&&... values, TailPipeline&& tailPipeline)
  {
    using DataStore = std::tuple<Values...>;
    if(queue_.full()) {
      send(SOMEHOW_FORWARD_OR_MOVE(std::any_cast<DataStore>(queue_.front()), FWD(tailPipeline));
    }
    queue_.push_back(std::make_any<DataStore>(values...));
  }

  explicit drop_last(std::size_t nbToDrop) queue_(nbToDrop) {}
};

Can it be made more efficient?

  • If the size is 1, we can use std::exchange to efficiently move the values. If the size is constant, we could use a circular buffer on stack (eg: with a backend on std::array instead of std::vector)
  • If the circular buffer implementation supports a std::exchange like operation, queue_.front() and queue_.push_back could be made one single operation
  • If the drop_last was templated instead of onReceive, there would be no need of using std::any

Big issue: SOMEHOW_FORWARD_OR_MOVE

Thanks for the idea.
Doesn't this implementation assume that all the (tuples of) values coming successively to the pipe are of the same type?

Doesn't this implementation assume that all the (tuples of) values coming successively to the pipe are of the same type?

Yes, but I feel that this is a "reasonably" safe (and in case of input coming from containers, guaranteed) assumption (what's a few throws among friends)

Perhaps, this limitation can be made explicit by templating the class itself (and thus removing the need for std::any)

Or else, it'd need tuple + lambda shenanigans to do it:

// the template part
template <class T, class... V>
struct exchange_result {
  T old;
  std::tuple<V...> tuple;
};

template <class Value, class V0, class...V>
auto tuple_exchange(std::tuple<V0, V...>&& existing, Value&& in) {
  auto a = std::move(std::get<0>(existing));
  return exchange_result{a, 
  std::make_tuple<V..., Value>(std::move(std::get<V>(existing))..., in)};
}

I am not sure if what I wish to do with lambda is legal (or possible), but essentially, I wanted to use onReceive to store a lambda in a std::function. The lambda acts the storage for the tuple and the queue logic, while returning a new lambda for the onReceive function to store (and delete the original).

EDIT:
If we force the interface to have a constant type (or std::any) we can have really nice interface