В С++, который происходит первым, копия возвращаемого объекта или деструкторы локальных объектов?
Я предполагаю, что там есть какой-то ответ, но я не мог найти его, потому что есть много вопросов, связанных с потоками, и мой довольно простой по сравнению.
Я не пытаюсь создать потоковый экземпляр или конструктор присваивания или что-то в этом роде.
Что мне интересно, если у меня есть класс, который представляет блокировку мьютекса, и я возвращаюсь из функции, которая его запускает, что происходит сначала, деструктор моего мьютекса (таким образом его отпирает) или конструктор копирования возвращаемое значение. Вот мой пример:
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 Операторы перехода
- При выходе из области действия (как бы это было сделано) деструкторы (12.4) вызываются для всех построенных объектов с автоматическим временем хранения (3.7.2) (именованные объекты или временные), объявленные в этой области, в обратный порядок их объявления. Передача из цикла, из блока или обратно после инициализированной переменной с автоматическим временем хранения включает в себя уничтожение переменных с автоматическим временем хранения, которые находятся в области в точке, переданной из...
Оператор return должен завершиться, чтобы выйти из области [function], подразумевая, что также должна завершиться инициализация копирования. Этот порядок не является явным. Различные другие цитаты из 3.7.2 и 12.8 кратко указывают то же самое, что и выше, без предоставления явного порядка. Рабочие пересмотры (после ноября 2014 года) включают приведенную ниже цитату для решения этой проблемы. Отчет отчет о дефектах уточняет изменение.
Из текущего рабочего черновика (N4527) стандарта, как видно на дату этого вопроса
6.6.3 Операция возврата
- Инициализация копии возвращаемого объекта секвенируется до уничтожения временного объекта в конце полного выражения, установленного операндом оператора 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
В то время как я не являюсь стандартным гуру, кажется совершенно очевидным, что после создания копии следует вызвать деструкторы - иначе сам объект, который вы копируете, будет уничтожен до того, как он будет скопирован...:)