Skip to content

Commit 3cedfe8

Browse files
committed
exec::sync_object
Adaptor which transforms a regular, synchronous object into an asynchronous object.
1 parent c21105f commit 3cedfe8

File tree

4 files changed

+213
-2
lines changed

4 files changed

+213
-2
lines changed

include/exec/lifetime.hpp

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,16 @@ struct t {
9494

9595
template<typename T>
9696
class storage_for_object {
97-
alignas(T) std::byte buffer_[sizeof(T)];
97+
union type_ {
98+
char c;
99+
T t;
100+
constexpr type_() noexcept : c() {}
101+
constexpr ~type_() noexcept {}
102+
};
103+
type_ storage_;
98104
public:
99105
constexpr T* get_storage() noexcept {
100-
return reinterpret_cast<T*>(buffer_);
106+
return std::addressof(storage_.t);
101107
}
102108
constexpr T& get_object() noexcept {
103109
return *std::launder(get_storage());

include/exec/sync_object.hpp

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
* Copyright (c) 2025 Robert Leahy. All rights reserved.
4+
* SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5+
*
6+
* Licensed under the Apache License, Version 2.0 with LLVM Exceptions (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* https://llvm.org/LICENSE.txt
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
#pragma once
20+
21+
#include <memory>
22+
#include <tuple>
23+
#include <type_traits>
24+
#include <utility>
25+
26+
#include "elide.hpp"
27+
#include "enter_scope_sender.hpp"
28+
#include "like_t.hpp"
29+
#include "../stdexec/execution.hpp"
30+
31+
namespace exec {
32+
33+
template<typename T, typename... Args>
34+
requires
35+
std::is_constructible_v<T, Args...> &&
36+
std::is_destructible_v<T>
37+
struct sync_object {
38+
using type = T;
39+
template<typename... Ts>
40+
requires (std::is_constructible_v<Args, Ts> && ...)
41+
constexpr explicit sync_object(Ts&&... ts) noexcept(
42+
(std::is_nothrow_constructible_v<Args, Ts> && ...))
43+
: args_((Ts&&)ts...)
44+
{}
45+
constexpr enter_scope_sender auto operator()(type* storage) &
46+
noexcept(noexcept(make_sender(*this, storage)))
47+
{
48+
return make_sender(*this, storage);
49+
}
50+
constexpr enter_scope_sender auto operator()(type* storage) const &
51+
noexcept(noexcept(make_sender(*this, storage)))
52+
{
53+
return make_sender(*this, storage);
54+
}
55+
constexpr enter_scope_sender auto operator()(type* storage) &&
56+
noexcept(noexcept(make_sender(std::move(*this), storage)))
57+
{
58+
return make_sender(std::move(*this), storage);
59+
}
60+
constexpr enter_scope_sender auto operator()(type* storage) const &&
61+
noexcept(noexcept(make_sender(std::move(*this), storage)))
62+
{
63+
return make_sender(std::move(*this), storage);
64+
}
65+
private:
66+
template<typename Self>
67+
static constexpr enter_scope_sender auto make_sender(Self&& self, type* storage)
68+
noexcept(
69+
std::is_nothrow_constructible_v<
70+
std::tuple<Args...>,
71+
like_t<Self, std::tuple<Args...>>>)
72+
{
73+
constexpr auto nothrow = std::is_nothrow_constructible_v<T, Args...>;
74+
return
75+
::STDEXEC::just(std::forward<Self>(self).args_) |
76+
::STDEXEC::then([storage](std::tuple<Args...>&& tuple) noexcept(nothrow) {
77+
const auto ptr = std::construct_at(
78+
storage,
79+
::exec::elide([&]() noexcept(nothrow) {
80+
return std::make_from_tuple<T>(std::move(tuple));
81+
}));
82+
return
83+
::STDEXEC::just() |
84+
// It's important we capture ptr not storage because storage just
85+
// points to storage where ptr actually points to an object
86+
::STDEXEC::then([ptr]() noexcept {
87+
ptr->~T();
88+
});
89+
});
90+
}
91+
std::tuple<Args...> args_;
92+
};
93+
94+
template<typename T, typename... Args>
95+
constexpr sync_object<T, std::decay_t<Args>...> make_sync_object(Args&&... args)
96+
noexcept((std::is_nothrow_constructible_v<std::decay_t<Args>, Args> && ...))
97+
{
98+
return sync_object<T, std::decay_t<Args>...>((Args&&)args...);
99+
}
100+
101+
} // namespace exec

test/exec/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ set(exec_test_sources
6969
test_enter_scopes.cpp
7070
test_within.cpp
7171
test_lifetime.cpp
72+
test_sync_object.cpp
7273
)
7374

7475
add_executable(test.exec ${exec_test_sources})

test/exec/test_sync_object.cpp

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
* Copyright (c) 2025 Robert Leahy. All rights reserved.
4+
* SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5+
*
6+
* Licensed under the Apache License, Version 2.0 with LLVM Exceptions (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* https://llvm.org/LICENSE.txt
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
#include <exec/sync_object.hpp>
20+
21+
#include <cstddef>
22+
#include <functional>
23+
#include <utility>
24+
25+
#include <catch2/catch.hpp>
26+
27+
#include <exec/lifetime.hpp>
28+
#include <stdexec/execution.hpp>
29+
30+
#include "../test_common/receivers.hpp"
31+
32+
namespace {
33+
34+
struct state {
35+
std::size_t constructed{0};
36+
std::size_t destroyed{0};
37+
};
38+
39+
class object {
40+
state& s_;
41+
public:
42+
int i;
43+
explicit constexpr object(state& s, int i) noexcept : s_(s), i(i) {
44+
++s_.constructed;
45+
}
46+
object(const object&) = delete;
47+
object& operator=(const object&) = delete;
48+
constexpr ~object() noexcept {
49+
++s_.destroyed;
50+
}
51+
};
52+
53+
// GCC 14 complains about an object being used outside its lifetime trying to
54+
// build this, but doesn't really give any clues about which object so it's
55+
// difficult to address
56+
#ifdef __clang__
57+
static_assert([]() {
58+
state s;
59+
struct receiver {
60+
using receiver_concept = ::STDEXEC::receiver_t;
61+
bool& b_;
62+
constexpr void set_value(const int i) && noexcept {
63+
b_ = i == 5;
64+
}
65+
};
66+
auto sender = ::exec::lifetime(
67+
[&](object& o) noexcept {
68+
return ::STDEXEC::just(o.i);
69+
},
70+
::exec::make_sync_object<object>(
71+
std::ref(s),
72+
5));
73+
bool success = false;
74+
auto op = ::STDEXEC::connect(
75+
std::move(sender),
76+
receiver{success});
77+
::STDEXEC::start(op);
78+
return success;
79+
}());
80+
#endif
81+
82+
TEST_CASE("Synchronous object may be adapted into asynchronous objects", "[sync_object]") {
83+
state s;
84+
auto sender = ::exec::lifetime(
85+
[&](object& o) {
86+
CHECK(s.constructed == 1);
87+
CHECK(s.destroyed == 0);
88+
return ::STDEXEC::just(o.i);
89+
},
90+
::exec::make_sync_object<object>(
91+
std::ref(s),
92+
5));
93+
auto op = ::STDEXEC::connect(
94+
std::move(sender),
95+
expect_value_receiver(5));
96+
CHECK(s.constructed == 0);
97+
CHECK(s.destroyed == 0);
98+
::STDEXEC::start(op);
99+
CHECK(s.constructed == 1);
100+
CHECK(s.destroyed == 1);
101+
}
102+
103+
} // unnamed namespace

0 commit comments

Comments
 (0)