Skip to content

Commit bb7e789

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 90ae907 commit bb7e789

File tree

10 files changed

+515
-0
lines changed

10 files changed

+515
-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: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
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> {
43+
std::array<T, N + M> result{};
44+
auto it = std::copy(std::cbegin(a1), std::cend(a1), std::begin(result));
45+
std::copy(std::cbegin(a2), std::cend(a2), it);
46+
return result;
47+
}
48+
49+
struct void_t {};
50+
template <typename T>
51+
using is_nonvoid_t = std::bool_constant<not std::is_same_v<T, void_t>>;
52+
53+
template <std::size_t Base, std::size_t Len, typename F, typename Args>
54+
constexpr auto invoke(F &&f, Args &&args) -> decltype(auto) {
55+
return [&]<std::size_t... Is>(
56+
std::index_sequence<Is...>) -> decltype(auto) {
57+
using R = std::invoke_result_t<F, decltype(get<Base + Is>(
58+
std::forward<Args>(args)))...>;
59+
if constexpr (std::is_void_v<R>) {
60+
std::forward<F>(f)(get<Base + Is>(std::forward<Args>(args))...);
61+
return void_t{};
62+
} else {
63+
return std::forward<F>(f)(
64+
get<Base + Is>(std::forward<Args>(args))...);
65+
}
66+
}(std::make_index_sequence<Len>{});
67+
}
68+
69+
template <typename... Fs> struct by_need {
70+
template <typename... Args>
71+
[[nodiscard]] CONSTEVAL static auto compute_call_info_impl() {
72+
auto results = std::array<call_info, sizeof...(Fs) + sizeof...(Args)>{};
73+
auto result_count = std::size_t{};
74+
75+
auto const test =
76+
[&]<std::size_t N, std::size_t Base, std::size_t Len>() -> bool {
77+
return [&]<std::size_t... Is>(std::index_sequence<Is...>) -> bool {
78+
if constexpr (requires {
79+
typename std::invoke_result_t<
80+
nth_t<N, Fs...>,
81+
nth_t<Base + Is, Args...>...>;
82+
}) {
83+
results[result_count++] = {N, Base, Len};
84+
return true;
85+
}
86+
return false;
87+
}(std::make_index_sequence<Len>{});
88+
};
89+
90+
auto const inner_loop = [&]<std::size_t N, std::size_t Base>() -> bool {
91+
constexpr auto max_len = sizeof...(Args) - Base;
92+
return [&]<std::size_t... Ls>(std::index_sequence<Ls...>) {
93+
return (... or
94+
test.template operator()<N, Base, max_len - Ls>());
95+
}(std::make_index_sequence<max_len + 1>{});
96+
};
97+
98+
auto const outer_loop = [&]<std::size_t N>() {
99+
return [&]<std::size_t... Bs>(std::index_sequence<Bs...>) -> bool {
100+
// if there are no args, still check the nullary call
101+
return ((sizeof...(Bs) == 0 and
102+
test.template operator()<N, 0, 0>()) or
103+
... or inner_loop.template operator()<N, Bs>());
104+
}(std::make_index_sequence<sizeof...(Args)>{});
105+
};
106+
107+
[&]<std::size_t... Ns>(std::index_sequence<Ns...>) {
108+
(outer_loop.template operator()<Ns>(), ...);
109+
}(std::make_index_sequence<sizeof...(Fs)>{});
110+
111+
return std::pair{results, result_count};
112+
}
113+
114+
template <typename... Args>
115+
[[nodiscard]] CONSTEVAL static auto compute_call_info() {
116+
constexpr auto given_calls = [] {
117+
constexpr auto cs = compute_call_info_impl<Args...>();
118+
return truncate_array<cs.second>(cs.first);
119+
}();
120+
static_assert(
121+
std::size(given_calls) == sizeof...(Fs),
122+
"call_by_need could not find calls for all the given functions");
123+
124+
constexpr auto extra_calls = [&] {
125+
constexpr auto cs = [&]<std::size_t... Is>(
126+
std::index_sequence<Is...>) {
127+
auto results =
128+
std::array<cbn_detail::call_info, sizeof...(Args)>{};
129+
auto unused_count = std::size_t{};
130+
if constexpr (sizeof...(Args) > 0) {
131+
for (auto i = std::size_t{}; i < sizeof...(Args); ++i) {
132+
if (std::none_of(std::cbegin(given_calls),
133+
std::cend(given_calls), [&](auto c) {
134+
return c.uses_arg(i);
135+
})) {
136+
results[unused_count++] = {sizeof...(Fs), i, 1};
137+
}
138+
}
139+
}
140+
return std::pair{results, unused_count};
141+
}(std::make_index_sequence<sizeof...(Args)>{});
142+
return truncate_array<cs.second>(cs.first);
143+
}();
144+
145+
return concat(given_calls, extra_calls);
146+
}
147+
};
148+
} // namespace cbn_detail
149+
150+
template <tuplelike Fs, tuplelike Args>
151+
constexpr auto call_by_need(Fs &&fs, Args &&args) {
152+
constexpr auto calls =
153+
[&]<std::size_t... Is, std::size_t... Js>(std::index_sequence<Is...>,
154+
std::index_sequence<Js...>) {
155+
return cbn_detail::by_need<decltype(get<Is>(
156+
std::forward<Fs>(fs)))...>::
157+
template compute_call_info<decltype(get<Js>(
158+
std::forward<Args>(args)))...>();
159+
}(std::make_index_sequence<tuple_size_v<remove_cvref_t<Fs>>>{},
160+
std::make_index_sequence<tuple_size_v<remove_cvref_t<Args>>>{});
161+
162+
auto new_fs = [&]<std::size_t... Is>(std::index_sequence<Is...>) {
163+
return tuple{get<Is>(std::forward<Fs>(fs))..., std::identity{}};
164+
}(std::make_index_sequence<tuple_size_v<Fs>>{});
165+
166+
auto ret = [&]<std::size_t... Is>(std::index_sequence<Is...>) {
167+
return tuple{cbn_detail::invoke<calls[Is].arg_base, calls[Is].arg_len>(
168+
get<calls[Is].fn_idx>(std::move(new_fs)),
169+
std::forward<Args>(args))...};
170+
}(std::make_index_sequence<calls.size()>{});
171+
return stdx::filter<cbn_detail::is_nonvoid_t>(std::move(ret));
172+
}
173+
} // namespace v1
174+
} // 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)