Skip to content
Open
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
34 changes: 22 additions & 12 deletions include/stdexec/__detail/__tuple.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ namespace STDEXEC
template <class... _Ts>
struct STDEXEC_ATTRIBUTE(empty_bases) __tuple;

//! The type with which an element declared as _Ty is passed on from a
//! tuple of the possibly cvref-qualified type _Tuple: _Ty with _Tuple's
//! cvref-qualifiers, as std::get gives it. Naming the element through the
//! forwarded tuple gives the same, except for an element of rvalue
//! reference type, which names an lvalue.
template <class _Tuple, class _Ty>
using __fwd_element_t = __mcall1<__copy_cvref_fn<_Tuple>, _Ty>&&;

template <class _Ty, std::size_t _Idx>
struct __box
{
Expand All @@ -79,9 +87,10 @@ namespace STDEXEC
static constexpr auto __apply(_Fn&& __fn, _Self&& __self, _Us&&... __us)
noexcept(__nothrow_callable<_Fn, _Us..., __copy_cvref_t<_Self, _Ts>...>) -> decltype(auto)
{
return static_cast<_Fn&&>(__fn)(
static_cast<_Us&&>(__us)...,
static_cast<_Self&&>(__self).STDEXEC_CWG1835_TEMPLATE __box<_Ts, _Index>::__value...);
return static_cast<_Fn&&>(
__fn)(static_cast<_Us&&>(__us)...,
static_cast<__fwd_element_t<_Self, _Ts>>(
__self.STDEXEC_CWG1835_TEMPLATE __box<_Ts, _Index>::__value)...);
}
};

Expand Down Expand Up @@ -233,7 +242,8 @@ namespace STDEXEC
template <class... _Ts>
STDEXEC_HOST_DEVICE_DEDUCTION_GUIDE __tuple(_Ts...) -> __tuple<_Ts...>;

# define STDEXEC_TUPLE_GET(_Idx) , static_cast<_Tuple&&>(__tupl).__val##_Idx
# define STDEXEC_TUPLE_GET(_Idx) \
, static_cast<__fwd_element_t<_Tuple, decltype(__tupl.__val##_Idx)>>(__tupl.__val##_Idx)

//
// __apply(fn, tuple, extra...)
Expand Down Expand Up @@ -383,35 +393,35 @@ namespace STDEXEC
}
else if constexpr (_Index == 0)
{
return static_cast<_Tuple&&>(__tupl).__val0;
return static_cast<__tup::__fwd_element_t<_Tuple, decltype(__tupl.__val0)>>(__tupl.__val0);
}
else if constexpr (_Index == 1)
{
return static_cast<_Tuple&&>(__tupl).__val1;
return static_cast<__tup::__fwd_element_t<_Tuple, decltype(__tupl.__val1)>>(__tupl.__val1);
}
else if constexpr (_Index == 2)
{
return static_cast<_Tuple&&>(__tupl).__val2;
return static_cast<__tup::__fwd_element_t<_Tuple, decltype(__tupl.__val2)>>(__tupl.__val2);
}
else if constexpr (_Index == 3)
{
return static_cast<_Tuple&&>(__tupl).__val3;
return static_cast<__tup::__fwd_element_t<_Tuple, decltype(__tupl.__val3)>>(__tupl.__val3);
}
else if constexpr (_Index == 4)
{
return static_cast<_Tuple&&>(__tupl).__val4;
return static_cast<__tup::__fwd_element_t<_Tuple, decltype(__tupl.__val4)>>(__tupl.__val4);
}
else if constexpr (_Index == 5)
{
return static_cast<_Tuple&&>(__tupl).__val5;
return static_cast<__tup::__fwd_element_t<_Tuple, decltype(__tupl.__val5)>>(__tupl.__val5);
}
else if constexpr (_Index == 6)
{
return static_cast<_Tuple&&>(__tupl).__val6;
return static_cast<__tup::__fwd_element_t<_Tuple, decltype(__tupl.__val6)>>(__tupl.__val6);
}
else if constexpr (_Index == 7)
{
return static_cast<_Tuple&&>(__tupl).__val7;
return static_cast<__tup::__fwd_element_t<_Tuple, decltype(__tupl.__val7)>>(__tupl.__val7);
}
else
{
Expand Down
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ set(stdexec_test_sources
stdexec/detail/test_completion_signatures.cpp
stdexec/detail/test_intrusive_mpsc_queue.cpp
stdexec/detail/test_intrusive_queue.cpp
stdexec/detail/test_tuple.cpp
stdexec/detail/test_utility.cpp
stdexec/queries/test_env.cpp
stdexec/queries/test_forwarding_queries.cpp
Expand Down
49 changes: 49 additions & 0 deletions test/exec/test_function.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1743,6 +1743,55 @@ namespace
}
}

struct string_appender
{
std::string suffix;
};

TEST_CASE("function passes curried arguments to the factory as their declared types",
"[types][function]")
{
SECTION("an rvalue reference stays an rvalue, so the factory can move from it")
{
using fn = exec::function<std::string(std::string &&)>;
// a curried rvalue reference must not be passed twice, so there is no
// lvalue connect
STATIC_REQUIRE(!std::copy_constructible<fn>);

std::string str = "a string too long for the small-string optimization";
fn sndr(std::move(str), [](std::string &&s) { return ex::just(std::move(s)); });
auto [ret] = ex::sync_wait(std::move(sndr)).value();
REQUIRE(ret == "a string too long for the small-string optimization");
REQUIRE(str.empty());
}

SECTION("an lvalue reference refers to the caller's object")
{
using fn = exec::function<int(int &)>;

int i = 0;
fn sndr(i, [](int &j) { return ex::just(++j); });
auto [ret] = ex::sync_wait(std::move(sndr)).value();
REQUIRE(ret == 1);
REQUIRE(i == 1);
}

SECTION("member functions' explicit arguments are forwarded the same way")
{
using fn = exec::function<std::string(std::string &&) const &>;

string_appender const appender{"!"};
std::string str = "a string too long for the small-string optimization";
fn sndr(appender,
std::move(str),
[](string_appender const &self, std::string &&s)
{ return ex::just(std::move(s) + self.suffix); });
auto [ret] = ex::sync_wait(std::move(sndr)).value();
REQUIRE(ret == "a string too long for the small-string optimization!");
REQUIRE(str.empty());
}
}

template <class Sig, class... Ts>
concept valid_function_type = requires { typename exec::function<Sig, Ts...>; };

Expand Down
107 changes: 107 additions & 0 deletions test/stdexec/detail/test_tuple.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Copyright (c) 2026 Ian Petersen
* 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.
*/
#include <catch2/catch_all.hpp>

#include <stdexec/__detail/__config.hpp>

#if STDEXEC_USE_MODULES()
import std;
import stdexec;
#else
# include <stdexec/__detail/__tuple.hpp>

# include <array>
# include <string>
# include <type_traits>
# include <utility>
#endif

using namespace STDEXEC;

namespace
{
//! Reports, for each argument, whether it was passed as an rvalue
struct rvalue_args
{
template <class... Ts>
constexpr auto operator()(Ts &&...) const noexcept -> std::array<bool, sizeof...(Ts)>
{
return {!std::is_lvalue_reference_v<Ts>...};
}
};

// more than eight elements, so the tuple stores its elements in base classes
using large_tuple = __tuple<int, int, int, int, int, int, int, int, int &, int &&>;

TEST_CASE("__apply passes each element of a tuple as std::get would", "[detail][tuple]")
{
int i = 0;
int j = 0;

SECTION("a moved tuple passes values and rvalue references as rvalues, and lvalue "
"references as lvalues")
{
__tuple<int, int &, int &&> tupl{0, i, std::move(j)};
REQUIRE(__apply(rvalue_args{}, std::move(tupl)) == std::array{true, false, true});
}

SECTION("an lvalue tuple passes every element as an lvalue")
{
__tuple<int, int &, int &&> tupl{0, i, std::move(j)};
REQUIRE(__apply(rvalue_args{}, tupl) == std::array{false, false, false});

auto const &ctupl = tupl;
REQUIRE(__apply(rvalue_args{}, ctupl) == std::array{false, false, false});
}

SECTION("the same holds for a tuple of more than eight elements")
{
large_tuple tupl{
{{0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {i}, {std::move(j)}}
};
std::array<bool, 10> moved{true, true, true, true, true, true, true, true, false, true};
REQUIRE(__apply(rvalue_args{}, std::move(tupl)) == moved);
REQUIRE(__apply(rvalue_args{}, tupl) == std::array<bool, 10>{});
}

SECTION("a function taking an element by value moves from an rvalue reference")
{
std::string str = "a string too long for the small-string optimization";
__tuple<std::string &&> tupl{std::move(str)};
auto taken = __apply([](std::string s) { return s; }, std::move(tupl));
REQUIRE(taken == "a string too long for the small-string optimization");
REQUIRE(str.empty());
}
}

TEST_CASE("__get returns each element of a tuple as std::get would", "[detail][tuple]")
{
using small_tuple = __tuple<int, int &, int &&>;
STATIC_REQUIRE(std::is_same_v<decltype(__get<0>(std::declval<small_tuple>())), int &&>);
STATIC_REQUIRE(std::is_same_v<decltype(__get<1>(std::declval<small_tuple>())), int &>);
STATIC_REQUIRE(std::is_same_v<decltype(__get<2>(std::declval<small_tuple>())), int &&>);
STATIC_REQUIRE(std::is_same_v<decltype(__get<2>(std::declval<small_tuple &>())), int &>);
STATIC_REQUIRE(
std::is_same_v<decltype(__get<0>(std::declval<small_tuple const &>())), int const &>);
STATIC_REQUIRE(std::is_same_v<decltype(__get<2>(std::declval<small_tuple const &>())), int &>);

STATIC_REQUIRE(std::is_same_v<decltype(__get<0>(std::declval<large_tuple>())), int &&>);
STATIC_REQUIRE(std::is_same_v<decltype(__get<8>(std::declval<large_tuple>())), int &>);
STATIC_REQUIRE(std::is_same_v<decltype(__get<9>(std::declval<large_tuple>())), int &&>);
STATIC_REQUIRE(std::is_same_v<decltype(__get<9>(std::declval<large_tuple &>())), int &>);
}
} // namespace