Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What's happening with the snippet below in a release build?

Tags:

c++

The code below generates a dangling reference as can be seen in the warning emitted by the compiler and the fact that the destructor for the A object in function g() is invoked before the function returns. One can also verify that in main() after "using the stack", the returned reference has garbage, at least in a debug build. But I couldn't reproduce the same behavior in a release build. Why is that? What kind of optimization the compiler is doing here to give the impression that the reference r is Ok?

#include <iostream>

struct A{
    A(int i) : i(i) { std::cout << "Ctor\n"; }
    A(const A& a) { i = a.i; std::cout << "Copy ctor\n"; }
    ~A() { std::cout << "Dtor\n"; }
    int i;
};

A& g(int i) { A x(i); return x; }

int main()
{
    const A& r = g(1);
    std::cout << "Using the stack\n";     
    std::cout << r.i << '\n';   // r.i has garbage in debug, but not in a release build.
}

PS. I would argue against NRVO, as the function doesn't return an A object.

Edit: In response to Mark Tolonen. Even if I include these expressions after const A& r = g(1); the release build doesn't show garbage in std::cout << r.i << '\n';

std::cout << "Using the stack ...................................................................................................................\n";
std::cout << "Using the stack ...................................................................................................................\n";
std::cout << "Using the stack ...................................................................................................................\n";
std::cout << "Using the stack ...................................................................................................................\n";
like image 981
WaldB Avatar asked Jun 29 '13 21:06

WaldB


2 Answers

It's just undefined behavior. You return a temporary by reference, anything can happen.

The A& g(int i) { A x(i); return x; } is illegal.

A debug build will probably clear the memory and cause errors because the memory was cleared.

A release build doesn't bother. You pay for what you use, right? It just leaves the memory untouched, but marks it as reclaimable by the OS. All gloves are off afterwards.

It's an (arguably) good thing that comes with the VC++ compiler. You'll see all sort of stuff happening in debug build to help you... well... debug better. Uninitialized pointers set to some specific value so that you know it's uninitialized, memory zeroed out after a delete so that you know it was deleted. This helps in identifying problems sooner, because in a release build you'd probably still see the memory if it wasn't overwritten, or access an uninitialized pointer and have it appear to work, etc. Problems you wouldn't see otherwise, and at the time you would spot would cause a lot of harm and would be very hard to diagnose.

like image 118
Luchian Grigore Avatar answered Sep 29 '22 08:09

Luchian Grigore


Here's what the speed-optimized (/O2 compiler switch) release build of Visual Studio 2012 64-bit actually does when it runs this code and prints out a one:

int main()
{
000000013F7C7E50  sub         rsp,28h  
    const A& r = g(1);
000000013F7C7E54  lea         rdx,[string "Ctor\n" (013F83DA4Ch)]  
000000013F7C7E5B  lea         rcx,[std::cout (013F85FAA0h)]  
000000013F7C7E62  call        std::operator<<<std::char_traits<char> > (013F7C1500h)  
000000013F7C7E67  lea         rdx,[string "Dtor\n" (013F83DA54h)]  
000000013F7C7E6E  lea         rcx,[std::cout (013F85FAA0h)]  
000000013F7C7E75  call        std::operator<<<std::char_traits<char> > (013F7C1500h)  
    std::cout << "Using the stack\n";     
000000013F7C7E7A  lea         rdx,[string "Using the stack\n" (013F83DA60h)]  
000000013F7C7E81  lea         rcx,[std::cout (013F85FAA0h)]  
000000013F7C7E88  call        std::operator<<<std::char_traits<char> > (013F7C1500h)  
    std::cout << r.i << '\n';   // r.i has garbage in debug, but not in a release build.
000000013F7C7E8D  lea         rcx,[std::cout (013F85FAA0h)]  
000000013F7C7E94  mov         edx,1  
000000013F7C7E99  call        std::basic_ostream<char,std::char_traits<char> >::operator<< (013F7C1384h)  
000000013F7C7E9E  mov         dl,0Ah  
000000013F7C7EA0  mov         rcx,rax  
000000013F7C7EA3  call        std::operator<<<std::char_traits<char> > (013F7C10EBh)  

Note that it doesn't even bother to really create and destroy the A object. All it does is call cout four times. Each time, rdx holds the object to print. The first three print the strings "Ctor\n", "Dtor\n", and "Using the stack\n". The last one looks like it just prints the integer in edx which is a 1.

The compiler can really do anything for undefined behavior. It prints something besides a one for space-optimized (/O1 compiler switch), or as the OP found, not-optimized (/Od).

like image 33
Mark Tolonen Avatar answered Sep 29 '22 07:09

Mark Tolonen