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::barrier
s 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
).
decltype
, allowing you to writestd::barrier<decltype([]() noexcept { /* logic */ })> barrier;
. – Remunerate