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.
2 Answers 2
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.
-
\$\begingroup\$ we could accommodate all of these concerns, I suppose, by optionally returning another tuple. But this is not a serious idea. \$\endgroup\$user1095108– user10951082021年08月07日 07:55:46 +00:00Commented Aug 7, 2021 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 astd::array<std::variant>
and then use a transform usingstd::visit
. \$\endgroup\$Toby Speight– Toby Speight2021年08月07日 08:02:28 +00:00Commented Aug 7, 2021 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
orfalse
. \$\endgroup\$user1095108– user10951082021年08月07日 08:27:15 +00:00Commented Aug 7, 2021 at 8:27 -
\$\begingroup\$ But when you say piping
std::tuple
s is unclear, I don't see any clear/standard ways of doing this. \$\endgroup\$user1095108– user10951082021年08月07日 08:39:53 +00:00Commented Aug 7, 2021 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\$Toby Speight– Toby Speight2021年08月07日 09:44:43 +00:00Commented Aug 7, 2021 at 9:44
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::tuple
s 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.