4

I just found a nasty bug in my code because I captured a const reference to a string by reference. By the time the lambda was run the original string object was already long gone and the referenced value was empty whereas the purpose was that it would contains the value of the original string, hence the bug.

What baffles me is that this did not invoke a crash at runtime: after all, shouldn't this be undefined behaviour since afaik there is a dangling reference? Moreover when looking at id under the debugger, it doesn't even look like garbage but just like a properly constructed empty string.

Here's the test case; this just prints an empty line:

typedef std::vector< std::function< void() > > functions; void AddFunction( const std::string& id, functions& funs ) { funs.push_back( [&id] () { //the type of id is const std::string&, but there //is no object to reference. UB? std::cout << id << std::endl; } ); } int main() { functions funs; AddFunction( "id", funs ); funs[ 0 ](); } 
5
  • You're yet another victim of the temporary to const-reference binding :( Commented Jul 21, 2011 at 11:32
  • yeah I know that already, luckily the unit tests pointed that out Commented Jul 21, 2011 at 11:35
  • you might have been less lucky, and it might have been working without problems. Imagine compiler adjusted the stack after AddFunction call, but the stack area where temporary resided was still intact. Then one day, kaboom! Commented Jul 21, 2011 at 17:53
  • 2
    this brings a question, you can either specify lambda to capture a reference or a copy, but there is no way to tell it to move from the temporary. Commented Jul 21, 2011 at 18:35
  • @Gene interesting question indeed.. Commented Jul 22, 2011 at 7:05

3 Answers 3

4

Undefined behavior means there is no requirement what should happen. There is no requirement that it should crash. Whatever memory your dangling reference points at, there's no reason it shouldn't contain something that looks like an empty string, and it's plausible that the destructor of string leaves the memory in that state.

Sign up to request clarification or add additional context in comments.

2 Comments

@stijn: it's not guaranteed, but if the destructor does leave the memory looking like an empty string, that's probably not by accident, it'll be because it clears some fields. The part that is fluke, is that the region of stack that used to contain the temporary string hasn't been reused yet.
just for testing I added the code above in an actual application and there I do get the runtime crashes indeed, for the exact reason you state: once the memory gets reused the string goes all bogus
3

Capturing anything by reference means that you have to take care that it's alive long enough. If you don't the program may just work, but it might just call Domino's and order a double pepperoni. At least, according to the standard.

3 Comments

Alas, nobody has yet shown me a compiler that would actually order pizza upon UB, so I'm beginning to be wary of this example.
I'd rather send a patch to the GCC folks with some IP geolocation :-)
@Kerrek SB: you need to introduce more UBs in the code to improve the odds :)
2

(as pointed out by dascandy) The problem has little or nothing to do with the const and reference syntax, more simply it's an abdication of the responsibility to ensure the existence of everything that is passed by reference at any time it is referenced. The literal in the function call is strictly temporary for that call and evaporates on return, so we are accessing a temporary - a flaw often detected by the compilers - just not in this case.

typedef std::vector<std::function<void()> > functions; void AddFunction(const std::string& id, functions& funs) { funs.push_back([&id] () { //the type of id is const std::string&, but there //is no object to reference. UB? std::cout <<"id="<< id << std::endl; }); } int emain() { functions funs; std::string ida("idA"); // let idB be done by the tenporary literal below std::string idc("idC"); AddFunction(ida, funs); AddFunction("idB", funs); AddFunction(idc, funs); funs[0](); //funs[1](); // uncomment this for (possibly) bizarre results funs[2](); std::cout<<"emain exit"<<std::endl; return 0; } int main(int argc, char* argv[]){ int iret = emain(); return 0; } 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.