2
\$\begingroup\$

This is a mythical beast we discussed in a previous question:

template <typename F, class Tuple>
constexpr void operator|(Tuple&& t, F f) noexcept(noexcept(f(std::get<0>(t))))
{
  [&]<auto ...I>(std::index_sequence<I...>) noexcept(noexcept(f(std::get<0>(t))))
  {
    (f(std::get<I>(t)), ...);
  }
  (std::make_index_sequence<std::tuple_size_v<std::remove_reference_t<Tuple>>>());
}

Usage:

std::forward_as_tuple(1, 2, 3) | [](auto&& v) { std::cout << v << std::endl; };

https://wandbox.org/permlink/WMd80R2njgpubRvi

You could pipe all sorts of things into a function, not just a tuple. Arrays, containers, variants, ..., even structs.

\$\endgroup\$
5
\$\begingroup\$

I don't see a great need for this in normal (clear, obvious) code; I believe that most C++ users would expect an ordinary function (in the pattern of std::for_each(), perhaps), rather than overloading the | operator. In the future, | become recognised as a composition operator, but that is not the case in 2021, and so it will be harder for readers to comprehend than the plain function. This is clearly a matter of opinion where reasonable folk may differ, of course, and will likely change as time passes.

I suppose it's arguably similar to | std::views::transform() - except that it can only be a sink of values, because it returns void instead of a tuple of the results. If f() returns void, that may be appropriate, but when f() is a value-returning function, we'll want access to those results (perhaps to pipe into another transform).

Why are we not perfect-forwarding the tuple elements? I'm not convinced that this will work with functions that accept (mutable) references and update the values.

No unit tests are presented. I would expect quite a large suite of tests for a template function such as this, and they should have been in the review request. I certainly wouldn't accept this code into a project unless the tests were in the same submission.

\$\endgroup\$
6
  • \$\begingroup\$ we could accommodate all of these concerns, I suppose, by optionally returning another tuple. But this is not a serious idea. \$\endgroup\$ Aug 7 at 7:55
  • \$\begingroup\$ Yes, that's what I would recommend - but only when f() applied to all the input types is non-void (remember it can have many overloads). It's probably simpler to just convert the tuple to a std::array<std::variant> and then use a transform using std::visit. \$\endgroup\$ Aug 7 at 8:02
  • \$\begingroup\$ a return value could also indicate, that a conditional pipe is desired, i.e. stop piping when the return value is true or false. \$\endgroup\$ Aug 7 at 8:27
  • \$\begingroup\$ But when you say piping std::tuples is unclear, I don't see any clear/standard ways of doing this. \$\endgroup\$ Aug 7 at 8:39
  • \$\begingroup\$ I believe that most C++ users would expect an ordinary function (in the pattern of std::for_each(), perhaps), rather than overloading the | operator. As I said in the other question, | may in future become recognised as a composition operator, but it is not so in 2021. That's clearly a matter of opinion where reasonable folk may differ, so I don't see any value in arguing the point! \$\endgroup\$ Aug 7 at 9:44
3
\$\begingroup\$

Incorrect noexcept clause

In the noexcept clause, you are only checking if the function applied to the first element of the tuple is noexcept. But overloads that handle the other elements might not be. So we need to check that they are all noexcept. The OP's code in the previous question handled this correctly.

Use concepts

The problem with your operator|() is that it matches a lot of things that it shouldn't. It would be better to make it match only std::tuples and other things that you can use std::get<>() for the first argument, and function-like things that accept the types in the tuple as the second argument.

\$\endgroup\$

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.