https://www.cppstories.com/2022/tuple-iteration-apply/ C++ Stories Stay up-to-date with Modern C++ Toggle navigation C++ Stories * * Start Here * Author * Resources * Archives * Privacy * Premium * My Book [ ] [Search] [cpp17indet] Last Update: 14 February 2022 C++ Templates: How to Iterate through std::tuple: std::apply and More [tuple_iter] Table of Contents * std:apply approach + The first approach - working? * Making it more generic * On std::decay and remove ref * Generic std::apply version * Summary In the previous article on the tuple iteration, we covered the basics. As a result, we implemented a function template that took a tuple and could nicely print it to the output. There was also a version with operator <<. Today we can go further and see some other techniques. The first one is with std::apply from C++17, a helper function for tuples. Today's article will also cover some strategies to make the iteration more generic and handle custom callable objects, not just printing. This is the second part of the small series. See the first article here where we discuss the basics. std:apply approach A handy helper for std::tuple is the std::apply function template that came in C++17. It takes a tuple and a callable object and then invokes this callable with parameters fetched from the tuple. Here's an example: #include #include int sum(int a, int b, int c) { return a + b + c; } void print(std::string_view a, std::string_view b) { std::cout << "(" << a << ", " << b << ")\n"; } int main() { std::tuple numbers {1, 2, 3}; std::cout << std::apply(sum, numbers) << '\n'; std::tuple strs {"Hello", "World"}; std::apply(print, strs); } Play @Compiler Explorer As you can see, std::apply takes sum or print functions and then "expands" tuples and calls those functions with appropriate arguments. Here's a diagram showing how it works: [apply_diag] Ok, but how does it relate to our problem? The critical thing is that std::apply hides all index generation and calls to std::get<>. That's why we can replace our printing function with std::apply and then don't use index_sequence. The first approach - working? The first approach that came to my mind was the following - create a variadic function template that takes Args... and pass it to std::apply: template void printImpl(const Args&... tupleArgs) { size_t index = 0; auto printElem = [&index](const auto& x) { if (index++ > 0) std::cout << ", "; std::cout << x; }; (printElem(tupleArgs), ...); } template void printTupleApplyFn(const std::tuple& tp) { std::cout << "("; std::apply(printImpl, tp); std::cout << ")"; } Looks... fine... right? The problem is that it doesn't compile :) GCC or Clang generates some general error which boils down to the following line: candidate template ignored: couldn't infer template argument '_Fn But how? Why cannot the compiler get the proper template parameters for printImpl? The problem lies in the fact that out printImpl is a variadic function template, so the compiler has to instantiate it. The instantiation doesn't happen when we call std::apply, but inside std::apply. The compiler doesn't know how the callable object will be called when we call std::apply, so it cannot perform the template deduction at this stage. We can help the compiler and pass the arguments: #include #include template void printImpl(const Args&... tupleArgs) { size_t index = 0; auto printElem = [&index](const auto& x) { if (index++ > 0) std::cout << ", "; std::cout << x; }; (printElem(tupleArgs), ...); } template void printTupleApplyFn(const std::tuple& tp) { std::cout << "("; std::apply(printImpl, tp); // << std::cout << ")"; } int main() { std::tuple tp { 10, 20, 3.14}; printTupleApplyFn(tp); } Play @Compiler Explorer. In the above example, we helped the compiler to create the requested instantiation, so it's happy to pass it to std::apply. But there's another technique we can do. How about helper callable type? struct HelperCallable { template void operator()(const Args&... tupleArgs) { size_t index = 0; auto printElem = [&index](const auto& x) { if (index++ > 0) std::cout << ", "; std::cout << x; }; (printElem(tupleArgs), ...); } }; template void printTupleApplyFn(const std::tuple& tp) { std::cout << "("; std::apply(HelperCallable(), tp); std::cout << ")"; } Can you see the difference? Now, what we do, we only pass a HelperCallable object; it's a concrete type so that the compiler can pass it without any issues. No template parameter deduction happens. And then, at some point, the compiler will call HelperCallable(args...), which invokes operator() for that struct. And it's now perfectly fine, and the compiler can deduce the types. In other words, we deferred the problem. So we know that the code works fine with a helper callable type... so how about a lambda? #include #include template void printTupleApply(const TupleT& tp) { std::cout << "("; std::apply([](const auto&... tupleArgs) { size_t index = 0; auto printElem = [&index](const auto& x) { if (index++ > 0) std::cout << ", "; std::cout << x; }; (printElem(tupleArgs), ...); }, tp ) std::cout << ")"; } int main() { std::tuple tp { 10, 20, 3.14, 42, "hello"}; printTupleApply(tp); } Play @Compiler Explorer Also works! I also simplified the template parameters to just template . As you can see, we have a lambda inside a lambda. It's similar to our custom type with operator(). You can also have a look at the transformation through C++ Insights: this link Making it more generic So far we focused on printing tuple elements. So we had a "fixed" function that was called for each argument. To go further with our ideas, let's try to implement a function that takes a generic callable object. For example: std::tuple tp { 10, 20, 30.0 }; printTuple(tp); for_each_tuple(tp, [](auto&& x){ x*=2; }); printTuple(tp); Let's start with the approach with index sequence: template void for_each_tuple_impl(TupleT&& tp, Fn&& fn, std::index_sequence) { (fn(std::get(std::forward(tp))), ...); } template >> void for_each_tuple(TupleT&& tp, Fn&& fn) { for_each_tuple_impl(std::forward(tp), std::forward(fn), std::make_index_sequence{}); } What happens here? First, the code uses universal references (forwarding references) to pass tuple objects. This is needed to support all kinds of use cases - especially if the caller wants to modify the values inside the tuple. That's why we need to use std::forward in all places. But why did I use remove_cvref_t? On std::decay and remove ref As you can see in my code I used: std::size_t TupSize = std::tuple_size_v> This is a new helper type from the C++20 trait that makes sure we get a "real" type from the type we get through universal reference. Before C++20, you can often find std::decay used or std::remove_reference. Here's a good summary from a question about tuple iteration link to Stackoverflow: As T&& is a forwarding reference, T will be tuple<...>& or tuple <...> const& when an lvalue is passed in; but std::tuple_size is only specialized for tuple<...>, so we must strip off the reference and possible const. Prior to C++20's addition of std::remove_cvref_t, using decay_t was the easy (if overkill) solution. Generic std::apply version We discussed an implementation with index sequence; we can also try the same with std::apply. Can it yield simpler code? Here's my try: template void for_each_tuple2(TupleT&& tp, Fn&& fn) { std::apply ( [&fn](auto&& ...args) { (fn(args), ...); }, std::forward(tp) ); } Look closer, I forgot to use std::forward when calling fn! We can solve this by using template lambdas available in C++20: template void for_each_tuple2(TupleT&& tp, Fn&& fn) { std::apply ( [&fn](T&& ...args) { (fn(std::forward(args)), ...); }, std::forward(tp) ); } Play @Compiler Explorer Additionally, if you want to stick to C++17, you can apply decltype on the arguments: template void for_each_tuple2(TupleT&& tp, Fn&& fn) { std::apply ( [&fn](auto&& ...args) { (fn(std::forward(args)), ...); }, std::forward(tp) ); } Play with code @Compiler Explorer. Summary It was a cool story, and I hope you learned a bit about templates. The background task was to print tuples elements and have a way to transform them. During the process, we went through variadic templates, index sequence, template argument deduction rules and tricks, std::apply, and removing references. I'm happy to discuss changes and improvements. Let me know in the comments below the article about your ideas. See the part one here: C++ Templates: How to Iterate through std::tuple: the Basics - C++ Stories. References: * Effective Modern C++ by Scott Meyers * C++ Templates: The Complete Guide (2nd Edition) by David Vandevoorde, Nicolai M. Josuttis, Douglas Gregor I've prepared a valuable bonus if you're interested in Modern C++! Learn all major features of recent C++ Standards! Check it out here: Download a free copy of C++20/C++17 Ref Cards! Similar Articles: * C++ Templates: How to Iterate through std::tuple: the Basics * constexpr vector and string in C++20 and One Big Limitation * C++ Return: std::any, std::optional, or std::variant? * C++20 Oxymoron: constexpr virtual * 12 Different Ways to Filter Containers in Modern C++ Tags: cpp, cpp17, cpp20, standard library, templates, Table of Contents * std:apply approach + The first approach - working? * Making it more generic * On std::decay and remove ref * Generic std::apply version * Summary Join Patreon C++17 In Detail C++ Lambda Story Recent Articles C++ Templates: How to Iterate through std::tuple: std::apply and More C++ Templates: How to Iterate through std::tuple: the Basics 20 Smaller yet Handy C++20 Features --------------------------------------------------------------------- 1. 2. C++ Templates: How to Iterate through std::tuple: the Basics >> (c) 2011-2022, Bartlomiej Filipek Disclaimer: Any opinions expressed herein are in no way representative of those of my employers. All data and information provided on this site is for informational purposes only. I try to write complete and accurate articles, but the web-site will not be liable for any errors, omissions, or delays in this information or any losses, injuries, or damages arising from its display or use. This site contains ads or referral links, which provide me with a commission. Thank you for your understanding. Built on the Hugo Platform!