Skip to content

Commit 832370c

Browse files
committed
✨ Add call_by_need
Problem: - Function call is asymmetric: it allows many arguments, but only one return value. - Higher-order asynchronous functions like `async::then` deal with calling multiple functions and forwarding multiple results; they don't have any building-blocks to work with. Solution: - Add `call_by_need` which, given a tuple of functions and a tuple of arguments, calls the functions as is possible, returning their results and passing on any unused arguments.
1 parent cc3d354 commit 832370c

File tree

9 files changed

+510
-0
lines changed

9 files changed

+510
-0
lines changed

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ target_sources(
4848
include/stdx/bitset.hpp
4949
include/stdx/byterator.hpp
5050
include/stdx/cached.hpp
51+
include/stdx/call_by_need.hpp
5152
include/stdx/compiler.hpp
5253
include/stdx/concepts.hpp
5354
include/stdx/ct_conversions.hpp

docs/call_by_need.adoc

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
2+
== `call_by_need.hpp`
3+
4+
`call_by_need` is a function that takes a xref:tuple.adoc#_tuple_hpp[tuple] of
5+
functions and a tuple of arguments, and applies the functions to the arguments
6+
as needed. It returns a tuple of results, with any unused arguments forwarded as
7+
if by a call to
8+
https://en.cppreference.com/w/cpp/utility/functional/identity.html[`std::identity`].
9+
10+
NOTE: `call_by_need` is available only in C++20 and later.
11+
12+
This is best explained with examples. The simplest example is when arguments
13+
match up with functions exactly, unambiguously and without conversions:
14+
[source,cpp]
15+
----
16+
// using strongly typed arguments for clarity
17+
template <auto> struct arg {};
18+
19+
auto funcs = stdx::tuple{[] (arg<0>) { return 17; },
20+
[] (arg<1>) { return 42; }};
21+
auto args = stdx::tuple{arg<0>{}, arg<1>{}};
22+
23+
auto r = stdx::call_by_need(funcs, args); // tuple{17, 42}
24+
----
25+
26+
If multiple functions take the same argument, that works too:
27+
[source,cpp]
28+
----
29+
auto funcs = stdx::tuple{[] (arg<0>) { return 17; },
30+
[] (arg<0>) { return 42; }};
31+
auto args = stdx::tuple{arg<0>{}};
32+
33+
auto r = stdx::call_by_need(funcs, args); // tuple{17, 42}
34+
----
35+
36+
If arguments are unused, they are treated as if `std::identity` was appended to
37+
the set of functions:
38+
[source,cpp]
39+
----
40+
auto funcs = stdx::tuple{[] (arg<0>) { return 17; }};
41+
auto args = stdx::tuple{arg<0>{}, arg<1>{}}; // arg<1>{} is unused
42+
43+
auto r = stdx::call_by_need(funcs, args); // tuple{17, arg<1>{}}
44+
----
45+
46+
Arguments and functions do not have to be in corresponding order; the results will be
47+
in the same order as the functions.
48+
[source,cpp]
49+
----
50+
auto funcs = stdx::tuple{[] (arg<0>) { return 17; },
51+
[] (arg<1>) { return 42; }};
52+
auto args = stdx::tuple{arg<2>{}, arg<1>{}, arg<0>{}}; // arg<2>{} is unused
53+
54+
auto r = stdx::call_by_need(funcs, args); // tuple{17, 42, arg<2>{}}
55+
----
56+
57+
Functions taking multiple arguments require them to be contiguous in the
58+
argument set, but they may overlap:
59+
[source,cpp]
60+
----
61+
auto funcs = stdx::tuple{[] (arg<0>, arg<1>) { return 17; },
62+
[] (arg<1>, arg<2>) { return 42; }};
63+
auto args = stdx::tuple{arg<0>{}, arg<1>{}, arg<2>{}}; // arg<1>{} is used twice
64+
65+
auto r = stdx::call_by_need(funcs, args); // tuple{17, 42}
66+
----
67+
68+
Functions returning `void` are called, but the result cannot contain `void` of course:
69+
[source,cpp]
70+
----
71+
auto funcs = stdx::tuple{[] (arg<0>) { /* called but returns void */ },
72+
[] (arg<1>) { return 42; }};
73+
auto args = stdx::tuple{arg<0>{}, arg<1>{}};
74+
75+
auto r = stdx::call_by_need(funcs, args); // tuple{42}
76+
----
77+
78+
Functions with default arguments will have them provided if possible:
79+
[source,cpp]
80+
----
81+
auto funcs = stdx::tuple{[] (arg<0>, int i = 17) { return i; }};
82+
83+
auto r1 = stdx::call_by_need(funcs, stdx::tuple{arg<0>{}}); // tuple{17} (using default arg)
84+
auto r2 = stdx::call_by_need(funcs, stdx::tuple{arg<0>{}, 18}); // tuple{18} (using provided arg)
85+
----
86+
87+
If a call cannot be made, it's a compile-time error:
88+
[source,cpp]
89+
----
90+
auto funcs = stdx::tuple{[] (int) {}};
91+
92+
auto r = stdx::call_by_need(funcs, stdx::tuple{}); // error! (no argument for int parameter)
93+
// static_assert: call_by_need could not find calls for all given functions
94+
----
95+
96+
97+
The actual algorithm implemented by `call_by_need` is as follows:
98+
99+
1. For each function:
100+
* Try to call it with all arguments `0 ... N-1`.
101+
* If that fails, try to call it with arguments `0 ... N-2`, etc until a call succeeds.
102+
* If all such calls fail, repeat with arguments `1 ... N-1`, etc.
103+
2. The results of the discovered well-formed calls (with `void` filtered out) become the `tuple` of results.
104+
3. Any unused arguments are appended to the `tuple` of results, keeping their original order stable.
105+
106+
NOTE: The elements of the results `tuple` must be movable.
107+
108+
CAUTION: The calls made by `call_by_need` are subject to the usual C++ argument
109+
conversion rules.
110+
111+
The following does not call either function with the second given argument
112+
(`42`) even though it looks like there is a correspondence between functions and
113+
arguments:
114+
115+
[source,cpp]
116+
----
117+
auto funcs = stdx::tuple{[] (char) { return 17; },
118+
[] (int) { return 18; }};
119+
auto args = stdx::tuple{'a', 42};
120+
121+
auto r = stdx::call_by_need(funcs, args); // tuple{17, 18, 42}
122+
----
123+
124+
Because the second function can be called with `'a'` (and that call possibility
125+
is found first by the above algorithm), `42` is passed through without
126+
participating in a call. Situations like this may in turn provoke conversion
127+
warnings (although in this case, `char` may be promoted to `int` without
128+
warning).

docs/index.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ include::bit.adoc[]
1515
include::bitset.adoc[]
1616
include::byterator.adoc[]
1717
include::cached.adoc[]
18+
include::call_by_need.adoc[]
1819
include::compiler.adoc[]
1920
include::concepts.adoc[]
2021
include::ct_conversions.adoc[]

docs/intro.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ The following headers are available:
7373
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/bitset.hpp[`bitset.hpp`]
7474
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/byterator.hpp[`byterator.hpp`]
7575
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/cached.hpp[`cached.hpp`]
76+
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/call_by_need.hpp[`call_by_need.hpp`]
7677
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/compiler.hpp[`compiler.hpp`]
7778
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/concepts.hpp[`concepts.hpp`]
7879
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/ct_conversions.hpp[`ct_conversions.hpp`]

include/stdx/call_by_need.hpp

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
#pragma once
2+
3+
#include <stdx/compiler.hpp>
4+
#include <stdx/ct_conversions.hpp>
5+
#include <stdx/tuple.hpp>
6+
#include <stdx/tuple_algorithms.hpp>
7+
#include <stdx/type_traits.hpp>
8+
9+
#include <algorithm>
10+
#include <array>
11+
#include <cstddef>
12+
#include <functional>
13+
#include <iterator>
14+
#include <type_traits>
15+
#include <utility>
16+
17+
template <auto...> struct undef_v;
18+
template <typename...> struct undef_t;
19+
20+
namespace stdx {
21+
inline namespace v1 {
22+
namespace cbn_detail {
23+
struct call_info {
24+
std::size_t fn_idx;
25+
std::size_t arg_base;
26+
std::size_t arg_len;
27+
28+
[[nodiscard]] constexpr auto uses_arg(std::size_t n) const {
29+
return n >= arg_base and n < arg_base + arg_len;
30+
}
31+
};
32+
33+
template <std::size_t R, typename T, std::size_t N>
34+
CONSTEVAL auto truncate_array(std::array<T, N> const &arr) {
35+
return [&]<std::size_t... Is>(std::index_sequence<Is...>) {
36+
return std::array<T, sizeof...(Is)>{arr[Is]...};
37+
}(std::make_index_sequence<R>{});
38+
}
39+
40+
template <typename T, std::size_t N, std::size_t M>
41+
CONSTEVAL auto concat(std::array<T, N> const &a1, std::array<T, M> const &a2) {
42+
std::array<T, N + M> result{};
43+
auto it = std::copy(std::cbegin(a1), std::cend(a1), std::begin(result));
44+
std::copy(std::cbegin(a2), std::cend(a2), it);
45+
return result;
46+
}
47+
48+
struct void_t {};
49+
template <typename T>
50+
using is_nonvoid_t = std::bool_constant<not std::is_same_v<T, void_t>>;
51+
52+
template <std::size_t Base, std::size_t Len, typename F, typename Args>
53+
constexpr auto invoke(F &&f, Args &&args) -> decltype(auto) {
54+
return [&]<std::size_t... Is>(
55+
std::index_sequence<Is...>) -> decltype(auto) {
56+
using R = std::invoke_result_t<F, decltype(get<Base + Is>(
57+
std::forward<Args>(args)))...>;
58+
if constexpr (std::is_void_v<R>) {
59+
std::forward<F>(f)(get<Base + Is>(std::forward<Args>(args))...);
60+
return void_t{};
61+
} else {
62+
return std::forward<F>(f)(
63+
get<Base + Is>(std::forward<Args>(args))...);
64+
}
65+
}(std::make_index_sequence<Len>{});
66+
}
67+
68+
template <typename... Fs> struct by_need {
69+
template <typename... Args>
70+
[[nodiscard]] CONSTEVAL static auto compute_call_info_impl() {
71+
auto results = std::array<call_info, sizeof...(Fs) + sizeof...(Args)>{};
72+
auto result_count = std::size_t{};
73+
74+
auto const test =
75+
[&]<std::size_t N, std::size_t Base, std::size_t Len>() -> bool {
76+
return [&]<std::size_t... Is>(std::index_sequence<Is...>) -> bool {
77+
if constexpr (requires {
78+
typename std::invoke_result_t<
79+
nth_t<N, Fs...>,
80+
nth_t<Base + Is, Args...>...>;
81+
}) {
82+
results[result_count++] = {N, Base, Len};
83+
return true;
84+
}
85+
return false;
86+
}(std::make_index_sequence<Len>{});
87+
};
88+
89+
auto const inner_loop = [&]<std::size_t N, std::size_t Base>() -> bool {
90+
constexpr auto max_len = sizeof...(Args) - Base;
91+
return [&]<std::size_t... Js>(std::index_sequence<Js...>) {
92+
return (... or
93+
test.template operator()<N, Base, max_len - Js>());
94+
}(std::make_index_sequence<max_len + 1>{});
95+
};
96+
97+
auto const outer_loop = [&]<std::size_t N>() {
98+
return [&]<std::size_t... Bs>(std::index_sequence<Bs...>) -> bool {
99+
// if there are no args, still check the nullary call
100+
return ((sizeof...(Bs) == 0 and
101+
test.template operator()<N, 0, 0>()) or
102+
... or inner_loop.template operator()<N, Bs>());
103+
}(std::make_index_sequence<sizeof...(Args)>{});
104+
};
105+
106+
[&]<std::size_t... Ns>(std::index_sequence<Ns...>) {
107+
(outer_loop.template operator()<Ns>(), ...);
108+
}(std::make_index_sequence<sizeof...(Fs)>{});
109+
110+
return std::pair{results, result_count};
111+
}
112+
113+
template <typename... Args>
114+
[[nodiscard]] CONSTEVAL static auto compute_call_info() {
115+
constexpr auto given_calls = [] {
116+
constexpr auto cs = compute_call_info_impl<Args...>();
117+
return truncate_array<cs.second>(cs.first);
118+
}();
119+
static_assert(
120+
std::size(given_calls) == sizeof...(Fs),
121+
"call_by_need could not find calls for all the given functions");
122+
123+
constexpr auto extra_calls = [&] {
124+
constexpr auto cs = [&]<std::size_t... Is>(
125+
std::index_sequence<Is...>) {
126+
auto results =
127+
std::array<cbn_detail::call_info, sizeof...(Args)>{};
128+
auto unused_count = std::size_t{};
129+
for (auto i = std::size_t{}; i < sizeof...(Args); ++i) {
130+
if (std::none_of(std::cbegin(given_calls),
131+
std::cend(given_calls),
132+
[&](auto c) { return c.uses_arg(i); })) {
133+
results[unused_count++] = {sizeof...(Fs), i, 1};
134+
}
135+
}
136+
return std::pair{results, unused_count};
137+
}(std::make_index_sequence<sizeof...(Args)>{});
138+
return truncate_array<cs.second>(cs.first);
139+
}();
140+
141+
return concat(given_calls, extra_calls);
142+
}
143+
};
144+
} // namespace cbn_detail
145+
146+
template <tuplelike Fs, tuplelike Args>
147+
constexpr auto call_by_need(Fs &&fs, Args &&args) {
148+
constexpr auto calls =
149+
[&]<std::size_t... Is, std::size_t... Js>(std::index_sequence<Is...>,
150+
std::index_sequence<Js...>) {
151+
return cbn_detail::by_need<decltype(get<Is>(
152+
std::forward<Fs>(fs)))...>::
153+
template compute_call_info<decltype(get<Js>(
154+
std::forward<Args>(args)))...>();
155+
}(std::make_index_sequence<tuple_size_v<remove_cvref_t<Fs>>>{},
156+
std::make_index_sequence<tuple_size_v<remove_cvref_t<Args>>>{});
157+
158+
auto new_fs = [&]<std::size_t... Is>(std::index_sequence<Is...>) {
159+
return tuple{get<Is>(std::forward<Fs>(fs))..., std::identity{}};
160+
}(std::make_index_sequence<tuple_size_v<Fs>>{});
161+
162+
auto ret = [&]<std::size_t... Is>(std::index_sequence<Is...>) {
163+
return tuple{cbn_detail::invoke<calls[Is].arg_base, calls[Is].arg_len>(
164+
get<calls[Is].fn_idx>(std::move(new_fs)),
165+
std::forward<Args>(args))...};
166+
}(std::make_index_sequence<calls.size()>{});
167+
return stdx::filter<cbn_detail::is_nonvoid_t>(std::move(ret));
168+
}
169+
} // namespace v1
170+
} // namespace stdx

test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ target_compile_definitions(
7878
if(${CMAKE_CXX_STANDARD} GREATER_EQUAL 20)
7979
add_tests(
8080
FILES
81+
call_by_need
8182
ct_format
8283
ct_string
8384
env

0 commit comments

Comments
 (0)