Skip to content

Commit fa878ff

Browse files
committed
exec::sync_object
Adaptor which transforms a regular, synchronous object into an asynchronous object.
1 parent 84f5f0f commit fa878ff

File tree

4 files changed

+212
-2
lines changed

4 files changed

+212
-2
lines changed

include/exec/lifetime.hpp

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

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

include/exec/sync_object.hpp

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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 "../stdexec/execution.hpp"
29+
30+
namespace experimental::execution {
31+
32+
template<typename T, typename... Args>
33+
requires
34+
std::is_constructible_v<T, Args...> &&
35+
std::is_destructible_v<T>
36+
struct sync_object {
37+
using type = T;
38+
template<typename... Ts>
39+
requires (std::is_constructible_v<Args, Ts> && ...)
40+
constexpr explicit sync_object(Ts&&... ts) noexcept(
41+
(std::is_nothrow_constructible_v<Args, Ts> && ...))
42+
: args_((Ts&&)ts...)
43+
{}
44+
constexpr enter_scope_sender auto operator()(type* storage) &
45+
noexcept(noexcept(make_sender(*this, storage)))
46+
{
47+
return make_sender(*this, storage);
48+
}
49+
constexpr enter_scope_sender auto operator()(type* storage) const &
50+
noexcept(noexcept(make_sender(*this, storage)))
51+
{
52+
return make_sender(*this, storage);
53+
}
54+
constexpr enter_scope_sender auto operator()(type* storage) &&
55+
noexcept(noexcept(make_sender(std::move(*this), storage)))
56+
{
57+
return make_sender(std::move(*this), storage);
58+
}
59+
constexpr enter_scope_sender auto operator()(type* storage) const &&
60+
noexcept(noexcept(make_sender(std::move(*this), storage)))
61+
{
62+
return make_sender(std::move(*this), storage);
63+
}
64+
private:
65+
template<typename Self>
66+
static constexpr enter_scope_sender auto make_sender(Self&& self, type* storage)
67+
noexcept(
68+
std::is_nothrow_constructible_v<
69+
std::tuple<Args...>,
70+
::STDEXEC::__copy_cvref_t<Self, std::tuple<Args...>>>)
71+
{
72+
constexpr auto nothrow = std::is_nothrow_constructible_v<T, Args...>;
73+
return
74+
::STDEXEC::just(std::forward<Self>(self).args_) |
75+
::STDEXEC::then([storage](std::tuple<Args...>&& tuple) noexcept(nothrow) {
76+
const auto ptr = std::construct_at(
77+
storage,
78+
::exec::elide([&]() noexcept(nothrow) {
79+
return std::make_from_tuple<T>(std::move(tuple));
80+
}));
81+
return
82+
::STDEXEC::just() |
83+
// It's important we capture ptr not storage because storage just
84+
// points to storage where ptr actually points to an object
85+
::STDEXEC::then([ptr]() noexcept {
86+
ptr->~T();
87+
});
88+
});
89+
}
90+
std::tuple<Args...> args_;
91+
};
92+
93+
template<typename T, typename... Args>
94+
constexpr sync_object<T, std::decay_t<Args>...> make_sync_object(Args&&... args)
95+
noexcept((std::is_nothrow_constructible_v<std::decay_t<Args>, Args> && ...))
96+
{
97+
return sync_object<T, std::decay_t<Args>...>((Args&&)args...);
98+
}
99+
100+
} // namespace exec

test/exec/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ set(exec_test_sources
7373
test_enter_scopes.cpp
7474
test_within.cpp
7575
test_lifetime.cpp
76+
test_sync_object.cpp
7677
)
7778

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