Home > Software engineering >  C std::barrier as class member
C std::barrier as class member

Time:09-28

How do you store an std::barrier as a class member,

Because the completion function can be a lambda, you can't type it properly, and using an std::function< void(void) noexcept > won't work either as std::function does not seem to support the noexcept keyword.

So it seems there is no generic base type for std::barrier completion functions

Small example

#include <barrier>

struct Storage
{
    std::barrier< ?? > barrier;
}

CodePudding user response:

std::barrier does not support polymorphic CompletionFunction types/values -- it doesn't have the adapting constructors found in some other types. So using std::function in there is a non-starter, regardless of its support -- two std::barriers with different CompletionFunction types are unrelated, and cannot be assigned to each other.

You could type erase it yourself, and write a poly_barrier. The barrier (pun intended) to doing this is that arrival_token doesn't seem to support polymorphism; there may be no guarantee it is the same type in different std::barrier<X> cases.

It is MoveConstructible, MoveAssignable and Destructible however, so we could type erase it as well.

First API sketch:

struct poly_barrier;
struct poly_barrier_vtable;
struct poly_arrival_token:private std::unique_ptr<void> {
  friend struct poly_barrier_vtable;
  poly_arrival_token(poly_arrival_token&&)=default;
  private:
    explicit poly_arrival_token(std::unique_ptr<void> ptr):
      poly_arrival_token(std::move(ptr))
    {}
};
struct poly_barrier_vtable;

struct poly_barrier {
  template<class CF>
  poly_barrier( std::ptrdiff_t expected, CF cf );
  ~poly_barrier();
  poly_barrier& operator=(poly_barrier const&)=delete;
  poly_arrival_token arrive( std::ptrdiff_t n = 1 );
  void wait( poly_arrival_token&& ) const;
  void arrive_and_wait();
  void arrive_and_drop();
private:
  poly_barrier_vtable const* vtable = nullptr;
  std::unique_ptr<void> state;
};

we now write up a vtable:

struct poly_barrier_vtable {
  void(*dtor)(void*) = 0;
  poly_arrival_token(*arrive)(void*, std::ptrdiff_t) = 0;
  void(*wait)(void const*, poly_arrival_token&& ) = 0;
  void(*arrive_and_wait)(void*) = 0;
  void(*arrive_and_drop)(void*) = 0;
private:
  template<class CF>
  static poly_arrival_token make_token( std::barrier<CF>::arrival_token token ) {
    return poly_arrival_token(std::make_unique<decltype(token)>(std::move(token)));
  }
  template<class CF>
  static std::barrier<CF>::arrival_token extract_token( poly_arrival_token token ) {
    return std::move(*static_cast<std::barrier<CF>::arrival_token*>(token.get()));
  }
protected:
  template<class CF>
  poly_barrier_vtable create() {
    using barrier = std::barrier<CF>;
    return {
       [](void* pb){
         return static_cast<barrier*>(pb)->~barrier();
      },
       [](void* pb, std::ptrdiff_t n)->poly_arrival_token{
        return make_token<CF>(static_cast<barrier*>(pb)->arrive(n));
      },
       [](void const* bp, poly_arrival_token&& token)->void{
        return static_cast<barrier const*>(pb)->wait( extract_token<CF>(std::move(token)) );
      },
       [](void* pb)->void{
        return static_cast<barrier*>(pb)->arrive_and_wait();
      },
       [](void* pb)->void{
        return static_cast<barrier*>(pb)->arrive_and_drop();
      }
    };
  }
public:
  template<class CF>
  poly_barrier_vtable const* get() {
    static auto const table = create<CF>();
    return &table;
  }
};

which we then use:

struct poly_barrier {
  template<class CF>
  poly_barrier( std::ptrdiff_t expected, CF cf ):
    vtable(poly_barrier_vtable<CF>::get()),
    state(std::make_unique<std::barrier<CF>>( expected, std::move(cf) ))
  {}
  ~poly_barrier() {
    if (vtable) vtable->dtor(state.get());
  }
  poly_barrier& operator=(poly_barrier const&)=delete;
  poly_arrival_token arrive( std::ptrdiff_t n = 1 ) {
    return vtable->arrive( state.get(), n );
  }
  void wait( poly_arrival_token&& token ) const {
    return vtable->wait( state.get(), std::move(token) );
  }
  void arrive_and_wait() {
    return vtable->arrive_and_wait(state.get());
  }
  void arrive_and_drop() {
    return vtable->arrive_and_drop(state.get());
  }
private:
  poly_barrier_vtable const* vtable = nullptr;
  std::unique_ptr<void> state;
};

and bob is your uncle.

There are probably typos above. It also doesn't support moving arbitrary barriers into it; adding a ctor should make that easy.

All calls to arrive involve a memory allocation, and all calls to wait deallocation due to not knowing what an arrival_token is; in theory, we could create a on-stack type erased token with a limited size, which I might do if I was using this myself.

arrive_and_wait and arrive_and_drop do not use heap allocation. You can drop arrive and wait if you don't need them.

Everything bounces through a manual vtable, so there are going to be some performance hits. You'll have to check if they are good enough.

I know this technique as C value-style type erasure, where we manually implement polymorphism in a C-esque style, but we automate the type erasure generation code using C templates. It will become far less ugly when we have compile time reflection in the language (knock on wood), and is the kind of thing you might do to implement std::function.

The code blows up in fun ways if you pass arrival tokens from one barrier to another. But so does std::barrier, so that seems fair.


A completely different approach, with amusingly similar implementation, would be to write a nothrow-on-call std::function.

Implementing std::function efficiently usually involves something similar to the vtable approach above, together with a small buffer optimization (SBO) to avoid memory allocation with small function objects (which I alluded to wanting to do with poly_arrival_token).

  • Related