Skip to content

Commit 7086d55

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 7086d55

File tree

7 files changed

+440
-0
lines changed

7 files changed

+440
-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: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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+
The actual algorithm implemented by `call_by_need` is as follows:
79+
80+
1. For each function:
81+
* Try to call it with all arguments `0 ... N-1`.
82+
* If that fails, try to call it with arguments `0 ... N-2`, etc until a call succeeds.
83+
* If all such calls fail, repeat with arguments `1 ... N-1`, etc.
84+
2. The results of the discovered well-formed calls (with `void` filtered out) become the `tuple` of results.
85+
3. Any unused arguments are appended to the `tuple` of results, keeping their original order stable.
86+
87+
NOTE: The elements of the results `tuple` must be movable.
88+
89+
CAUTION: The calls made by `call_by_need` are subject to the usual C++ argument
90+
conversion rules.
91+
92+
The following does not call either function with the second given argument
93+
(`42`) even though it looks like there is a correspondence between functions and
94+
arguments:
95+
96+
[source,cpp]
97+
----
98+
auto funcs = stdx::tuple{[] (char) { return 17; },
99+
[] (int) { return 18; }};
100+
auto args = stdx::tuple{'a', 42};
101+
102+
auto r = stdx::call_by_need(funcs, args); // tuple{17, 18, 42}
103+
----
104+
105+
Because the second function can be called with `'a'` (and that call possibility
106+
is found first by the above algorithm), `42` is passed through without
107+
participating in a call. Situations like this may in turn provoke conversion
108+
warnings (although in this case, `char` may be promoted to `int` without
109+
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_start_idx;
26+
std::size_t arg_end_idx;
27+
28+
[[nodiscard]] constexpr auto uses_arg(std::size_t n) const {
29+
return n >= arg_start_idx and n < arg_end_idx;
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 B, std::size_t E, 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<B + Is>(
57+
std::forward<Args>(args)))...>;
58+
if constexpr (std::is_void_v<R>) {
59+
std::forward<F>(f)(get<B + Is>(std::forward<Args>(args))...);
60+
return void_t{};
61+
} else {
62+
return std::forward<F>(f)(get<B + Is>(std::forward<Args>(args))...);
63+
}
64+
}(std::make_index_sequence<E - B>{});
65+
}
66+
67+
template <typename... Fs> struct by_need {
68+
template <typename... Args>
69+
[[nodiscard]] CONSTEVAL static auto compute_call_info_impl() {
70+
auto results = std::array<call_info, sizeof...(Fs) + sizeof...(Args)>{};
71+
auto result_count = std::size_t{};
72+
73+
auto const test =
74+
[&]<std::size_t N, std::size_t I, std::size_t J>() -> bool {
75+
constexpr auto arg_base = I;
76+
constexpr auto arg_top = sizeof...(Args) - J;
77+
if constexpr (arg_top < arg_base) {
78+
return false;
79+
} else {
80+
auto const b =
81+
[&]<std::size_t... As>(std::index_sequence<As...>) -> bool {
82+
if constexpr (requires {
83+
typename std::invoke_result_t<
84+
nth_t<N, Fs...>,
85+
nth_t<arg_base + As, Args...>...>;
86+
}) {
87+
results[result_count++] = {N, arg_base, arg_top};
88+
return true;
89+
}
90+
return false;
91+
}(std::make_index_sequence<arg_top - arg_base>{});
92+
return b;
93+
}
94+
};
95+
96+
auto const inner_loop = [&]<std::size_t N, std::size_t I>() -> bool {
97+
return [&]<std::size_t... Js>(std::index_sequence<Js...>) {
98+
return (... or test.template operator()<N, I, Js>());
99+
}(std::make_index_sequence<sizeof...(Args)>{});
100+
};
101+
102+
auto const outer_loop = [&]<std::size_t N>() {
103+
return [&]<std::size_t... Is>(std::index_sequence<Is...>) -> bool {
104+
return (... or inner_loop.template operator()<N, Is>());
105+
}(std::make_index_sequence<sizeof...(Args)>{});
106+
};
107+
108+
[&]<std::size_t... Ns>(std::index_sequence<Ns...>) {
109+
(outer_loop.template operator()<Ns>(), ...);
110+
}(std::make_index_sequence<sizeof...(Fs)>{});
111+
112+
return std::pair{results, result_count};
113+
}
114+
115+
template <typename... Args>
116+
[[nodiscard]] CONSTEVAL static auto compute_call_info() {
117+
constexpr auto given_calls = [] {
118+
constexpr auto cs = compute_call_info_impl<Args...>();
119+
return truncate_array<cs.second>(cs.first);
120+
}();
121+
122+
constexpr auto extra_calls = [&] {
123+
constexpr auto cs = [&]<std::size_t... Is>(
124+
std::index_sequence<Is...>) {
125+
auto results =
126+
std::array<cbn_detail::call_info, sizeof...(Args)>{};
127+
auto unused_count = std::size_t{};
128+
for (auto i = std::size_t{}; i < sizeof...(Args); ++i) {
129+
if (std::none_of(std::cbegin(given_calls),
130+
std::cend(given_calls),
131+
[&](auto c) { return c.uses_arg(i); })) {
132+
results[unused_count++] = {sizeof...(Fs), i, i + 1};
133+
}
134+
}
135+
return std::pair{results, unused_count};
136+
}(std::make_index_sequence<sizeof...(Args)>{});
137+
return truncate_array<cs.second>(cs.first);
138+
}();
139+
140+
return concat(given_calls, extra_calls);
141+
}
142+
};
143+
} // namespace cbn_detail
144+
145+
template <tuplelike Fs, tuplelike Args>
146+
constexpr auto call_by_need(Fs &&fs, Args &&args) {
147+
constexpr auto calls =
148+
[&]<std::size_t... Is, std::size_t... Js>(std::index_sequence<Is...>,
149+
std::index_sequence<Js...>) {
150+
return cbn_detail::by_need<decltype(get<Is>(
151+
std::forward<Fs>(fs)))...>::
152+
template compute_call_info<decltype(get<Js>(
153+
std::forward<Args>(args)))...>();
154+
}(std::make_index_sequence<tuple_size_v<remove_cvref_t<Fs>>>{},
155+
std::make_index_sequence<tuple_size_v<remove_cvref_t<Args>>>{});
156+
157+
auto new_fs = [&]<std::size_t... Is>(std::index_sequence<Is...>) {
158+
return tuple{get<Is>(std::forward<Fs>(fs))..., std::identity{}};
159+
}(std::make_index_sequence<tuple_size_v<Fs>>{});
160+
161+
auto ret = [&]<std::size_t... Is>(std::index_sequence<Is...>) {
162+
return tuple{
163+
cbn_detail::invoke<calls[Is].arg_start_idx, calls[Is].arg_end_idx>(
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)