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
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ target_sources(
include/stdx/bitset.hpp
include/stdx/byterator.hpp
include/stdx/cached.hpp
include/stdx/call_by_need.hpp
include/stdx/compiler.hpp
include/stdx/concepts.hpp
include/stdx/ct_conversions.hpp
Expand Down
128 changes: 128 additions & 0 deletions docs/call_by_need.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@

== `call_by_need.hpp`

`call_by_need` is a function that takes a xref:tuple.adoc#_tuple_hpp[tuple] of
functions and a tuple of arguments, and applies the functions to the arguments
as needed. It returns a tuple of results, with any unused arguments forwarded as
if by a call to
https://en.cppreference.com/w/cpp/utility/functional/identity.html[`std::identity`].

NOTE: `call_by_need` is available only in C++20 and later.

This is best explained with examples. The simplest example is when arguments
match up with functions exactly, unambiguously and without conversions:
[source,cpp]
----
// using strongly typed arguments for clarity
template <auto> struct arg {};

auto funcs = stdx::tuple{[] (arg<0>) { return 17; },
[] (arg<1>) { return 42; }};
auto args = stdx::tuple{arg<0>{}, arg<1>{}};

auto r = stdx::call_by_need(funcs, args); // tuple{17, 42}
----

If multiple functions take the same argument, that works too:
[source,cpp]
----
auto funcs = stdx::tuple{[] (arg<0>) { return 17; },
[] (arg<0>) { return 42; }};
auto args = stdx::tuple{arg<0>{}};

auto r = stdx::call_by_need(funcs, args); // tuple{17, 42}
----

If arguments are unused, they are treated as if `std::identity` was appended to
the set of functions:
[source,cpp]
----
auto funcs = stdx::tuple{[] (arg<0>) { return 17; }};
auto args = stdx::tuple{arg<0>{}, arg<1>{}}; // arg<1>{} is unused

auto r = stdx::call_by_need(funcs, args); // tuple{17, arg<1>{}}
----

Arguments and functions do not have to be in corresponding order; the results will be
in the same order as the functions.
[source,cpp]
----
auto funcs = stdx::tuple{[] (arg<0>) { return 17; },
[] (arg<1>) { return 42; }};
auto args = stdx::tuple{arg<2>{}, arg<1>{}, arg<0>{}}; // arg<2>{} is unused

auto r = stdx::call_by_need(funcs, args); // tuple{17, 42, arg<2>{}}
----

Functions taking multiple arguments require them to be contiguous in the
argument set, but they may overlap:
[source,cpp]
----
auto funcs = stdx::tuple{[] (arg<0>, arg<1>) { return 17; },
[] (arg<1>, arg<2>) { return 42; }};
auto args = stdx::tuple{arg<0>{}, arg<1>{}, arg<2>{}}; // arg<1>{} is used twice

auto r = stdx::call_by_need(funcs, args); // tuple{17, 42}
----

Functions returning `void` are called, but the result cannot contain `void` of course:
[source,cpp]
----
auto funcs = stdx::tuple{[] (arg<0>) { /* called but returns void */ },
[] (arg<1>) { return 42; }};
auto args = stdx::tuple{arg<0>{}, arg<1>{}};

auto r = stdx::call_by_need(funcs, args); // tuple{42}
----

Functions with default arguments will have them provided if possible:
[source,cpp]
----
auto funcs = stdx::tuple{[] (arg<0>, int i = 17) { return i; }};

auto r1 = stdx::call_by_need(funcs, stdx::tuple{arg<0>{}}); // tuple{17} (using default arg)
auto r2 = stdx::call_by_need(funcs, stdx::tuple{arg<0>{}, 18}); // tuple{18} (using provided arg)
----

If a call cannot be made, it's a compile-time error:
[source,cpp]
----
auto funcs = stdx::tuple{[] (int) {}};

auto r = stdx::call_by_need(funcs, stdx::tuple{}); // error! (no argument for int parameter)
// static_assert: call_by_need could not find calls for all given functions
----


The actual algorithm implemented by `call_by_need` is as follows:

1. For each function:
* Try to call it with all arguments `0 ... N-1`.
* If that fails, try to call it with arguments `0 ... N-2`, etc until a call succeeds.
* If all such calls fail, repeat with arguments `1 ... N-1`, etc.
2. The results of the discovered well-formed calls (with `void` filtered out) become the `tuple` of results.
3. Any unused arguments are appended to the `tuple` of results, keeping their original order stable.

NOTE: The elements of the results `tuple` must be movable.

CAUTION: The calls made by `call_by_need` are subject to the usual C++ argument
conversion rules.

The following does not call either function with the second given argument
(`42`) even though it looks like there is a correspondence between functions and
arguments:

[source,cpp]
----
auto funcs = stdx::tuple{[] (char) { return 17; },
[] (int) { return 18; }};
auto args = stdx::tuple{'a', 42};

auto r = stdx::call_by_need(funcs, args); // tuple{17, 18, 42}
----

Because the second function can be called with `'a'` (and that call possibility
is found first by the above algorithm), `42` is passed through without
participating in a call. Situations like this may in turn provoke conversion
warnings (although in this case, `char` may be promoted to `int` without
warning).
1 change: 1 addition & 0 deletions docs/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ include::bit.adoc[]
include::bitset.adoc[]
include::byterator.adoc[]
include::cached.adoc[]
include::call_by_need.adoc[]
include::compiler.adoc[]
include::concepts.adoc[]
include::ct_conversions.adoc[]
Expand Down
1 change: 1 addition & 0 deletions docs/intro.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ The following headers are available:
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/bitset.hpp[`bitset.hpp`]
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/byterator.hpp[`byterator.hpp`]
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/cached.hpp[`cached.hpp`]
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/call_by_need.hpp[`call_by_need.hpp`]
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/compiler.hpp[`compiler.hpp`]
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/concepts.hpp[`concepts.hpp`]
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/ct_conversions.hpp[`ct_conversions.hpp`]
Expand Down
174 changes: 174 additions & 0 deletions include/stdx/call_by_need.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
#pragma once

#include <stdx/compiler.hpp>
#include <stdx/ct_conversions.hpp>
#include <stdx/tuple.hpp>
#include <stdx/tuple_algorithms.hpp>
#include <stdx/type_traits.hpp>

#include <algorithm>
#include <array>
#include <cstddef>
#include <functional>
#include <iterator>
#include <type_traits>
#include <utility>

template <auto...> struct undef_v;
template <typename...> struct undef_t;

namespace stdx {
inline namespace v1 {
namespace cbn_detail {
struct call_info {
std::size_t fn_idx;
std::size_t arg_base;
std::size_t arg_len;

[[nodiscard]] constexpr auto uses_arg(std::size_t n) const {
return n >= arg_base and n < arg_base + arg_len;
}
};

template <std::size_t R, typename T, std::size_t N>
CONSTEVAL auto truncate_array(std::array<T, N> const &arr) {
return [&]<std::size_t... Is>(std::index_sequence<Is...>) {
return std::array<T, sizeof...(Is)>{arr[Is]...};
}(std::make_index_sequence<R>{});
}

template <typename T, std::size_t N, std::size_t M>
CONSTEVAL auto concat(std::array<T, N> const &a1, std::array<T, M> const &a2)
-> std::array<T, N + M> {
std::array<T, N + M> result{};
auto it = std::copy(std::cbegin(a1), std::cend(a1), std::begin(result));
std::copy(std::cbegin(a2), std::cend(a2), it);
return result;
}

struct void_t {};
template <typename T>
using is_nonvoid_t = std::bool_constant<not std::is_same_v<T, void_t>>;

template <std::size_t Base, std::size_t Len, typename F, typename Args>
constexpr auto invoke(F &&f, Args &&args) -> decltype(auto) {
return [&]<std::size_t... Is>(
std::index_sequence<Is...>) -> decltype(auto) {
using R = std::invoke_result_t<F, decltype(get<Base + Is>(
std::forward<Args>(args)))...>;
if constexpr (std::is_void_v<R>) {
std::forward<F>(f)(get<Base + Is>(std::forward<Args>(args))...);
return void_t{};
} else {
return std::forward<F>(f)(
get<Base + Is>(std::forward<Args>(args))...);
}
}(std::make_index_sequence<Len>{});
}

template <typename... Fs> struct by_need {
template <typename... Args>
[[nodiscard]] CONSTEVAL static auto compute_call_info_impl() {
auto results = std::array<call_info, sizeof...(Fs) + sizeof...(Args)>{};
auto result_count = std::size_t{};

auto const test =
[&]<std::size_t N, std::size_t Base, std::size_t Len>() -> bool {
return [&]<std::size_t... Is>(std::index_sequence<Is...>) -> bool {
if constexpr (requires {
typename std::invoke_result_t<
nth_t<N, Fs...>,
nth_t<Base + Is, Args...>...>;
}) {
results[result_count++] = {N, Base, Len};
return true;
}
return false;
}(std::make_index_sequence<Len>{});
};

auto const inner_loop = [&]<std::size_t N, std::size_t Base>() -> bool {
constexpr auto max_len = sizeof...(Args) - Base;
return [&]<std::size_t... Ls>(std::index_sequence<Ls...>) {
return (... or
test.template operator()<N, Base, max_len - Ls>());
}(std::make_index_sequence<max_len + 1>{});
};

auto const outer_loop = [&]<std::size_t N>() {
return [&]<std::size_t... Bs>(std::index_sequence<Bs...>) -> bool {
// if there are no args, still check the nullary call
return ((sizeof...(Bs) == 0 and
test.template operator()<N, 0, 0>()) or
... or inner_loop.template operator()<N, Bs>());
}(std::make_index_sequence<sizeof...(Args)>{});
};

[&]<std::size_t... Ns>(std::index_sequence<Ns...>) {
(outer_loop.template operator()<Ns>(), ...);
}(std::make_index_sequence<sizeof...(Fs)>{});

return std::pair{results, result_count};
}

template <typename... Args>
[[nodiscard]] CONSTEVAL static auto compute_call_info() {
constexpr auto given_calls = [] {
constexpr auto cs = compute_call_info_impl<Args...>();
return truncate_array<cs.second>(cs.first);
}();
static_assert(
std::size(given_calls) == sizeof...(Fs),
"call_by_need could not find calls for all the given functions");

constexpr auto extra_calls = [&] {
constexpr auto cs = [&]<std::size_t... Is>(
std::index_sequence<Is...>) {
auto results =
std::array<cbn_detail::call_info, sizeof...(Args)>{};
auto unused_count = std::size_t{};
if constexpr (sizeof...(Args) > 0) {
for (auto i = std::size_t{}; i < sizeof...(Args); ++i) {
if (std::none_of(std::cbegin(given_calls),
std::cend(given_calls), [&](auto c) {
return c.uses_arg(i);
})) {
results[unused_count++] = {sizeof...(Fs), i, 1};
}
}
}
return std::pair{results, unused_count};
}(std::make_index_sequence<sizeof...(Args)>{});
return truncate_array<cs.second>(cs.first);
}();

return concat(given_calls, extra_calls);
}
};
} // namespace cbn_detail

template <tuplelike Fs, tuplelike Args>
constexpr auto call_by_need(Fs &&fs, Args &&args) {
constexpr auto calls =
[&]<std::size_t... Is, std::size_t... Js>(std::index_sequence<Is...>,
std::index_sequence<Js...>) {
return cbn_detail::by_need<decltype(get<Is>(
std::forward<Fs>(fs)))...>::
template compute_call_info<decltype(get<Js>(
std::forward<Args>(args)))...>();
}(std::make_index_sequence<tuple_size_v<remove_cvref_t<Fs>>>{},
std::make_index_sequence<tuple_size_v<remove_cvref_t<Args>>>{});

auto new_fs = [&]<std::size_t... Is>(std::index_sequence<Is...>) {
return tuple{get<Is>(std::forward<Fs>(fs))..., std::identity{}};
}(std::make_index_sequence<tuple_size_v<Fs>>{});

auto ret = [&]<std::size_t... Is>(std::index_sequence<Is...>) {
return tuple{cbn_detail::invoke<calls[Is].arg_base, calls[Is].arg_len>(
get<calls[Is].fn_idx>(std::move(new_fs)),
std::forward<Args>(args))...};
}(std::make_index_sequence<calls.size()>{});
return stdx::filter<cbn_detail::is_nonvoid_t>(std::move(ret));
}
} // namespace v1
} // namespace stdx
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ target_compile_definitions(
if(${CMAKE_CXX_STANDARD} GREATER_EQUAL 20)
add_tests(
FILES
call_by_need
ct_format
ct_string
env
Expand Down
Loading