3
\$\begingroup\$

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.

\$\endgroup\$
3
\$\begingroup\$

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.
\$\endgroup\$
7
  • \$\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\$ Feb 6 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\$ Feb 6 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\$ Feb 6 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\$ Feb 6 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 with constexpr. IIRC, the lambda can also be declared constexpr, but I'm really walking on thin ice now... \$\endgroup\$ Feb 6 at 16:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.