jacobsa
jacobsa

Reputation: 6609

What is the lifetime of a C++ method pointer expression?

I have a generic function that invokes a callable it's handed; the moral equivalent of a call to std::invoke, except it's part of a coroutine:

// Wait for some condition to be true, then invoke f with the supplied
// arguments. The caller must ensure that all references remain valid
// until the returned task is done.
template <typename F, typename Args...>
Task<void> WaitForConditionAndInvoke(F&& f, Args&&... args) {
  co_await SomeCondition();
  std::invoke(f, std::forward<Args>(args)...);
}

Because of the requirement that the input arguments remain valid, it's not always legal to pass WaitForConditionAndInvoke a temporary like a lambda (unless e.g. the returned task is directly co_awaited as part of a single expression). This is true even if using unary + to convert a lambda with no bound state to a function pointer: the function pointer is itself a temporary, and asan seems to correctly complain about using it after it's destroyed.

My question is whether using a member function pointer is legal:

struct Object {
  void SomeMethod();
};

// Some global object; we're not worried about the object's lifetime.
Object object;

Task<void> WaitForConditionAndInvokeOnGlobalObject() {
  return WaitForConditionAndInvoke(&Object::SomeMethod, &object);
}

This seems to work fine, but it's unclear to me what the lifetime of the pointer that &Object::SomeMethod evaluates to is. Is this guaranteed to be a constant expression, i.e. not a temporary? What part of the standard covers this?

Upvotes: 3

Views: 173

Answers (2)

Quuxplusone
Quuxplusone

Reputation: 27290

As the commenters say, there's no lifetime issue here. &Object::SomeMethod is a constant expression, just like 42 or &WaitForConditionAndInvokeOnGlobalObject.

You might have confused yourself by naming your type struct Object, so the expression &Object::SomeMethod kind of looks like it involves an object... but it doesn't; Object is the name of a type. There is no object involved in that expression; it's a simple compile-time constant, just like offsetof(Object, SomeDataMember) or sizeof(Object) would be.

No object involved = no lifetime issues involved.

EDIT (thus completely changing my answer): Ah, aschepler is right, you're concerned with the lifetime of the thing-referred-to-by-F&& f in

template <typename F, typename Args...>
Task<void> WaitForConditionAndInvoke(F&& f, Args&&... args) {
  co_await SomeCondition();
  std::invoke(f, std::forward<Args>(args)...);
}

which is the temporary object with value &Object::SomeMethod, which of course dies at the end of its full-expression, i.e., at the semicolon at the end of return WaitForConditionAndInvoke(&Object::SomeMethod, &object);. As soon as you hit that semicolon, all of the temporaries go out of scope and any references to them (like, that f captured in the coroutine frame) are definitely dangling.

Upvotes: -1

aschepler
aschepler

Reputation: 72431

That WaitForConditionAndInvoke coroutine will be dangerous unless every argument including the functor f refers to an object with lifetime long enough. For example, WaitForConditionAndInvoke(std::abs, 1) has undefined behavior because of the object materialized to initialize a reference with the int prvalue expression 1. There is no difference per the Standard for constant expression arguments here, although a constant expression value could help compilers implement it in a way which "works" using a dead object's known value.

To fix this, you could have your function move every rvalue argument into a local object:

// All rvalue arguments are moved from.
// The caller must make sure all lvalue arguments remain valid until
// the returned task is done.
template <typename F, typename Args...>
Task<void> WaitForConditionAndInvoke(F&& f, Args&&... args) {
  // local_f could be declared an lvalue reference or object,
  // but not an rvalue reference:
  F local_f(std::forward<F>(f));
  // Similarly, the template arguments to tuple are lvalue references
  // or object types.
  std::tuple<Args...> local_args(std::forward<Args>(args)...);
  co_await SomeCondition();
  std::apply(std::forward<F>(local_f), std::move(local_args));
}

Or to be even safer, do as std::bind does, and move/copy everything. The calling code can specify that the functor or functor argument(s) should be treated as a reference with no move or copy using std::ref or std::cref. In fact, that implementation is just:

// All arguments are moved or copied from.
// A std::reference_wrapper can be used to skip the move/copy of
// the referenced object.
template <typename F, typename Args...>
Task<void> WaitForConditionAndInvoke(F&& f, Args&&... args) {
  auto bound_f = std::bind(std::forward<F>(f), std::forward<Args>(args)...);
  co_await SomeCondition();
  bound_f();
}

Upvotes: 3

Related Questions