Function argument evaluation order vs Lambda capture evaluation order
Asked Answered
R

1

8

It seems that both order of function argument evaluation, as well as the order of lambda capture initializers, is unspecified by the C++ standard.

(See http://en.cppreference.com/w/cpp/language/lambda as well as Order of evaluation in C++ function parameters)

This causes some concern for me due to how it may interact with move semantics.

Suppose I have an object of type T that may have a copy or move constructor that throws. Then suppose I have a move-only object, such as an std::promise. Consider the following situation:

T value; // some type that potentially throws when moved or copied
promise<U> pr; // a promise whose result is some type U
future<U> fut = pr.get_future(); 

std::thread(
  [v = std::move(value), pr = std::move(pr)]() {
    try {
      // do some stuff

      pr.set_value(/* whatever */);
    }
    catch (...) { pr.set_exception(std::current_exception()); }
  }
).detach();

// return the future

Now, we have a try/catch block that is executed inside the std::thread, but we don't have any exception handling for anything that might go wrong while initializing the thread. Specifically, what can we do if the expression v = std::move(value) in the lambda capture list ends up throwing an exception? Ideally, we'd want to handle it with a try-catch block and then just call pr.set_exception(...), like this:

 try {
    std::thread(
      [v = std::move(value), pr = std::move(pr)]() {
        try {
          // do some stuff

          pr.set_value(/* whatever */);
        }
        catch (...) { pr.set_exception(std::current_exception()); }
      }
    ).detach();
  }
  catch (...) {
    pr.set_exception(std::current_exception()); 
  }

There's just one major problem: when we get to our outer catch block, we don't know if the expression pr = std::move(pr) has already been called, because we have no guarantee about the order for a list of lambda-capture initializers. So when we say pr.set_exception(...) we don't know if our promise is even valid anymore, because we don't know if the promise was move-constructed before the expression v = std::move(value) was evaluated.

So how can we handle the case where the move or copy constructor for T might throw?

The only solution I can think of - maybe - is to wrap the lambda in a call to std::bind, like this:

std::thread(
  std::bind(
    [v = std::move(value)](promise<U>& pr) {
      // ...
    },
    std::move(pr)
  )
).detach();


Here, even though we don't have any guarantee about the order of function argument evaluation either, it's my understanding that we're still guaranteed that the expression v = std::move(value) would need to be evaluated before the promise is actually move constructed, since the expression std::move(pr) doesn't actually move construct the promise - it just casts it to an R-value. The promise would only be move-constructed later, inside the call to std::bind, but not as an effect of one of the function arguments being evaluated.

However, I'm not entirely sure about this solution. I'm not sure if the standard somehow may still allow for the compiler to move-construct the promise before T is move/copy constructed.

So, does my solution using std::bind solve this problem? If not, what are some ways to solve this?

Radiometer answered 25/5, 2018 at 21:18 Comment(1)
You can stick just about anything into a std::unique_ptr to make it safely movable if that's an acceptable solution.Abscissa
R
4

Your std::bind works (bind takes arguments by reference, the closure object's initialization is sequenced before the execution of bind's body, and the move from the promise necessarily happens inside bind).

It's, however, rather pointless since std::thread's constructor can already pass along arbitrary arguments.

std::thread(
    [v = std::move(value)](promise<U> pr) {
      // ...
    },
    std::move(pr)
).detach();

Note that std::thread passes the arguments as rvalues, unlike bind.

Rodi answered 26/5, 2018 at 1:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.