Capturing lvalue References in C++11 Lambdas

Capturing lvalue References in C++11 Lambdas

By Pete Barber

Overload, 22(119):19-20, February 2014


How confusing does it get when references refer to references and references are captured by value? Pete Barber shows us that it all falls out in the C++ consistency wash.

Recently the question ‘what is the type of an lvalue reference when captured by reference in a C++11 lambda?’ was raised at work. It turns out that it’s a reference to whatever the original reference was too. This is just like taking a reference to an existing reference, e.g.

  int foo = 7;
  int& rfoo = foo;
  int& rfoo1 = rfoo;
  int& rfoo2 = rfoo1;

All references refer to foo rather than rfoo2->rfoo1->rfoo->foo which means running the code in Listing 1

std::cout << "foo:" << foo << ", rfoo:" << rfoo 
          << ", rfoo1:" << rfoo1 << ", rfoo2:" 
          << rfoo2 
          << '\n';
++foo;

std::cout << "foo:" << foo << ", rfoo:" << rfoo 
          << ", rfoo1:" << rfoo1 << ", rfoo2:" 
          << rfoo2 
          << '\n';

std::cout << "&foo:" << &foo << ", &rfoo:"
          << &rfoo  << ", &rfoo1:" << &rfoo1 
          << ", &rfoo2:" << &rfoo2 
          << '\n';
			
Listing 1

gives the following results:

  foo:7, rfoo:7, rfoo1:7, rfoo2:7
  foo:8, rfoo:8, rfoo1:8, rfoo2:8
  &foo:00D3FB0C, &rfoo:00D3FB0C,
  &rfoo1:00D3FB0C,   &rfoo2:00D3FB0C

I.e. all the references are aliases for the original foo hence the same value is displayed including when the original is modified and that the address of each variable is the same, that of foo .

There is nothing surprising here. It’s just basic C++ but it’s a long time since I’ve thought about it which is why with lambdas, l-value, r-value and universal references I sometimes do a double take on what was once obvious.

The same happens with lambda capture but it’s a slightly more interesting story. Take the example in Listing 2:

int foo = 99;
int& rfoo = foo;
int& rfoo1 = foo;

std::cout << "foo:" << foo << ", rfoo:" << rfoo 
          << ", rfoo1:" << rfoo1 
          << '\n';

std::cout << "&foo:" << &foo << ", &rfoo:"
          << &rfoo << ", &rfoo1:" << &rfoo1
          << '\n';

auto l = [foo, rfoo, &rfoo1]()
{
    std::cout << "foo:" << foo << '\n';
    std::cout << "rfoo:" << rfoo << '\n';
    std::cout << "rfoo1:" << rfoo1 << '\n';

    std::cout << "&foo:" << &foo << ", &rfoo:" 
              << &rfoo << ", &rfoo1:" << &rfoo1 
              << '\n';
};

foo = 100;

l();
			
Listing 2

which gives:

  foo:99, rfoo:99, rfoo1:99
  &foo:00D3FB0C, &rfoo:00D3FB0C, &rfoo1:00D3FB0C
  foo:99
  rfoo:99
  rfoo1:100
  &foo:00D3FAE0, &rfoo:00D3FAE4, &rfoo1:00D3FB0C

To begin with it behaves as per the first example in that foo , rfoo and rfoo1 all give the same value. This is because rfoo and rfoo1 are effectively aliases for foo as shown when displaying their addresses; they’re all the same.

However, when these same variables are captured it’s a different story: The capture of foo is of no surprise as this is by-value so displays the captured value of 99 despite the original foo being changed to 100 prior to the lambda being invoked. Its address is that of a new variable; a member of the lambda.

It starts to get interesting with the capture of rfoo . When the lambda is invoked this too displays 99, the original captured value. Also, its address is not that of the original foo . It seems that the reference itself has not been captured but rather what it refers too, in this case an int with the value of 99. It appears to have been magically dereferenced as part of the capture.

This is the correct behaviour and when thought about becomes somewhat obvious. It’s just like assigning a variable from a reference, e.g.

  int foo = 7;
  int& rfoo = foo;
  int bar = rfoo;

bar doesn’t become an int& and rfoo is magically dereferenced except in this scenario there is nothing magical at all, it’s as expected. If int were replaced with auto, e.g.

  auto bar = rfoo;

then it would be expected that bar is an int as auto strips of CV and reference qualifiers.

Finally, there is rfoo1 . This too is odd as it is attempting to take a reference to a reference. As seen in the first example this is perfectly fine. The end effect is that there can’t be a reference to reference and so on and all are aliases of the original variable.

This is pretty much what’s happening here. It’s irrelevant that the target of the capture is a reference. In the end the capture by reference is capture by reference of the underlying variable, i.e. what rfoo1 refers too, in this case foo not rfoo1 itself. This is demonstrated twofold by rfoo1 within the lambda displaying the updated value of foo and also that the address of rfoo1 within the lambda is that of foo outside it.

This is as per the standard section 5.1.2 Lambda expression sub-note 14:

An entity is captured by copy if it is implicitly captured and the capture-default is = or if it is explicitly captured with a capture that does not include an & . For each entity captured by copy, an unnamed nonstatic data member is declared in the closure type. The declaration order of these members is unspecified.

The type of such a data member is the type of the corresponding captured entity if the entity is not a reference to an object, or the referenced type otherwise. [Note: If the captured entity is a reference to a function, the corresponding data member is also a reference to a function.]

The sentence in bold states that for a reference captured by value then the type of the captured value is the type referred to, i.e. the reference aspect has been removed the crucial part being ‘or the referenced type otherwise’. 1

Finally, Listing 3 is a vivid example showing that a reference captured by value involves a dereference.

class Bar
{
private:
  int mValue;

public:
  Bar(const Bar&) : mValue(9999)
  {
  }

public:
  Bar(const int value) : mValue(value) {}
  int GetValue() const { return mValue; }
  void SetValue(const int value) { 
    mValue = value; }
};

Bar bar(1);
Bar& rbar = bar;
Bar& rbar1 = bar;

std::cout << "&bar:" << &bar << ", &rbar:" 
  << &rbar<< ", &rbar1:" << &rbar1 << '\n';

auto l2 = [bar, rbar, &rbar1]()
{
 std::cout << "bar:" << bar.GetValue() << '\n';
 std::cout << "rbar:" << rbar.GetValue() << '\n';
 std::cout << "rbar1:" << rbar1.GetValue() 
           << '\n';

  std::cout << "&bar:" << &bar << ", &rbar:" 
            << &rbar<< ", &rbar1:" << &rbar1 
            << '\n';
};

bar.SetValue(2);

l2();
			
Listing 3

The class bar provides a crude copy-constructor that sets the stored value to 9999. The following output is similar to that in the previous example in that the addresses of bar and rbar in the lambda differ from that of bar showing they’re copies whilst rbar1 is the same. Secondly, the value of mValue stored within Bar is shown as 9999 for the first two captured variables meaning they were copy-constructed.

  &bar:00D3FB0C, &rbar:00D3FB0C, &rbar1:00D3FB0C
  bar:9999
  rbar:9999
  rbar1:2
  &bar:00D3FAE0, &rbar:00D3FAE4, &rbar1:00D3FB0C

Making the copy-construct private (by commenting out the seemingly unnecessary public: ) prevents compilation. (See Listing 4.)

1>------ Build started: Project: References, Configuration: Debug Win32 ------
1>       main.cpp
1>       c:\users\pete\desktop\references\references\main.cpp(85): error C2248: 'Bar::Bar' : cannot access private member declared in class 'Bar'
1>       c:\users\pete\desktop\references\references\main.cpp(59) : see declaration of 'Bar::Bar'
1>       c:\users\pete\desktop\references\references\main.cpp(54) : see declaration of 'Bar'
1>       c:\users\pete\desktop\references\references\main.cpp(59) : see declaration of 'Bar::Bar'
1>       c:\users\pete\desktop\references\references\main.cpp(54) : see declaration of 'Bar'
			
Listing 4

At first the whole capturing of references by reference seems somewhat mind bending and a unique issue. However, when briefly analysed it quickly becomes clear that there is nothing extraordinary happening at all. In fact it is pleasing to see that far from being complicated it is just another example of where references to references have to be considered and that their treatment in this context is the same as in others. The same is true for the capture of references by value. Consistency is good.

  1. I haven't experimented with references to functions





Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.