Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ postfix expression undefined vs unspecified behaviour

Apologies in advance, I know the general topic of evaluation order has had a lot of SO questions on it already. However, having looked at them, I want to clarify a few specific points that I don't think amount to a duplication of anything. Suppose I have the following code:

#include <iostream>

auto myLambda(int& n)
{
    ++n;
    return [](int param) { std::cout << "param: " << param << std::endl; };
}

int main()
{
    int n{0};

    myLambda(n)(n);
    return 0;
}

The program above outputs "n: 0" when I compile it. Here we have unspecified ordering at play: it could have just as easily output "n: 1" had a different evaluation order taken place.

My questions are:

  1. What exactly is the sequencing relationship at play during the final function invocation above (i.e. the lambda-expression invocation), between the postfix expression myLambda(0), its argument n, and the subsequent function call itself?

  2. Is the above an example of undefined or unspecified behaviour - and why exactly (with reference to the standard)?

  3. If I changed the lambda code to [](int param) { std::cout << "hello" << std::endl } (i.e. made the outcome independent of its parameter and thus any evaluation order decisions, making behaviour deterministic) would the answer to 2) above still be the same?

EDIT: I've change the lambda parameter name from 'n' to 'param' because that seemed to be causing confusion.

like image 219
Smeeheey Avatar asked May 20 '16 10:05

Smeeheey


3 Answers

Ironically (since the example uses C++11 features, and other answers have been distracted by that) the logic that makes this sample have unspecified behaviour dates back to C++98, Section 5, para 4

Except where noted, the order of evaluation of operands of individual operators and subexpressions of individual expressions, and the order in which side effects take place, is unspecified. Between the previous and next sequence point a scalar object shall have its stored value modified at most once by the evaluation of an expression. Furthermore, the prior value shall be accessed only to determine the value to be stored. The requirements of this paragraph shall be met for each allowable ordering of the subexpressions of a full expression; otherwise the behavior is undefined.

Essentially the same clause exists in all C++ standards although, as noted in comment by Marc van Leeuwen, recent C++ standards no longer use the concept of sequence points. The net effect is the same: within a statement, the order or evaluation of operands of operators and subexpressions of individual expressions remains unspecified.

The unspecified behaviour occurs because an expression n is evaluated twice in the statement

myLambda(n)(n); 

One evaluation of the expression n (to obtain a reference) is associated with the first (n) and another evaluation of an expression n (to obtain a value) is associated with the second (n). The order of evaluation of those two expressions (even though they are, optically, both n) is unspecified.

Similar clauses exist in ALL C++ standards, and have the same result - unspecified behaviour on the statement myLambda(n)(n), regardless of how myLambda() implemented

For example, myLambda() could be implemented in C++98 (and all later C++ standards, including C++11 and later) like this

 class functor  {       functor() {};       int operator()(int n) { std::cout << "n: " << n << std::endl; };  };   functor myLambda(int &n)   {        ++n;        return functor();  }   int main()  {       int n = 0;        myLambda(n)(n);       return 0;  } 

since the code in the question is just a (C++11) technique (or shorthand) for achieving the same effect as this.

The above answers OP's questions 1. and 2. The unspecified behaviour occurs in main(), is unrelated to how myLambda() itself is implemented.

To answer the OP's third question, the behaviour is still unspecified if the lambda (or the functor's operator()) in my example) is modified to not access the value of its argument. The only difference is that the program as a whole produces no visible output that might vary between compilers.

like image 66
Peter Avatar answered Sep 19 '22 11:09

Peter


The n in the definition of the lambda is a formal argument to the function that the lambda defines. It has no connection to the argument to myLambda that's also named n. The behavior here is dictated entirely by the way these two functions are called. In myLambda(n)(n) the order of evaluation of the two function arguments is unspecified. The lambda can be called with an argument of 0 or 1, depending in the compiler.

If the lambda had been defined with [=n]()... it would behave differently.

like image 25
Pete Becker Avatar answered Sep 17 '22 11:09

Pete Becker


I didn't manage to find proper reference to standard but I see that it has similar behavior to argument evaluation order asked here and the order of function arguments evaluation is not defined by the standard:

5.2.2 Function call

8 [ Note: The evaluations of the postfix expression and of the argument expressions are all unsequenced relative to one another. All side effects of argument expression evaluations are sequenced before the function is entered (see 1.9). —end note ]

So here's how it goes inside the calls on different compilers:

#include <iostream>
#include <functional>

struct Int
{
    Int() { std::cout << "Int(): " << v << std::endl; }
    Int(const Int& o) { v = o.v; std::cout << "Int(const Int&): " << v << std::endl; }
    Int(int o) { v = o; std::cout << "Int(int): " << v << std::endl; }
    ~Int() { std::cout << "~Int(): " << v << std::endl; }
    Int& operator=(const Int& o) { v = o.v; std::cout << "operator= " << v << std::endl; return *this; }

    int v;
};

namespace std
{
    template<>
    Int&& forward<Int>(Int& a) noexcept
    {
        std::cout << "Int&: " << a.v << std::endl;
        return static_cast<Int&&>(a);
    }

    template<>
    Int&& forward<Int>(Int&& a) noexcept
    {
        std::cout << "Int&&: " << a.v << std::endl;
        return static_cast<Int&&>(a);
    }
}

std::function<void(Int)> myLambda(Int& n)
{
    std::cout << "++n: " << n.v << std::endl;
    ++n.v;
    return [&](Int m) { 
        std::cout << "n: " << m.v << std::endl;
    };
}

int main()
{
    Int n(0);

    myLambda(n)(n);
    return 0;
}

GCC g++ -std=c++14 -O2 -Wall -pedantic -pthread main.cpp && ./a.out and MSVC

Int(int): 0
Int(const Int&): 0
++n: 0
Int&: 0
Int&: 0
Int(const Int&): 0
n: 0
~Int(): 0
~Int(): 0
~Int(): 1

So it creates variable and copies it to pass to returned lamba.

Clang clang++ -std=c++14 main.cpp && ./a.out

Int(int): 0
++n: 0
Int(const Int&): 1
Int&: 1
Int&: 1
Int(const Int&): 1
n: 1
~Int(): 1
~Int(): 1
~Int(): 1

Here it creates variable evaluates function and then passees copy the lamba.

And the order of evaluation is:

struct A
{
    A(int) { std::cout << "1" << std::endl; }
    ~A() { std::cout << "-1" << std::endl; }
};

struct B
{
    B(double) { std::cout << "2" << std::endl; }
    ~B() { std::cout << "-2" << std::endl; }
};

void f(A, B) { }

int main()
{
    f(4, 5.);
}

MSVC and GCC:

2
1
-1
-2

Clang:

1
2
-2
-1

As in clang order is forward and the argument to lambda is passed after evaluation of the function's argument

like image 38
Teivaz Avatar answered Sep 16 '22 11:09

Teivaz