В С++, который происходит первым, копия возвращаемого объекта или деструкторы локальных объектов?

Я предполагаю, что там есть какой-то ответ, но я не мог найти его, потому что есть много вопросов, связанных с потоками, и мой довольно простой по сравнению.

Я не пытаюсь создать потоковый экземпляр или конструктор присваивания или что-то в этом роде.

Что мне интересно, если у меня есть класс, который представляет блокировку мьютекса, и я возвращаюсь из функции, которая его запускает, что происходит сначала, деструктор моего мьютекса (таким образом его отпирает) или конструктор копирования возвращаемое значение. Вот мой пример:

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;
  }

Где-то в другом месте мы вызываем get_data...

 string result = get_data();

Возвращаясь к C на секунду, вы никогда не возвращаете указатель на глобальную переменную, так как локальная переменная выходит за пределы области после возврата.

У С++ нет этой проблемы, потому что x будет скопирован в результат. Мне интересно, когда это произойдет. Будет ли мой замок свободен до того, как будет сделана копия?

В этом простом примере "возвращаемые данные" - это статическая информация, но с которой я работаю, это данные, которые могут быть изменены другим потоком (также заблокированным на одном MutexLock), поэтому, если блокировка освобождается до копирования -результат, копия может быть повреждена.

Я не уверен, что хорошо объясняю этот вопрос, поэтому я попытаюсь уточнить, не имеет ли это смысла.

Ответ 1

Для предыдущих стандартов (здесь я буду использовать С++ 03), самый близкий стандарт приходит к объявлению последовательности операций в возврате от 6.6

6.6 Операторы перехода

  1. При выходе из области действия (как бы это было сделано) деструкторы (12.4) вызываются для всех построенных объектов с автоматическим временем хранения (3.7.2) (именованные объекты или временные), объявленные в этой области, в обратный порядок их объявления. Передача из цикла, из блока или обратно после инициализированной переменной с автоматическим временем хранения включает в себя уничтожение переменных с автоматическим временем хранения, которые находятся в области в точке, переданной из...

Оператор return должен завершиться, чтобы выйти из области [function], подразумевая, что также должна завершиться инициализация копирования. Этот порядок не является явным. Различные другие цитаты из 3.7.2 и 12.8 кратко указывают то же самое, что и выше, без предоставления явного порядка. Рабочие пересмотры (после ноября 2014 года) включают приведенную ниже цитату для решения этой проблемы. Отчет отчет о дефектах уточняет изменение.

Из текущего рабочего черновика (N4527) стандарта, как видно на дату этого вопроса

6.6.3 Операция возврата

  1. Инициализация копии возвращаемого объекта секвенируется до уничтожения временного объекта в конце полного выражения, установленного операндом оператора return, который, в свою очередь, уничтожение локальных переменных (6.6) блока, охватывающего оператор return.

Обратите внимание, что эта цитата относится непосредственно к 6.6. Поэтому я думаю, что можно с уверенностью предположить, что объект Mutex всегда будет уничтожен после того, как выражение return вернет исходное значение.

Ответ 2

Самый простой способ запомнить порядок разрушения заключается в том, что он выполняется в противоположном порядке создания при выходе из блока, и вы покидаете блок после возврата.

Если вы думаете об этом, последняя построенная находится поверх стека, т.е. временные значения, необходимые для оператора return, затем автоматические, которые находятся в обратном порядке.

Оператором возврата в этом случае может быть RVO или NRVO (оптимизация с наименьшим значением возвращаемого значения), которая фактически является перемещением. Но даже это не очевидно из-за SSO (оптимизация небольших строк), что может стать новой конструкцией.

Возвращаемое значение помещается в "возвратный стек" в конце возврата, перед уничтожением. Первоначально он был помещен в стек, а затем скопирован, может быть, пару раз, прежде чем назначить var, который был предназначен также. (N) RVO делает его немного более мутным, поскольку он намерен поместить его в конечный пункт назначения, если это возможно.

Если мы посмотрим на порядок творения и разрушения, используя 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

Это явно неэффективно, давайте включим -O3, используя курсив, чтобы обозначить измененный код

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* 

теперь мы можем добавить (N) RVO, который обманывает, добавляя обратный адрес к параметру функций, поэтому get_data() становится 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*

что оставляет нас с

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

Ответ 3

Практическое дополнение к Rollen D'Souza.

Итак, теперь у нас есть цитата из стандарта. Теперь, как это выглядит в реальном коде?

Разборка (VS2015, режим отладки) этого кода:

#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();
}

... показывает:

     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        @[email protected] (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        @[email protected] (0A114E7h)  
 add         esp,10Ch  
 cmp         ebp,esp  
 call        __RTC_CheckEsp (0A1125Dh)  
 mov         esp,ebp  
 pop         ebp  
 ret

Конструктор копирования, представляющий интерес, является первым call после 11: return s;. Мы видим, что этот вызов выполняется до того, как какой-либо из деструкторов (а разрушение, в свою очередь, будет отменено до порядка построения).

Ответ 4

В то время как я не являюсь стандартным гуру, кажется совершенно очевидным, что после создания копии следует вызвать деструкторы - иначе сам объект, который вы копируете, будет уничтожен до того, как он будет скопирован...:)