About the new C ++ standards

Today I have a pretty short post. I probably wouldn’t write it, but on Habré in the comments you can quite often find the opinion that the pros are getting worse, the committee makes it unclear what is unclear why, and generally give me back my 2007th. And then such a clear example suddenly came across.







Almost exactly five years ago I wrote about how to make currying in C ++. Well, if you could write foo(bar, baz, quux)



, then you could write Curry(foo)(bar)(baz)(quux)



. Then C ++ 14 just came out and was barely supported by compilers, so the code only used C ++ 11 chips (plus a couple of crutches to simulate library functions from C ++ 14).







And then I stumbled upon this code again, and my eyes hurt just how verbose it is. Plus, I turned the calendar not so long ago and noticed that now is the year 2019, and you can see how C ++ 17 can make our life easier.







We'll see?







OK, we will see.







The original implementation, from which we will dance, looks something like this:







 template<typename F, typename... PrevArgs> class CurryImpl { const F m_f; const std::tuple<PrevArgs...> m_prevArgs; public: CurryImpl (F f, const std::tuple<PrevArgs...>& prev) : m_f { f } , m_prevArgs { prev } { } private: template<typename T> std::result_of_t<F (PrevArgs..., T)> invoke (const T& arg, int) const { return invokeIndexed (arg, std::index_sequence_for<PrevArgs...> {}); } template<typename IF> struct Invoke { template<typename... IArgs> auto operator() (IF fr, IArgs... args) -> decltype (fr (args...)) { return fr (args...); } }; template<typename R, typename C, typename... Args> struct Invoke<R (C::*) (Args...)> { R operator() (R (C::*ptr) (Args...), C c, Args... rest) { return (c.*ptr) (rest...); } R operator() (R (C::*ptr) (Args...), C *c, Args... rest) { return (c->*ptr) (rest...); } }; template<typename T, std::size_t... Is> auto invokeIndexed (const T& arg, std::index_sequence<Is...>) const -> decltype (Invoke<F> {} (m_f, std::get<Is> (m_prevArgs)..., arg)) { return Invoke<F> {} (m_f, std::get<Is> (m_prevArgs)..., arg); } template<typename T> auto invoke (const T& arg, ...) const -> CurryImpl<F, PrevArgs..., T> { return { m_f, std::tuple_cat (m_prevArgs, std::tuple<T> { arg }) }; } public: template<typename T> auto operator() (const T& arg) const -> decltype (invoke (arg, 0)) { return invoke (arg, 0); } }; template<typename F> CurryImpl<F> Curry (F f) { return { f, {} }; }
      
      





In m_f



lies the stored functor, in m_prevArgs



- the arguments stored on previous calls.







Here operator()



must determine whether it is already possible to call the saved functor, or whether it is necessary to continue to accumulate arguments, so it makes a fairly standard SFINAE using the invoke



helper. In addition, in order to call the functor (or check its callability), we cover it all with another SFINAE layer to understand how to do it (because we need to call the pointer to the member and, say, the free function in different ways), and for this we use the Invoke



helper structure, which is probably incomplete ... In short, a lot of things.







Well, this thing absolutely disgustingly works with move semantics, perfect forwarding and other words sweet to the heart of the plus sign of our time. Repairing this will be a little more difficult than necessary, since in addition to the directly solved task, there is also a bunch of code that is not quite related to it.







Well, again, in C ++ 11 there are no things like std::index_sequence



and related things, or the alias std::result_of_t



, so pure C ++ 11 code would be even harder.







So, finally, let's move on to C ++ 17.







Firstly, we do not need to specify the return type operator()



, we can write simply:







 template<typename T> auto operator() (const T& arg) const { return invoke (arg, 0); }
      
      





Technically, this is not exactly the same ("linking" is displayed in different ways), but within the framework of our task this is not essential.







In addition, we do not need to do SFINAE with our hands to check the m_f



with the stored arguments. C ++ 17 gives us two cool features: constexpr if



and std::is_invocable



. Throw out everything that we had before and write the skeleton of the new operator()



:







 template<typename T> auto operator() (const T& arg) const { if constexpr (std::is_invocable_v<F, PrevArgs..., T>) //   else //       arg }
      
      





The second branch is trivial, you can copy the code that was already:







 template<typename T> auto operator() (const T& arg) const { if constexpr (std::is_invocable_v<F, PrevArgs..., T>) //   else return CurryImpl<F, PrevArgs..., T> { m_f, std::tuple_cat (m_prevArgs, std::tuple<T> { arg }) }; }
      
      





The first branch will be more interesting. We need to call m_f



, passing all the arguments stored in m_prevArgs



, plus arg



. Fortunately, we no longer need any integer_sequence



: in C ++ 17 there is a standard library function std::apply



to call a function with arguments stored in tuple



. Only we need to put another argument ( arg



) at the end of the dummy, so we can either make std::tuple_cat



, or just unpack std::apply



'we can use the existing dummy generic lambda (another feature that appeared after C ++ 11, though not in the 17th!). In my experience, instantiating dummies is slow (in compute time, of course), so I’ll choose the second option. In the lambda itself, I need to call m_f



, and to do this correctly, I can use the library function that appeared in C ++ 17, std::invoke



, by throwing out the Invoke



helper written by hand:







 template<typename T> auto operator() (const T& arg) const { if constexpr (std::is_invocable_v<F, PrevArgs..., T>) { auto wrapper = [this, &arg] (auto&&... args) { return std::invoke (m_f, std::forward<decltype (args)> (args)..., arg); }; return std::apply (std::move (wrapper), m_prevArgs); } else return CurryImpl<F, PrevArgs..., T> { m_f, std::tuple_cat (m_prevArgs, std::tuple<T> { arg }) }; }
      
      





It is useful to note how the auto



deduced return type allows you to return values ​​of different types in different branches if constexpr



.







In any case, that is basically all. Or along with the necessary harness:







 template<typename F, typename... PrevArgs> class CurryImpl { const F m_f; const std::tuple<PrevArgs...> m_prevArgs; public: CurryImpl (F f, const std::tuple<PrevArgs...>& prev) : m_f { f } , m_prevArgs { prev } { } template<typename T> auto operator() (const T& arg) const { if constexpr (std::is_invocable_v<F, PrevArgs..., T>) { auto wrapper = [this, &arg] (auto&&... args) { return std::invoke (m_f, std::forward<decltype (args)> (args)..., arg); }; return std::apply (std::move (wrapper), m_prevArgs); } else return CurryImpl<F, PrevArgs..., T> { m_f, std::tuple_cat (m_prevArgs, std::tuple<T> { arg }) }; } }; template<typename F, typename... Args> CurryImpl<F, Args...> Curry (F f, Args&&... args) { return { f, std::forward_as_tuple (std::forward<Args> (args)...) }; }
      
      





I think this is a significant improvement over the original version. And reading is easier. Even somehow boring, no challenge .







In addition, we could also get rid of the Curry



function and use CurryImpl



directly, relying on deduction guides, but it is better to do this when we deal with perfect forwarding and the like. Which smoothly brings us ...







Now it’s completely obvious how terrible this is in terms of copying arguments, this unfortunate perfect forwarding, and the like. But more importantly, fixing it is now much easier. But we, however, will do it somehow in the next post.







Instead of a conclusion



Firstly, in C ++ 20, std::bind_front



will appear, which will cover the lion's share of my user cases in which I need such a thing. You can generally throw it away. Sad







Secondly, writing on the pros is getting easier, even if you write some kind of template code with metaprogramming. You no longer have to think about which SFINAE option to choose, how to unpack the dummy, how to call the function. Just take and write, if constexpr



, std::apply



, std::invoke



. On the one hand, it’s good; I don’t want to return to C ++ 14 or, especially, 11. On the other, it feels like a lion's layer of skills is becoming unnecessary. No, it’s still useful to be able to screw something like that on templates and understand how all this library magic works inside of you, but if you had to do this all the time, now it’s much less common. It causes some strange emotions.








All Articles