I wrote a simple piping operator in c++. I just wanted to make sure that my code was robust, modern c++ code, and made correct use of perfect forwarding.
Here's the code:
#include <concepts>
#include <type_traits>
#include <utility>
#include <vector>
#include <iostream>
#include <deque>
//Accept an object on the left hand side and a function on the right
template <typename Any, typename... Args>
inline auto operator| (const Any& obj, std::invocable<Any> auto func){
// Correct use of perfect forwarding??
return func(std::forward<const Any&>(obj));
}
//Version for a functor which accepts a reference. Do I need to cover other cases?
template <typename Any, typename... Args>
inline auto operator| (Any& obj, std::invocable<Any&> auto func){
return func(std::forward<Any&>(obj));
}
And here is how the user uses it:
int main() {
auto x = std::vector{1 , 2 , 3 , 4 , 5 , 6};
// Piping example 0
auto y = 5 | [] (int x) {return x * 2;}
| [] (int x) {return x * 3;}
;
std::cout << y << '\n';
// Piping example 1
x | [] (std::vector<int>& vec) -> std::vector<int>& {vec.push_back(7); return vec;};
std::cout << x[6];
return 0;
}
And if you want to clean up the lambda syntax (I don't mind personally) and have a cleaner, neater syntax you can do:
namespace piping {
template <typename T>
auto push_back(const T& obj) {
return [obj] (auto& vec) -> auto& {vec.push_back(obj); return vec;};;
}
}
/* ... Some code */
int main() {
auto x = std::vector{1 , 2 , 3 ,4 ,5 , 6};
// A bit cleaner...
x | piping::push_back(7);
}
Did I make correct use of modern c++ and perfect forwarding? Thanks in advance.
1 Answer 1
That seems a worthwhile objective. The syntax looks a bit clunky when we have to write lambdas inline like that, but you'll probably develop a library of useful filters (like standard Ranges ones) that look much more natural.
I don't think we've got the use of forwarding references quite right - and you'll be pleased to hear that fixing that should simplify the code a bit.
What we should do is use typename Any&&
. Although that looks like an rvalue-reference, it's actually a forwarding reference because Any
is a template parameter.
Any&&
allows the template parameter to bind to either lvalue or rvalue, so we just need the one function:
template <typename Any>
auto operator| (Any&& obj, std::invocable<Any> auto&& func) {
return func(std::forward<Any>(obj));
}
(Note also the auto&&
for func
, so it can be a mutable functor if we want).
Having written that, I'm not so convinced that I like operator|()
to be modifying its argument. I would prefer a | b
to leave a
alone, and have to write a |= b
when I intend a
to be modified in place.
Minor points:
- There are headers included but not needed for the template:
#include <vector> #include <iostream> #include <deque>
inline
is implicit for a template function.- We don't need to spell out the full return type for the "append 7" lambda in the example -
-> auto&
is easier.
-
\$\begingroup\$ Thank you for all your feedback and explaining perfect forwarding!! I had never quite understood it until now... About the "inline" keyword, the compiler (-O3) seemed to be better at optimising my code when the "inline" was there?? \$\endgroup\$SomeProgrammer– SomeProgrammer2021年02月06日 16:40:13 +00:00Commented Feb 6, 2021 at 16:40
-
\$\begingroup\$ I'm surprised by that. If the
inline
is helpful, then continue using it. I don't think it can do any harm. \$\endgroup\$Toby Speight– Toby Speight2021年02月06日 16:41:15 +00:00Commented Feb 6, 2021 at 16:41 -
\$\begingroup\$ It's worth searching the web for good articles on perfect forwarding and forwarding references. Another good search term is universal references, which is the older term for this construct. \$\endgroup\$Toby Speight– Toby Speight2021年02月06日 16:47:31 +00:00Commented Feb 6, 2021 at 16:47
-
\$\begingroup\$ yes it surprised me as well. In fact both versions of my code inlined the function, but the version with "inline" straight up calculated everything at compile - time, while the version without inline had to call the lambda. \$\endgroup\$SomeProgrammer– SomeProgrammer2021年02月06日 16:48:27 +00:00Commented Feb 6, 2021 at 16:48
-
\$\begingroup\$ OOh - perhaps it would make sense to add
constexpr
to the declaration? I'm not 100% sure about that, though - I'm just a beginner withconstexpr
. IIRC, the lambda can also be declaredconstexpr
, but I'm really walking on thin ice now... \$\endgroup\$Toby Speight– Toby Speight2021年02月06日 16:54:07 +00:00Commented Feb 6, 2021 at 16:54