Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions include/exec/repeat_n.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,25 +76,27 @@ namespace experimental::execution
template <class _Error>
constexpr void set_error(_Error &&__err) noexcept
{
auto *__state = __state_;
STDEXEC_TRY
{
auto __err_copy = static_cast<_Error &&>(__err); // make a copy of the error...
__state_->__cleanup(); // ... because this could potentially invalidate it.
STDEXEC::set_error(std::move(__state_->__rcvr_), std::move(__err_copy));
__state->__cleanup(); // ... because this could potentially invalidate it.
STDEXEC::set_error(std::move(__state->__rcvr_), std::move(__err_copy));
}
STDEXEC_CATCH_ALL
{
if constexpr (!__nothrow_decay_copyable<_Error>)
{
STDEXEC::set_error(std::move(__state_->__rcvr_), std::current_exception());
STDEXEC::set_error(std::move(__state->__rcvr_), std::current_exception());
}
}
}

constexpr void set_stopped() noexcept
{
__state_->__cleanup();
STDEXEC::set_stopped(std::move(__state_->__rcvr_));
auto *__state = __state_;
__state->__cleanup();
STDEXEC::set_stopped(std::move(__state->__rcvr_));
}

[[nodiscard]]
Expand Down
31 changes: 17 additions & 14 deletions include/exec/repeat_until.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,17 @@ namespace experimental::execution
template <class... _Booleans>
constexpr void set_value(_Booleans &&...__bools) noexcept
{
auto *__state = __state_;
if constexpr ((__is_bool_constant<_Booleans, true> && ...))
{
// Always done:
__state_->__cleanup();
STDEXEC::set_value(std::move(__state_->__rcvr_));
__state->__cleanup();
STDEXEC::set_value(std::move(__state->__rcvr_));
}
else if constexpr ((__is_bool_constant<_Booleans, false> && ...))
{
// Never done:
__state_->__repeat();
__state->__repeat();
}
else
{
Expand All @@ -91,20 +92,20 @@ namespace experimental::execution
bool const __done = (static_cast<bool>(static_cast<_Booleans &&>(__bools)) && ...);
if (__done)
{
__state_->__cleanup();
STDEXEC::set_value(std::move(__state_->__rcvr_));
__state->__cleanup();
STDEXEC::set_value(std::move(__state->__rcvr_));
}
else
{
__state_->__repeat();
__state->__repeat();
}
}
STDEXEC_CATCH_ALL
{
if constexpr (!__is_nothrow)
{
__state_->__cleanup();
STDEXEC::set_error(std::move(__state_->__rcvr_), std::current_exception());
__state->__cleanup();
STDEXEC::set_error(std::move(__state->__rcvr_), std::current_exception());
}
}
}
Expand All @@ -113,26 +114,28 @@ namespace experimental::execution
template <class _Error>
constexpr void set_error(_Error &&__err) noexcept
{
auto *__state = __state_;
STDEXEC_TRY
{
auto __err_copy = static_cast<_Error &&>(__err); // make a local copy of the error...
__state_->__cleanup(); // ... because this could potentially invalidate it.
STDEXEC::set_error(std::move(__state_->__rcvr_), static_cast<_Error &&>(__err_copy));
__state->__cleanup(); // ... because this could potentially invalidate it.
STDEXEC::set_error(std::move(__state->__rcvr_), static_cast<_Error &&>(__err_copy));
}
STDEXEC_CATCH_ALL
{
if constexpr (!__nothrow_decay_copyable<_Error>)
{
__state_->__cleanup();
STDEXEC::set_error(std::move(__state_->__rcvr_), std::current_exception());
__state->__cleanup();
STDEXEC::set_error(std::move(__state->__rcvr_), std::current_exception());
}
}
}

constexpr void set_stopped() noexcept
{
__state_->__cleanup();
STDEXEC::set_stopped(std::move(__state_->__rcvr_));
auto *__state = __state_;
__state->__cleanup();
STDEXEC::set_stopped(std::move(__state->__rcvr_));
}

[[nodiscard]]
Expand Down
49 changes: 49 additions & 0 deletions test/exec/test_repeat_n.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,36 @@

#include <test_common/catch2.hpp>

#include "test_repeat_receiver_lifetime.hpp"

using namespace STDEXEC;

namespace
{
namespace lifetime_test = repeat_receiver_lifetime_test;

struct send_error
{
using signature = ex::set_error_t(int);

template <class Receiver>
void operator()(Receiver &&rcvr) const noexcept
{
ex::set_error(static_cast<Receiver &&>(rcvr), 42);
}
};

struct send_stopped
{
using signature = ex::set_stopped_t();

template <class Receiver>
void operator()(Receiver &&rcvr) const noexcept
{
ex::set_stopped(static_cast<Receiver &&>(rcvr));
}
};

TEST_CASE("repeat_n returns a sender", "[adaptors][repeat_n]")
{
auto snd = exec::repeat_n(ex::just() | then([] {}), 10);
Expand Down Expand Up @@ -115,6 +141,29 @@ namespace
CHECK(count == 1);
}

TEST_CASE("repeat_n does not access its child receiver after cleanup", "[adaptors][repeat_n]")
{
SECTION("set_error")
{
bool invalidated = false;
auto snd = lifetime_test::invalidate_on_destroy_sender{send_error{}, &invalidated}
| exec::repeat_n(1);
auto op = ex::connect(std::move(snd), expect_error_receiver{42});
ex::start(op);
CHECK(invalidated);
}

SECTION("set_stopped")
{
bool invalidated = false;
auto snd = lifetime_test::invalidate_on_destroy_sender{send_stopped{}, &invalidated}
| exec::repeat_n(1);
auto op = ex::connect(std::move(snd), expect_stopped_receiver{});
ex::start(op);
CHECK(invalidated);
}
}

TEST_CASE("running deeply recursing algo on repeat_n doesn't blow the stack",
"[adaptors][repeat_n]")
{
Expand Down
76 changes: 76 additions & 0 deletions test/exec/test_repeat_receiver_lifetime.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright (c) 2026 NVIDIA Corporation
*
* Licensed under the Apache License, Version 2.0 with LLVM Exceptions
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://llvm.org/LICENSE.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once

#include "stdexec/execution.hpp"

namespace repeat_receiver_lifetime_test
{
namespace ex = STDEXEC;

template <class Completion>
struct invalidate_on_destroy_sender
{
using sender_concept = ex::sender_tag;
using completion_signatures = ex::completion_signatures<typename Completion::signature>;

template <class Receiver>
struct operation
{
operation(Receiver rcvr, Completion completion, bool *invalidated) noexcept
: rcvr_(static_cast<Receiver &&>(rcvr))
, completion_(static_cast<Completion &&>(completion))
, invalidated_(invalidated)
{}

~operation()
{
if constexpr (requires { rcvr_.__self_->__rcvr_.__state_; })
{
if (started_)
{
rcvr_.__self_->__rcvr_.__state_ = nullptr;
*invalidated_ = true;
}
}
}

void start() & noexcept
{
started_ = true;
completion_(static_cast<Receiver &&>(rcvr_));
}

Receiver rcvr_;
Completion completion_;
bool *invalidated_;
bool started_ = false;
};

template <ex::receiver_of<completion_signatures> Receiver>
auto connect(Receiver rcvr) const -> operation<Receiver>
{
return {static_cast<Receiver &&>(rcvr), completion_, invalidated_};
}

Completion completion_;
bool *invalidated_;
};

template <class Completion>
STDEXEC_HOST_DEVICE_DEDUCTION_GUIDE
invalidate_on_destroy_sender(Completion, bool *) -> invalidate_on_destroy_sender<Completion>;
} // namespace repeat_receiver_lifetime_test
Loading
Loading