23

I imagine there's an answer to this somewhere, but I couldn't find it because there are lots of threading questions, and mine's pretty simple by comparison.

I'm not trying to make a threadsafe copy or assignment constructor or anything like that.

What I'm wondering, is if I have a class that represents a mutex lock and I return from a function that instantiates it, which happens first, the destructor of my mutex (thus unlocking it) or the copy constructor of the return value. Here's my example:

string blah::get_data(void)
  {
    MutexLock ml(shared_somewhere_else); // so this locks two threads from calling get_data at the same time

    string x = "return data";
    return x;
  }

Somewhere else, we call get_data...

 string result = get_data();

Thinking back to C for a second, you never return a pointer to a global variable, because the local variable goes out of scope after you return.

C++ doesn't have this problem because x will get copied into result. What I'm wondering is when that happens. Will my lock free before the copy is made?

In this simple example "return data" is static information, but what I'm working with, it's data that can be changed by another thread (also locked on the same MutexLock) so if the lock frees before the copy-to-result is made, the copy could get corrupted.

I'm not sure I'm explaining the question well, so I'll try to clarify if this doesn't make sense.

stu
  • 8,461
  • 18
  • 74
  • 112
  • 3
    You have two objects named `x` in the same scope. – aschepler Oct 15 '15 at 13:59
  • Quick check on Visual C++ compiler shows the MutexLock destructor being the last thing to execute. Not sure what the standard says about this. – Mohamad Elghawi Oct 15 '15 at 14:06
  • 1
    Might [this](http://stackoverflow.com/questions/19046608/are-locals-destroyed-before-or-after-evaluation-of-a-function-return-value) be related? – Paolo M Oct 15 '15 at 14:06
  • oops, I'll fix that (double x thing) – stu Oct 15 '15 at 14:07
  • 1
    Or [this](http://stackoverflow.com/questions/8437763/c-return-value-created-before-or-after-auto-var-destruction) – Paolo M Oct 15 '15 at 14:07
  • I don't want to know when the return expression is evaluated, I want to know when it is copied. (from the first link) – stu Oct 15 '15 at 14:09
  • Ok, look at my second link and to [this](http://stackoverflow.com/questions/275214/scope-and-return-values-in-c) – Paolo M Oct 15 '15 at 14:10
  • the second one looks closer to what I'm talking about, but that example is more complex. – stu Oct 15 '15 at 14:10
  • the third one is interesting, but there aren't two copies done, the answer seems to refer to simple types, copying classes is more complicated. – stu Oct 15 '15 at 14:12
  • @stu Your assumption that object will be copied might be wrong. See: [What are copy elision and return value optimization?](http://stackoverflow.com/questions/12953127/what-are-copy-elision-and-return-value-optimization) – Ivan Aksamentov - Drop Oct 15 '15 at 14:14
  • In my example, yes, I can see where the copy might not be made, but in the example from here http://stackoverflow.com/questions/8437763/c-return-value-created-before-or-after-auto-var-destruction the thing being returned is not local to the function and I would think isn't a candidate for that copy optimization. – stu Oct 15 '15 at 14:33

4 Answers4

23

For previous standards (here I will use C++ 03), the closest the standard comes to declaring the sequence of operations in a return is from 6.6

6.6 Jump statements

  1. On exit from a scope (however accomplished), destructors (12.4) are called for all constructed objects with automatic storage duration (3.7.2) (named objects or temporaries) that are declared in that scope, in the reverse order of their declaration. Transfer out of a loop, out of a block, or back past an initialized variable with automatic storage duration involves the destruction of variables with automatic storage duration that are in scope at the point transferred from...

The return statement must complete in order to exit the [function] scope, implying that the copy-initialization must also complete. This order is not explicit. Various other quotes from 3.7.2 and 12.8 concisely state the same as above without providing explicit order. Working revisions (after Nov. 2014) include the quote below to address that. The defect report clarifies the change.

From the current working draft (N4527) of the standard as seen on the date of this question

6.6.3 The Return Statement

  1. The copy-initialization of the returned entity is sequenced before the destruction of temporaries at the end of the full-expression established by the operand of the return statement, which, in turn, is sequenced before the destruction of local variables (6.6) of the block enclosing the return statement.

Notice that this quote refers directly to 6.6. So I think it is safe to assume that the Mutex object will always be destroyed after the return expression has copy-initialized the return value.

Rollen
  • 1,212
  • 11
  • 15
  • What draft version is that? Seems like the perfect quote, but I can't find it in the drafts I use for C++03, 11 or 14. – TartanLlama Oct 15 '15 at 14:11
  • 2
    Of course, with the [return value optimization](https://en.wikipedia.org/wiki/Return_value_optimization), no actual copy may be made (it may be constructed directly into the caller's return value space), but that's functionally equivalent to the behavior described in the standard when a copy does take place (either way, it precedes destructors). – ShadowRanger Oct 15 '15 at 14:13
  • @TartanLlama I am not sure which one the answerer has but it is in N4296 – NathanOliver Oct 15 '15 at 14:13
  • N4527 was the one I used. and yes it should hold for Move Initialization as well. I'd really expect this to be the same across all versions (with maybe some additional language for C++11 move semantics. – Rollen Oct 15 '15 at 14:14
  • 1
    @RollenD'Souza cool, that quote isn't in previous versions, but the intent is the same with descriptions of block scope, automatic storage duration and return statements. Might be good to note the draft version in your answer. – TartanLlama Oct 15 '15 at 14:16
  • Looks like there was a [defect report](http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1885) for this and the supplied quote was added in October 2014. – TartanLlama Oct 15 '15 at 14:22
  • 1
    @TartanLlama good find. I was searching through the standard to see if any language directly implied the quote added but there really isn't, which would explain the defect.... *6.6* comes very close but leaves 'leaving the scope' open to interpretation "(2) On exit from a scope (however accomplished), destructors are called for all constructed objects with automatic storage duration (named objects or temporaries) that are declared in that scope, in the reverse order of their declaration." – Rollen Oct 15 '15 at 14:27
  • Quote a standard, please. – Lightness Races in Orbit Oct 15 '15 at 14:49
1

While I am no standard guru, it seems quite obvious that destructors should be called after the copy is made - otherwise the very object you are copying would be destroyed before it is copied... :)

SergeyA
  • 61,605
  • 5
  • 78
  • 137
  • 3
    "seems obvious" and "what the optimizing compiler is allowed to do" are two very very different things. – stu Oct 15 '15 at 14:39
1

The easiest way to remember the order of destruction is that it is done in the opposite order of creation at leaving the block and you leave the block after the return.

If you think about it, the latest constructed is on top of the stack, ie. the temporaries needed for the return statement, then the automatic which are in opposite order.

The return statement in this case might be an RVO or NRVO (Named Return Value Optimization) which effectively is a move. But even that is not certain due to SSO(small string optimization) which could cause it to be a new construction.

The return value is placed on the "return stack" at the end of return, before destruction. Originally it was placed on the stack and then copied around, maybe a couple of times before being assigned to the var that it was intended too. The (N)RVO makes it a bit more murky as it intent to place it at the final destination if possible.

If we look at the order of creations and destructions using as-if

Mutex     -> stack +mutex
string x  -> stack +string x base ie. length, capacity and data pointer
          -> heap  +string x data
return x  -> stack +string r base (this is a copy)
          -> heap  +string r data (this is a copy)
end block -> start destruction
destroy x -> heap  -string x data
             stack -string x base
mutex     -> stack -mutex
return to main
          -> destroy old result data
copy return value to result
          -> copy  return base to result base
          -> heap  +new result data
          -> copy  return data to result data
destroy r -> heap  -return data
          -> stack -return base

This clearly is ineffective lets turn on -O3 using italic to denote changed code

Mutex     -> stack +mutex
string x  -> stack +string x base ie. length, capacity and data pointer
          -> heap  +string x data
return x  -> *no need to copy, x is where we want it*
end block -> start destruction
destroy x -> *no need to destroy x as we need it*
mutex     -> stack -mutex
return to main
          -> destroy old result data
copy return value to result
          -> copy return base to result base
          -> *no need to copy the data as its the same*
destroy r -> heap  -return data
          -> stack *only data need to be destroyed so base is destroyed by adjusting stack pointer* 

now we can add (N)RVO which is cheating by adding the return address to the functions parameter, so get_data() becomes get_data(string& result)

*place result on stack
          -> +stack &result*
Mutex     -> stack +mutex
string x  -> *string x is not needed as we use result& *
*if new data is longer than result.capacity 
          -> destroy old data
          -> heap  +string x data
else      -> just copy it*
end block -> start destruction
mutex     -> stack -mutex
return to main
          -> *there is no old result data to destroy*
*data is already in correct position so no copy return value to result*
*there is no return value on stack so don'tdestroy it*

which leaves us with

place result on stack
          -> +stack &result
Mutex     -> stack +mutex
if new data is longer than result.capacity 
          -> destroy old data
          -> heap  +string x data
else      -> just copy it
end block -> start destruction
mutex     -> stack -mutex
return to main
Surt
  • 15,501
  • 3
  • 23
  • 39
  • I agree with your statement about order of destruction and leaving the block after return, but if you look at it from the calling function's perspective, it could also seem that the final assignment happens after the function is completely gone. Thus it is not entirely obvious if you look at it from that perspective. Where is the thing that the assignment is being done from, if the function has already ended? – stu Oct 15 '15 at 14:43
  • Added a bit on explanation, hope its useful. – Surt Oct 15 '15 at 15:39
  • Very nice detailed explanation. And I see in every case, the result ends up somewhere else before the mutex is freed, which is what I was looking for. Thanks. – stu Oct 15 '15 at 18:05
1

A practical addition to Rollen D'Souza's answer.

So now we have a quote from the standard. Now, how does it look like in real code?

Disassembly (VS2015, Debug mode) of this code :

#include <thread>
#include <mutex>
#include <iostream>


std::mutex g_i_mutex;

std::string get_data() {
    std::lock_guard<std::mutex> lock(g_i_mutex);
    std::string s = "Hello";
    return s;
}

int main() {
    std::string s = get_data();
}

...shows:

     8: std::string get_data() {
 push        ebp  
 mov         ebp,esp  
 push        0FFFFFFFFh  
 push        0A1B6F8h  
 mov         eax,dword ptr fs:[00000000h]  
 push        eax  
 sub         esp,100h  
 push        ebx  
 push        esi  
 push        edi  
 lea         edi,[ebp-10Ch]  
 mov         ecx,40h  
 mov         eax,0CCCCCCCCh  
 rep stos    dword ptr es:[edi]  
 mov         eax,dword ptr ds:[00A21008h]  
 xor         eax,ebp  
 mov         dword ptr [ebp-10h],eax  
 push        eax  
 lea         eax,[ebp-0Ch]  
 mov         dword ptr fs:[00000000h],eax  
 mov         dword ptr [ebp-108h],0  
     9:     std::lock_guard<std::mutex> lock(g_i_mutex);
 push        0A212D0h  
 lea         ecx,[lock]  
 call        std::lock_guard<std::mutex>::lock_guard<std::mutex> (0A11064h)  
 mov         dword ptr [ebp-4],0  
    10:     std::string s = "Hello";
 push        0A1EC30h  
 lea         ecx,[s]  
 call        std::basic_string<char,std::char_traits<char>,std::allocator<char> >::basic_string<char,std::char_traits<char>,std::allocator<char> > (0A112A8h)  
    11:     return s;
 lea         eax,[s]  
 push        eax  
 mov         ecx,dword ptr [ebp+8]  
 call        std::basic_string<char,std::char_traits<char>,std::allocator<char> >::basic_string<char,std::char_traits<char>,std::allocator<char> > (0A110CDh)  
 mov         ecx,dword ptr [ebp-108h]  
 or          ecx,1  
 mov         dword ptr [ebp-108h],ecx  
 lea         ecx,[s]  
 call        std::basic_string<char,std::char_traits<char>,std::allocator<char> >::~basic_string<char,std::char_traits<char>,std::allocator<char> > (0A11433h)  
 mov         dword ptr [ebp-4],0FFFFFFFFh  
 lea         ecx,[lock]  
 call        std::lock_guard<std::mutex>::~lock_guard<std::mutex> (0A114D8h)  
 mov         eax,dword ptr [ebp+8]  
    12: }
 push        edx  
 mov         ecx,ebp  
 push        eax  
 lea         edx,ds:[0A1642Ch]  
 call        @_RTC_CheckStackVars@8 (0A114BFh)  
 pop         eax  
 pop         edx  
 mov         ecx,dword ptr [ebp-0Ch]  
 mov         dword ptr fs:[0],ecx  
 pop         ecx  
 pop         edi  
 pop         esi  
 pop         ebx  
 mov         ecx,dword ptr [ebp-10h]  
 xor         ecx,ebp  
 call        @__security_check_cookie@4 (0A114E7h)  
 add         esp,10Ch  
 cmp         ebp,esp  
 call        __RTC_CheckEsp (0A1125Dh)  
 mov         esp,ebp  
 pop         ebp  
 ret

The copy constructor in interest appears to be a first call after 11: return s;. We can see that this call is performed before any of the destructors (and destruction is in turn is in order reversed to order of construction).

Community
  • 1
  • 1
Ivan Aksamentov - Drop
  • 12,860
  • 3
  • 34
  • 61
  • Certainly a definitive way to find out what's going on. :-) – stu Oct 15 '15 at 14:44
  • @stu Yep. However note that compilers might have bugs and behave in non-standard ways. So you cannot know if another compiler or even another version of the same compiler will behave the same way. That's why we still need language lawyers ;) – Ivan Aksamentov - Drop Oct 15 '15 at 15:01
  • @stu worse, this shows what happens in one bit of code compiled on one day with one version of the compiler. Extrapolating to all bits of code, compled by multiple compiler versions, and on different days is foolish without the backing of a standards interpretation. – Yakk - Adam Nevraumont Oct 15 '15 at 17:03