c++multithreadingbarrier

C++ std::barrier as class member


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;
}

Solution

  • Warning: this answer contains errors

    You can put a type-erasing CompletionFunction into std::barrier even though it lacks adapting constructors. It may require an extra bit of casting.

    You may have to use a move-only type erasing function due to the various requirements that std::barrier places on its CompletionFunction.

    Note that std::barrier does not support assigning - using smart-storage (unique ptr seems best) might fix any issues you have here. The semantics of the wrapping structure get complex regardless of which you use. Note that copying a barrier is semantically nonsense; moving a barrier could make sense.

    Original, over-complex answer:

    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).