-
Notifications
You must be signed in to change notification settings - Fork 8
Description
There are two issues with best::tap::operator[] -- a typo that prevents use, and a lifetime issue.
Here is the current implementation, for reference.
Lines 178 to 201 in 3475255
| template <typename Guard, typename Cb> | |
| constexpr auto tap<Guard, Cb>::operator[](auto&& arg) const& { | |
| return best::tap([&](auto&& arg) -> decltype(auto) { | |
| return ((BEST_FWD(arg))->**this)[BEST_FWD(arg)]; | |
| }); | |
| } | |
| template <typename Guard, typename Cb> | |
| constexpr auto tap<Guard, Cb>::operator[](auto&& arg) & { | |
| return best::tap([&](auto&& arg) -> decltype(auto) { | |
| return ((BEST_FWD(arg))->**this)[BEST_FWD(arg)]; | |
| }); | |
| } | |
| template <typename Guard, typename Cb> | |
| constexpr auto tap<Guard, Cb>::operator[](auto&& arg) const&& { | |
| return best::tap([&](auto&& arg) -> decltype(auto) { | |
| return ((BEST_FWD(arg))->*BEST_MOVE(*this))[BEST_FWD(arg)]; | |
| }); | |
| } | |
| template <typename Guard, typename Cb> | |
| constexpr auto tap<Guard, Cb>::operator[](auto&& arg) && { | |
| return best::tap([&](auto&& arg) -> decltype(auto) { | |
| return ((BEST_FWD(arg))->*BEST_MOVE(*this))[BEST_FWD(arg)]; | |
| }); | |
| } |
First
There is a name collision for arg -- the lambda argument shadows the function argument. This prevents any actual use. The solution is straightforward: change one of the names, add a few tests to prevent any future issues.
Second
We run into some weird lifetime issues here. In general, operator[] returns reference types. The decltype(auto) signature preserves the reference. This means that code like this:
best::test Tap = [](auto& t) {
// Generates a vector of multiples of n, to the requested length
best::tap multiples = [](int&& n){
return [&](int len){
best::vec<int> res;
for (int i=1; i<len; ++i) {
res.push(n * i);
}
return res;
};
};
// generates the first 10 multiples, and then selects the second
t.expect_eq((2->*multiples(10))[1], 4); // succeeds
t.expect_eq(2->*multiples(10)[1], 4); // fails
};
Will return a dangling reference to the objects on the stack of the lambda we just left:
TEST: _ZN4best8tap_test3TapE ]
failed expect_eq() at best/func/tap_test.cc:45
expected these values to be equal:
1696621669
4
There are scenarios where this behavior is valid. If the tap produces, say, a reference to a static object, than references into that container are presumably also static, and valid. However, I believe the average case is a stack-scoped return from.
The documentation alludes to oddities with this behavior, but stops short of fully describing the behavior:
->*also binds stronger thanoperator()andoperator[]. However, the vanilla tap,best::tap, overloadsoperator()andoperator[]to give the illusion otherwise:foo->*my_tap(bar)will be parsed asoperator->*(foo, my_tap.operator()(bar)): using()or[]on a tap will "bind" those arguments, returning a new tap that "does what you expect", such thatfoo->*my_tap(bar)is almost(foo->*my_tap)(bar).
As far as resolution, I am happy to submit a PR, and have two proposed resolutions:
Option 1: Leave it alone
In short, just fix the argument naming, and add a sentence in the documentation for best::tap explaining the behavior outlined above. This maintains maximal flexibility for the user, even if it is a bit of a foot-gun.
Option 2: Prefer return-by-value
We can also just update the return types of the function to return a best::unref<decltype(/* the entire function body */)>. Returning-by-value, I believe, will present a more ergonomic behavior for library users. It does, however, introduce 'hidden' costs(an implicit copy constructor call), and prevent more advanced users for intentionally returning references.
I will submit a PR for whichever option you prefer, @mcy.