diff --git a/CMakeLists.txt b/CMakeLists.txt index 257baae..9a5b76d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/docs/call_by_need.adoc b/docs/call_by_need.adoc new file mode 100644 index 0000000..ae8dc88 --- /dev/null +++ b/docs/call_by_need.adoc @@ -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 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). diff --git a/docs/index.adoc b/docs/index.adoc index 321568a..efd3605 100644 --- a/docs/index.adoc +++ b/docs/index.adoc @@ -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[] diff --git a/docs/intro.adoc b/docs/intro.adoc index fccb96a..90f22ae 100644 --- a/docs/intro.adoc +++ b/docs/intro.adoc @@ -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`] diff --git a/include/stdx/call_by_need.hpp b/include/stdx/call_by_need.hpp new file mode 100644 index 0000000..2b810c9 --- /dev/null +++ b/include/stdx/call_by_need.hpp @@ -0,0 +1,174 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +template struct undef_v; +template 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 +CONSTEVAL auto truncate_array(std::array const &arr) { + return [&](std::index_sequence) { + return std::array{arr[Is]...}; + }(std::make_index_sequence{}); +} + +template +CONSTEVAL auto concat(std::array const &a1, std::array const &a2) + -> std::array { + std::array 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 +using is_nonvoid_t = std::bool_constant>; + +template +constexpr auto invoke(F &&f, Args &&args) -> decltype(auto) { + return [&]( + std::index_sequence) -> decltype(auto) { + using R = std::invoke_result_t( + std::forward(args)))...>; + if constexpr (std::is_void_v) { + std::forward(f)(get(std::forward(args))...); + return void_t{}; + } else { + return std::forward(f)( + get(std::forward(args))...); + } + }(std::make_index_sequence{}); +} + +template struct by_need { + template + [[nodiscard]] CONSTEVAL static auto compute_call_info_impl() { + auto results = std::array{}; + auto result_count = std::size_t{}; + + auto const test = + [&]() -> bool { + return [&](std::index_sequence) -> bool { + if constexpr (requires { + typename std::invoke_result_t< + nth_t, + nth_t...>; + }) { + results[result_count++] = {N, Base, Len}; + return true; + } + return false; + }(std::make_index_sequence{}); + }; + + auto const inner_loop = [&]() -> bool { + constexpr auto max_len = sizeof...(Args) - Base; + return [&](std::index_sequence) { + return (... or + test.template operator()()); + }(std::make_index_sequence{}); + }; + + auto const outer_loop = [&]() { + return [&](std::index_sequence) -> bool { + // if there are no args, still check the nullary call + return ((sizeof...(Bs) == 0 and + test.template operator()()) or + ... or inner_loop.template operator()()); + }(std::make_index_sequence{}); + }; + + [&](std::index_sequence) { + (outer_loop.template operator()(), ...); + }(std::make_index_sequence{}); + + return std::pair{results, result_count}; + } + + template + [[nodiscard]] CONSTEVAL static auto compute_call_info() { + constexpr auto given_calls = [] { + constexpr auto cs = compute_call_info_impl(); + return truncate_array(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::index_sequence) { + auto results = + std::array{}; + 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{}); + return truncate_array(cs.first); + }(); + + return concat(given_calls, extra_calls); + } +}; +} // namespace cbn_detail + +template +constexpr auto call_by_need(Fs &&fs, Args &&args) { + constexpr auto calls = + [&](std::index_sequence, + std::index_sequence) { + return cbn_detail::by_need( + std::forward(fs)))...>:: + template compute_call_info( + std::forward(args)))...>(); + }(std::make_index_sequence>>{}, + std::make_index_sequence>>{}); + + auto new_fs = [&](std::index_sequence) { + return tuple{get(std::forward(fs))..., std::identity{}}; + }(std::make_index_sequence>{}); + + auto ret = [&](std::index_sequence) { + return tuple{cbn_detail::invoke( + get(std::move(new_fs)), + std::forward(args))...}; + }(std::make_index_sequence{}); + return stdx::filter(std::move(ret)); +} +} // namespace v1 +} // namespace stdx diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 07ce849..0b5bc68 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -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 diff --git a/test/call_by_need.cpp b/test/call_by_need.cpp new file mode 100644 index 0000000..98635f0 --- /dev/null +++ b/test/call_by_need.cpp @@ -0,0 +1,199 @@ +#include "detail/tuple_types.hpp" + +#include +#include + +#include + +namespace { +template struct arg_t {}; +template constexpr auto arg = arg_t{}; + +template +constexpr auto operator==(arg_t, arg_t) -> bool { + if constexpr (std::is_same_v, + stdx::remove_cvref_t>) { + return V1 == V2; + } + return false; +} +} // namespace + +TEST_CASE("single function, exact args", "[call_by_need]") { + constexpr auto r = stdx::call_by_need( + stdx::tuple{[](arg_t<0>) { return 17; }}, stdx::tuple{arg<0>}); + STATIC_REQUIRE(std::is_same_v const>); + STATIC_REQUIRE(get<0>(r) == 17); +} + +TEST_CASE("single function, drop void", "[call_by_need]") { + auto called = 0; + auto const r = stdx::call_by_need(stdx::tuple{[&](arg_t<0>) { ++called; }}, + stdx::tuple{arg<0>}); + STATIC_REQUIRE(std::is_same_v const>); + CHECK(called == 1); +} + +TEST_CASE("single function, extra args (beginning)", "[call_by_need]") { + constexpr auto r = stdx::call_by_need(stdx::tuple{[](arg_t<1>) {}}, + stdx::tuple{arg<0>, arg<1>}); + STATIC_REQUIRE(std::is_same_v> const>); +} + +TEST_CASE("single function, extra args (end)", "[call_by_need]") { + constexpr auto r = stdx::call_by_need(stdx::tuple{[](arg_t<0>) {}}, + stdx::tuple{arg<0>, arg<1>}); + STATIC_REQUIRE(std::is_same_v> const>); +} + +TEST_CASE("multi function, exact args", "[call_by_need]") { + constexpr auto r = stdx::call_by_need( + stdx::tuple{[](arg_t<0>) { return 17; }, [](arg_t<1>) { return 18; }}, + stdx::tuple{arg<0>, arg<1>}); + STATIC_REQUIRE(std::is_same_v const>); + STATIC_REQUIRE(get<0>(r) == 17); + STATIC_REQUIRE(get<1>(r) == 18); +} + +TEST_CASE("multi function, drop void", "[call_by_need]") { + auto called = 0; + auto const r = stdx::call_by_need( + stdx::tuple{[](arg_t<0>) { return 17; }, [&](arg_t<1>) { ++called; }}, + stdx::tuple{arg<0>, arg<1>}); + STATIC_REQUIRE(std::is_same_v const>); + CHECK(get<0>(r) == 17); + CHECK(called == 1); +} + +TEST_CASE("multi function, extra args (beginning)", "[call_by_need]") { + constexpr auto r = stdx::call_by_need( + stdx::tuple{[](arg_t<1>) { return 17; }, [](arg_t<2>) { return 18; }}, + stdx::tuple{arg<0>, arg<1>, arg<2>}); + STATIC_REQUIRE( + std::is_same_v> const>); + STATIC_REQUIRE(get<0>(r) == 17); + STATIC_REQUIRE(get<1>(r) == 18); +} + +TEST_CASE("multi function, extra args (middle)", "[call_by_need]") { + constexpr auto r = stdx::call_by_need( + stdx::tuple{[](arg_t<0>) { return 17; }, [](arg_t<2>) { return 18; }}, + stdx::tuple{arg<0>, arg<1>, arg<2>}); + STATIC_REQUIRE( + std::is_same_v> const>); + STATIC_REQUIRE(get<0>(r) == 17); + STATIC_REQUIRE(get<1>(r) == 18); +} + +TEST_CASE("multi function, extra args (end)", "[call_by_need]") { + constexpr auto r = stdx::call_by_need( + stdx::tuple{[](arg_t<0>) { return 17; }, [](arg_t<1>) { return 18; }}, + stdx::tuple{arg<0>, arg<1>, arg<2>}); + STATIC_REQUIRE( + std::is_same_v> const>); + STATIC_REQUIRE(get<0>(r) == 17); + STATIC_REQUIRE(get<1>(r) == 18); +} + +TEST_CASE("multi function, same args", "[call_by_need]") { + constexpr auto r = stdx::call_by_need( + stdx::tuple{[](arg_t<0>) { return 17; }, [](arg_t<0>) { return 18; }}, + stdx::tuple{arg<0>, arg<1>}); + STATIC_REQUIRE( + std::is_same_v> const>); + STATIC_REQUIRE(get<0>(r) == 17); + STATIC_REQUIRE(get<1>(r) == 18); +} + +TEST_CASE("multi function, overlapping args", "[call_by_need]") { + constexpr auto r = + stdx::call_by_need(stdx::tuple{[](arg_t<0>, arg_t<1>) { return 17; }, + [](arg_t<1>, arg_t<2>) { return 18; }}, + stdx::tuple{arg<0>, arg<1>, arg<2>}); + STATIC_REQUIRE(std::is_same_v const>); + STATIC_REQUIRE(get<0>(r) == 17); + STATIC_REQUIRE(get<1>(r) == 18); +} + +TEST_CASE("move-only arg", "[call_by_need]") { + constexpr auto r = stdx::call_by_need( + stdx::tuple{[](move_only) { return 17; }}, stdx::tuple{move_only{17}}); + STATIC_REQUIRE(std::is_same_v const>); + STATIC_REQUIRE(get<0>(r) == 17); +} + +TEST_CASE("move-only arg (by reference)", "[call_by_need]") { + auto t = stdx::tuple{move_only{1}}; + auto const r = + stdx::call_by_need(stdx::tuple{[](move_only &) { return 17; }}, t); + STATIC_REQUIRE(std::is_same_v const>); + CHECK(get<0>(r) == 17); +} + +TEST_CASE("move-only return", "[call_by_need]") { + constexpr auto r = + stdx::call_by_need(stdx::tuple{[](arg_t<0>) { return move_only{17}; }}, + stdx::tuple{arg<0>}); + STATIC_REQUIRE(std::is_same_v const>); + STATIC_REQUIRE(get<0>(r).value == 17); +} + +TEST_CASE("converted arguments", "[call_by_need]") { + auto const r = stdx::call_by_need(stdx::tuple{[](char c) { + CHECK(c == 'a'); + return 17; + }, + [](int i) { + CHECK(i == 'a'); + return 18; + }}, + stdx::tuple{'a', 42}); + STATIC_REQUIRE( + std::is_same_v const>); + CHECK(get<0>(r) == 17); + CHECK(get<1>(r) == 18); + CHECK(get<2>(r) == 42); +} + +TEST_CASE("nullary function, no args", "[call_by_need]") { + auto called = 0; + auto const r = stdx::call_by_need(stdx::tuple{[&] { + ++called; + return 17; + }}, + stdx::tuple{}); + STATIC_REQUIRE(std::is_same_v const>); + CHECK(called == 1); +} + +TEST_CASE("nullary function, passthrough arg", "[call_by_need]") { + auto called = 0; + auto const r = stdx::call_by_need(stdx::tuple{[&] { + ++called; + return 17; + }}, + stdx::tuple{arg<0>}); + STATIC_REQUIRE( + std::is_same_v> const>); + CHECK(called == 1); +} + +TEST_CASE("default arguments unfilled", "[call_by_need]") { + constexpr auto r = stdx::call_by_need( + stdx::tuple{[](int i = 17) { return i; }}, stdx::tuple{}); + STATIC_REQUIRE(std::is_same_v const>); + STATIC_REQUIRE(get<0>(r) == 17); +} + +TEST_CASE("default arguments filled", "[call_by_need]") { + constexpr auto r = stdx::call_by_need( + stdx::tuple{[](int i = 17) { return i; }}, stdx::tuple{18}); + STATIC_REQUIRE(std::is_same_v const>); + STATIC_REQUIRE(get<0>(r) == 18); +} + +TEST_CASE("no functions given", "[call_by_need]") { + constexpr auto r = stdx::call_by_need(stdx::tuple{}, stdx::tuple{17}); + STATIC_REQUIRE(std::is_same_v const>); + STATIC_REQUIRE(get<0>(r) == 17); +} diff --git a/test/fail/CMakeLists.txt b/test/fail/CMakeLists.txt index d251add..64ccc93 100644 --- a/test/fail/CMakeLists.txt +++ b/test/fail/CMakeLists.txt @@ -30,6 +30,7 @@ add_fail_tests( if(${CMAKE_CXX_STANDARD} GREATER_EQUAL 20) add_fail_tests( atomic_bool_dec + call_by_need ct_format_mismatch dynamic_span_no_ct_capacity dynamic_container_no_ct_capacity diff --git a/test/fail/call_by_need.cpp b/test/fail/call_by_need.cpp new file mode 100644 index 0000000..1a5dd52 --- /dev/null +++ b/test/fail/call_by_need.cpp @@ -0,0 +1,8 @@ +#include + +// EXPECT: call_by_need could not find calls for all the given functions + +auto main() -> int { + constexpr auto r = + stdx::call_by_need(stdx::tuple{[](int) {}}, stdx::tuple{}); +} diff --git a/usage_test/main.cpp b/usage_test/main.cpp index 1be3f43..948141e 100644 --- a/usage_test/main.cpp +++ b/usage_test/main.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include