Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Function argument evaluation order vs Lambda capture evaluation order

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?

like image 633
Siler Avatar asked May 25 '18 21:05

Siler


People also ask

What is a lambda capture?

Capture clauseA lambda can introduce new variables in its body (in C++14), and it can also access, or capture, variables from the surrounding scope. A lambda begins with the capture clause. It specifies which variables are captured, and whether the capture is by value or by reference.

Which can be used to change the order of evaluation of expression?

The order of precedence can be altered by using parentheses around the parts of the mathematical expression that needs to be evaluated first.

Is C++ left to right evaluation?

Postfix expressions are evaluated from left to right. This includes functions calls and member selection expressions. Assignment expressions are evaluated from right to left. This includes compound assignments.

What is the use of lambda function in C++?

C++ Lambda expression allows us to define anonymous function objects (functors) which can either be used inline or passed as an argument. Lambda expression was introduced in C++11 for creating anonymous functors in a more convenient and concise way.


1 Answers

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.

like image 184
T.C. Avatar answered Nov 15 '22 01:11

T.C.