Skip to content

Commit

Permalink
Clean up final_act and finally, closes #846 (#977)
Browse files Browse the repository at this point in the history
Somewhere along the way, GSL's implementation of final_act and finally seems to have become way overthought. This PR is to re-simplify these facilities back to what C++ Core Guidelines C.30 said which is simple and clear and works. It just copies the invocable thing, and doesn't bother trying to optimize the copy. This should be fine, because we're typically passing something that's cheap to copy, often a stateless lambda.

The problem in #846 appears to be because finally looks like was originally written as a const&/&& overload (its state at the time that issue was opened)... to eliminate a copy when you invoke it with a temporary. If so, then the && was probably never intended to be a forwarder, but an rvalue reference that tripped over the horrid C++ syntax collision where a && parameter magically instead means a forwarding reference because the type happens to be a template parameter type here. So I suspect the original author was just trying to write an rvalue overload, and the forwarder that's there now was never intended at all.
  • Loading branch information
hsutter committed Oct 10, 2022
1 parent 849f7ce commit 7d49d4b
Show file tree
Hide file tree
Showing 2 changed files with 22 additions and 23 deletions.
35 changes: 12 additions & 23 deletions include/gsl/util
Expand Up @@ -65,40 +65,29 @@ template <class F>
class final_action
{
public:
static_assert(!std::is_reference<F>::value && !std::is_const<F>::value &&
!std::is_volatile<F>::value,
"Final_action should store its callable by value");
explicit final_action(const F& ff) noexcept : f{ff} { }
explicit final_action(F&& ff) noexcept : f{std::move(ff)} { }

explicit final_action(F f) noexcept : f_(std::move(f)) {}
~final_action() noexcept { if (invoke) f(); }

final_action(final_action&& other) noexcept
: f_(std::move(other.f_)), invoke_(std::exchange(other.invoke_, false))
{}
: f(std::move(other.f)), invoke(std::exchange(other.invoke, false))
{ }

final_action(const final_action&) = delete;
final_action& operator=(const final_action&) = delete;
final_action& operator=(final_action&&) = delete;

// clang-format off
GSL_SUPPRESS(f.6) // NO-FORMAT: attribute // terminate if throws
// clang-format on
~final_action() noexcept
{
if (invoke_) f_();
}
final_action(const final_action&) = delete;
void operator=(const final_action&) = delete;
void operator=(final_action&&) = delete;

This comment has been minimized.

Copy link
@beinhaerter

beinhaerter Oct 19, 2022

Contributor

Why are the return values changed to void? Why is that better? That looks suspicious.

This comment has been minimized.

Copy link
@hsutter

hsutter Oct 20, 2022

Author Collaborator

It's just my habit... they are deleted anyway, so there's no point in allowing chaining and writing void means the same thing, and is less typing, than final_action&... YMMV?

This comment has been minimized.

Copy link
@beinhaerter

beinhaerter Oct 24, 2022

Contributor

OK, understood. Technically I can follow. But I see three disadvantages.

  1. It violates C.63: Make move assignment non-virtual, take the parameter by &&, and return by non-const& and thus might trigger unneccessary warnings.
  2. It takes more brain capacity to read the code. The return type is unusual, so when reading left to right it is the first thing you read and that you wonder about. Only later you see the =delete, (need to) think about it and then understand why the return value is void.
  3. The code is not copy-paste safe. I like to have code that can be copy-pasted and is still valid. If a newby copies this code and changes the =delete to {...} he has bad code.

This comment has been minimized.

Copy link
@hsutter

hsutter Oct 26, 2022

Author Collaborator

Good point about that it violates C.63 (and C.60)... I've opened Guidleines issue 1988 for this but forgot to cross-link it, will do that now. Thanks!

This comment has been minimized.

Copy link
@JohelEGP

JohelEGP Oct 26, 2022

Contributor

Isn't it unfortunate that final_action stops being a model of std::movable due to this change?

This comment has been minimized.

Copy link
@beinhaerter

beinhaerter Oct 26, 2022

Contributor

Isn't it unfortunate that final_action stops being a model of std::movable due to this change?

It was not move assignable before Herb's change. Before and after the change it was and is =delete.


private:
F f_;
bool invoke_{true};
F f;
bool invoke = true;
};

// finally() - convenience function to generate a final_action
template <class F>
GSL_NODISCARD final_action<typename std::remove_cv<typename std::remove_reference<F>::type>::type>
finally(F&& f) noexcept
GSL_NODISCARD auto finally(F&& f) noexcept
{
return final_action<typename std::remove_cv<typename std::remove_reference<F>::type>::type>(
std::forward<F>(f));
return final_action<std::decay_t<F>>{std::forward<F>(f)};
}

// narrow_cast(): a searchable way to do narrowing casts of values
Expand Down
10 changes: 10 additions & 0 deletions tests/utils_tests.cpp
Expand Up @@ -112,6 +112,16 @@ TEST(utils_tests, finally_function_ptr)
EXPECT_TRUE(j == 1);
}

TEST(utils_tests, finally_function)
{
j = 0;
{
auto _ = finally(g);
EXPECT_TRUE(j == 0);
}
EXPECT_TRUE(j == 1);
}

TEST(utils_tests, narrow_cast)
{
int n = 120;
Expand Down

0 comments on commit 7d49d4b

Please sign in to comment.