Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ scoping rules for immediately evaluated lambdas

Tags:

c++

lambda

When I run this C++ code:

#include <iostream>
#include <vector>
using namespace std;

int main()
{
  vector<int> vec = [&] ()
  {
    cout<<"pre vec.size() = "<< vec.size() <<endl;
    vector<int> retval = {};
    for(int i = 0; i < 10; ++i)
      vec.push_back(i); // I typed "vec", not "retval"
    
    cout<<"vec.size() = "<< vec.size() <<endl;
    cout<<"retval.size() = "<< retval.size() <<endl;
    return retval;
  }();
  
  cout<< vec.size() <<endl;
}

I get the output:

pre vec.size() = 34354494244
vec.size() = 10
retval.size() = 10
10

Inside the lambda, vec seems to be uninitialized first (see the size). Why does push_backing to it not cause a (valgrind) error?

After the loop, vec's size is 10, which is okay. But why does retval also already have size 10 (and not 0)?

Since I fill vec inside the lambda, but return the empty retval, why does vec have size 10 after evaluation (and not 0)?

I tried both g++ 9.3.0 and clang++ 10.0.0 and get the behavior.

like image 903
user1239014 Avatar asked Oct 02 '20 11:10

user1239014


2 Answers

Try making a small tweak to your program. Add a statement that prints the addresses of both vectors:

#include <iostream>
#include <vector>
using namespace std;

int main()
{
  vector<int> vec = [&] ()
  {
    cout<<"pre vec.size() = "<< vec.size() <<endl;
    vector<int> retval = {};

    std::cout << &retval << " = " << &vec << std::endl;
    for(int i = 0; i < 10; ++i)
      vec.push_back(i); // I typed "vec", not "retval"

    cout<<"vec.size() = "<< vec.size() <<endl;
    cout<<"retval.size() = "<< retval.size() <<endl;
    return retval;
  }();

  cout<< vec.size() <<endl;
}

gcc shows the same address for both retval and vec! This is the result of named return value optimization, which is allowed. This is why valgrind fails to detect this undefined behavior. Undefined behavior means "anything can happen", including the program working as intended. Which is what happened here: the actual construction of vec took place when retval was constructed, and no copy occurred as a result of returning from the lambda. The vector was already constructed.

Note, however, that named return value optimization is optional, and not required to be implemented. You cannot rely on every compiler working this way, setting aside the original issue of undefined behavior.

like image 110
Sam Varshavchik Avatar answered Sep 18 '22 12:09

Sam Varshavchik


What you're looking for is called return value optimisation.

In the context of the C++ programming language, return value optimization (RVO) is a compiler optimization that involves eliminating the temporary object created to hold a function's return value.

source (Wikipedia)

This means for your code, that instead allocating a the variable retval on the stack, it directly uses vec. That's why vec has been implicitly "initialised"
(Attention: This statement may be a bit oversimplifie but easy to imagine!).

You can disable this optimisation with -fno-elide-constructors (GCC)
Test: https://godbolt.org/z/bhTbjf

PS: This code invokes undefined behaviour! Don't do this ;)

like image 36
WolverinDEV Avatar answered Sep 18 '22 12:09

WolverinDEV