C++ std::barrier as class member
Asked Answered
C

2

8

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;
}
Concordat answered 26/9, 2022 at 12:40 Comment(2)
For lambdas, from C++20 onwards you can write lambdas within unevaluated contexts such as decltype, allowing you to write std::barrier<decltype([]() noexcept { /* logic */ })> barrier;.Remunerate
"Because the completion function can be a lambda" Then don't make it a lambda. The lambda police will not pull you over if you use a named type. This is a problem purely of your own making.Tribadism
B
1

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

Blighter answered 26/9, 2022 at 14:9 Comment(2)
What does it mean that it “does not support polymorphic CompletionFunction types/values”? You certainly could use std::function explicitly as the type if it weren’t for the noexcept bit in the question, and std::move_only_function can be used (given an appropriate constructor argument).Blouin
@DavisHerring You are correct - in my head I assumed CompletionFunction was a lambda and didn't think about putting the type erasure in there!Blighter
M
2

Here is a working example in C++20 using a std::barrier object as a class member variable along with the arrive_and_wait method for synchronizing multiple threads. Notice that the default no-op completion function is implied with the empty barrier template:

#include <barrier>
#include <thread>
#include <vector>

using namespace std;

class Foo{
public:
    explicit Foo(int thCnt) : b(thCnt){}
    barrier<> b;

    void thFun(int thNum){
        printf("thNum: %i\n",thNum);
        b.arrive_and_wait();
    }
};

int main(){
    int numWorkers = 5;
    Foo f(numWorkers + 1); // +1 for the b.arrive_and_wait called in main()
    vector<thread> threadVec(numWorkers);
    for(int i = 0 ; i<numWorkers; ++i){
        threadVec.emplace_back(&Foo::thFun,&f,i);
    }
    f.b.arrive_and_wait(); //barrier waits for all threads to finish.
    for(auto &th : threadVec){
        if(th.joinable()) th.join();
    }
    printf("done\n");
    return 0;
}
Magi answered 30/12, 2023 at 22:54 Comment(0)
B
1

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

Blighter answered 26/9, 2022 at 14:9 Comment(2)
What does it mean that it “does not support polymorphic CompletionFunction types/values”? You certainly could use std::function explicitly as the type if it weren’t for the noexcept bit in the question, and std::move_only_function can be used (given an appropriate constructor argument).Blouin
@DavisHerring You are correct - in my head I assumed CompletionFunction was a lambda and didn't think about putting the type erasure in there!Blighter

© 2022 - 2024 — McMap. All rights reserved.