std::apply
is a typical example of those numerous standard C++ std lib types begging to be made comfortable. Not just usable.
In its raw form, it requires a lot of typing. And a lot of typing means a lot of bugs. Made by the team of course, not you. So, I made one little proxy type aka “wrapper” that is really simple but makes std::apply
usage, much more palatable. And safe.
The Use Case
Requirement: There exists a variadic “summa” function. It simply adds together all of its arguments and returns the result. The types of arguments must be allowed to be “anything”.
I think “the ability to think abstractly” is the key. The first abstraction I will produce is this simple callable object. It is indeed recursive but does not have to be.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// invocable object // be forewarned: this is not owned by your team // it is imposed on the project struct summa_service final { // returns sum of its arguments template <typename T, typename... A> T operator()(T first, A... second) noexcept { if constexpr (sizeof...(second) > 0) { return first + this->operator()(second...); } else { return first; } } }; |
( summa()
as lambda is possible and is presented also here).
1 2 3 4 5 6 7 8 9 10 |
/* recursive generic lambda */ auto summa = [](auto first, auto ... second) { if constexpr (sizeof ... (second) > 0) { return first + summa(second ...); } else { return first; } }; |
A little Test Unit macro:
1 2 |
// Test Unit we shall use in all examples bellow #define TU(x) std::cout << "\n" << (# x) << ":\t" << (x) |
Usage is simple. But. It works for use cases only when all the arguments are of the same type.
1 2 3 4 5 6 7 8 9 10 |
// two int#s TU(summa(1,2)); // Anything than has "+" operator overloaded works using namespace std::string_literals; TU(summa("one"s,"two"s)); /* the output: summa(1,2): 3 summa("one"s,"two"s): onetwo */ |
And here is the cruel last-minute requirement, you already might have sensed: That callable object is not owned by you or your team. It is in the library your company has paid for and it can not be changed.
Not a problem says you, we will use std::apply
. But you are smart and you do not want to use std::apply
in its native form.
The Solution
OK, let me show you immediately the API, I have developed. It is not made of function calls, it is made of one abstraction.
To use the above callable object (or any other callable object), we would need first to “runtime package” it as “applicator”. And in that sense, we do not need to own it. Instead, we will use a wrapper aka “proxy” to make the whole business of using std::apply
much easier to comprehend and use. Your fellow developers aka users will never need to see the implementation, they will just use the applicator. In that sense, it is a “proxy” object.
1 2 3 4 5 |
// we have a summa( ... ) callable object // which adds together all the arguments // and returns the result // we will use it with std::apply through this object auto sumator = dbj::make_applicator(summa); |
The design and API usage philosophy is: you first make a specific “applicator” from a specific Callable object. Then you use it, wherever and whenever you need it.
The key difference vs the naked summa(...)
usage is you can use all these fancy types, std::apply
allows you to use them. Thus anything that behaves like a tuple: std::pair
, std::array
, and on top of that some more types thanks to this proxy API.
Ok, but is this really usable? How the code using it feels and looks. Read on.
Before and After
For the “before and after” comparisons, to each API example below, I have added std::apply
raw usage, on the line below.
1 2 3 4 5 6 7 8 9 10 11 |
// Remember, we use this Test Unit // #define TU(x) std::cout << "\n" << (# x) << "\n\t" << (x)) // summa can not use the pair arg, but summator can TU(sumator(std::pair(1, 2))); // std::apply naked usage TU(std::apply(summa, std::pair(1, 2))); // std::array works as expected too std::array<int, 5> a5 = { 1, 2, 3, 4, 5 }; TU(sumator(a5)); // std::apply naked usage TU(std::apply(summa, a5)); |
Perhaps not a big deal. Shorter; but just slightly shorter. Ok, how about using tuples (as authors of std::apply intended), as the primary and only argument type to be passed to std::apply
. Here is the difference.
The key requirement and its solution
1 2 3 4 5 6 7 8 |
// my way :) // passing tuple arg to the summator proxy object // here we solve the key requirement: arguments of different types // we add floats and int, not a big deal but we have not done anything // to make that work TU(sumator(2.0f, 3.0f, 1, 2)); // the std way TU(std::apply(summa, std::make_tuple(2.0f, 3.0f, 1, 2))); |
That’s a big difference in ease of usage.
Obviously one does depend on the existence of the required “+” operator for sumator
to work:
1 2 3 |
// does not compile of course, as // there is no + operator adding strings and numbers TU(sumator(2.0f, 3.0f, "one"s, "two"s)); |
Obviously, you can create the necessary operator yourself:
1 2 3 4 |
int operator + ( int i_ , std::string s_ ) { return std::stoi(s_) + i_ ; } |
Adding int and string will now work. Silly but true.
1 2 3 4 5 6 |
// now this works TU(sumator(3, 4, "1"s , "2"s)); // the output is // sumator(3, 4, "1"s , "2"s): 19 // excersize for the reader: output should be 10, not 19. // Where is the problem? |
Keep in mind this is a silly use case. It just proves the API concept. The same API will work with anything that is an invocable object. Without needing to change that object. It makes the callable object readily and truly reusable. In a non-intrusive way.
More arg types
And then there are arg types that my API and its proxy can do and that std::apply
can not do on its own:
1 2 3 |
// init list arg TU(sumator({ 1,2,3 })); // no can do -- TU(std::apply(summa, {1,2,3})); |
Initializer list’s, std::apply
simply does not let you use. And what about native arrays?
1 2 3 4 |
// native array arg int a5[]{ 1, 2, 3, 4, 5 }; TU(sumator(a5)); // no can do -- TU(std::apply(summa, a5)); |
I know std::array is rather nice, and I use it too whenever I can, but in real-life code, native arrays no one can avoid. An ocean of them native arrays in fact, all used by legacy API. Instead of writing your own transformations, feel free to use my API.
There are more perhaps convincing types of examples. What about F( a, b, F(c, d))
situation? Function result is used as one of the arguments for the same function. Let’s see:
1 2 3 4 5 6 7 8 9 10 |
// sumator as arg // again different types // 2.0f + 3.0f + (11 + 12) <-- summa(2.0, 3.0, summa(11,12)) TU(sumator(2.0,3.0, sumator(11,12) )); // naked std::apply usage is possible but rather fiddly // close your eyes :) TU(std::apply(summa, std::make_tuple(2.0f, 3.0f, std::apply(summa, std::make_pair(11, 12)) ))); |
Caveat Emptor
One can attempt this kind of easy computation with or without the help of my API. I might think if you need to use std::apply
, this API you might find rather useful.
The primary purpose of this API is to encapsulate and add value to std::apply
. That in turn improves the resiliency of your code (projects) as it will not be sprinkled with ad-hoc usage very likely in concert with some bugs.
You can even use it as a blueprint for your own proxy object that will also enforce some additional business rules. The key thing is you have it and thus you can use it.
The Free Code
Is rather simple and lovingly short. Here is the full implementation straight from the Godbolt. There is no magic.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
#include <stdlib.h> #include <array> #include <iomanip> #include <iostream> #include <string> #include <tuple> #include <utility> using std::array; // Test Unit #define TU(x) std::cout << "\n" << std::setw(40) << (#x) << ":\t" << (x) // std::to_array is C++20 or above namespace dbj { namespace detail { template <class T, std::size_t N, std::size_t... I> constexpr inline std::array<std::remove_cv_t<T>, N> to_array_impl( T(&&a)[N], std::index_sequence<I...>) noexcept { return {{std::move(a[I])...}}; } } // namespace detail template <class T, std::size_t N> constexpr inline std::array<std::remove_cv_t<T>, N> to_array( T(&&a)[N]) noexcept { return detail::to_array_impl(std::move(a), std::make_index_sequence<N>{}); } } // namespace dbj // the proxy type hiding the std::apply complexity in the back // and extending the std::apply functionality namespace dbj { template <typename INVOCABLE> class apply_helper final { // not carried arround // there is only one instance per type // made by this template inline static INVOCABLE invocable_{}; public: // apply the pair of values template <typename T1, typename T2> auto operator()(std::pair<T1, T2> pp_) noexcept { // roadmap: LOCK here return std::apply(invocable_, pp_); } // apply the tuple or args template <typename... ARGS> auto operator()(ARGS... args) noexcept { // roadmap: LOCK here auto tuple_ = std::make_tuple(args...); return std::apply(invocable_, tuple_); } // apply the native array // also takes care of init list call template <typename T, size_t N> auto operator()(const T (&array_)[N]) noexcept { // roadmap: LOCK here array<T, N> std_array = dbj::to_array(std::move(array_)); return std::apply(invocable_, std_array); } template <typename T, size_t N> auto operator()(std::array<T, N> array_) noexcept { // roadmap: LOCK here return std::apply(invocable_, array_); } }; // apply_helper // one factory method to make the instances of the above // invokable is passed by value and kept by value // inside apply_helper template <typename F> inline apply_helper<F> make_applicator(F&) noexcept { return apply_helper<F>{}; }; } // namespace dbj // invocable object struct summa_service final { // returns sum of its arguments template <typename T, typename... A> T operator()(T first, A... second) noexcept { if constexpr (sizeof...(second) > 0) { return first + this->operator()(second...); } else { return first; } } }; /////////////////////////////////////////////////////////////// // OPTIONAL TESTING int main(void) { summa_service summa; TU(summa(1, 2)); using namespace std::string_literals; TU(summa("one"s, "two"s)); auto arr = dbj::to_array({1, 2, 3}); // auto sumator = dbj::apply_helper<typeof_summa(int)>{}; auto sumator = dbj::make_applicator(summa); // summa can not use the pair arg, but summator can TU(sumator(std::pair(1, 2))); // std::apply naked usage TU(std::apply(summa, std::pair(1, 2))); // std::array works as expected too std::array<int, 5> a5 = {1, 2, 3, 4, 5}; TU(sumator(a5)); // std::apply naked usage TU(std::apply(summa, a5)); // passing tuple arg to the summator proxy object TU(sumator(2.0f, 3.0f, 1, 2)); // init list arg TU(sumator({1, 2, 3})); // native array arg int arr5[]{1, 2, 3, 4, 5}; TU(sumator(a5)); // sumator as arg // 2.0f + 3.0f + (11 + 12) <-- summa(2.0, 3.0, summa(11,12)) TU(sumator(2.0, 3.0, sumator(11, 12))); // naked std::apply usage is possible but rather fiddly // close your eyes :) TU(std::apply( summa, std::make_tuple(2.0f, 3.0f, std::apply(summa, std::make_pair(11, 12))))); return 42; } |
summator
is proxy, using std::apply
to invoke summa(...)
. The output produced is:
1 2 3 4 5 6 7 8 9 10 11 12 |
summa(1, 2): 3 summa("one"s, "two"s): onetwo sumator(std::pair(1, 2)): 3 std::apply(summa, std::pair(1, 2)): 3 sumator(a5): 15 std::apply(summa, a5): 15 sumator(2.0f, 3.0f, 1, 2): 8 sumator(3, 4, "1"s , "2"s): 19 sumator({1, 2, 3}): 6 sumator(a5): 15 sumator(2.0, 3.0, sumator(11, 12)): 28 std::apply( summa, std::make_tuple(2.0f, 3.0f, std::apply(summa, std::make_pair(11, 12)))): 28 |
To casual C++ blog readers, this might look silly. But if you find yourself you really need std::apply
, that API is probably far from silly.