在哪个先发生C++,返回对象的副本或本地对象的析构函数?
in C++ which happens first, the copy of a return object or local object's destructors?
我想在某个地方有一个答案,但是我找不到它,因为有很多线程问题,相比之下,我的相当简单。
我不是要创建一个线程安全的复制或赋值构造函数或类似的东西。
我想知道的是,如果我有一个代表互斥锁的类,并且我从一个实例化它的函数返回,这首先发生,我的互斥锁的析构函数(从而解锁它)或返回值的复制构造函数。下面是我的例子:
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语言,你永远不会返回指向全局变量的指针,因为在你返回之后,局部变量就超出了作用域。
c++没有这个问题,因为x会被复制到result中。我想知道这是什么时候发生的。在复印之前,我的锁能开吗?
在这个简单的例子中,"返回数据"是静态信息,但我正在处理的是,它的数据可以被另一个线程更改(也锁定在相同的MutexLock上),所以如果锁在copy-to-result之前释放,副本可能会损坏。
我不确定我是否很好地解释了这个问题,所以我会试着澄清一下这是否有意义。
对于以前的标准(这里我将使用c++ 03),最接近于声明返回操作序列的标准是6.6
6.6 Jump语句
- 在退出作用域时(无论如何完成),对于在该作用域中声明的所有具有自动存储持续时间(3.7.2)的构造对象(命名对象或临时对象)调用析构函数(12.4)与声明顺序相反。从循环、块或返回到具有自动存储持续时间的初始化变量之外,涉及到销毁具有自动存储持续时间的变量,这些变量在从…转移的点上处于作用域内。
return语句必须完成才能退出[function]作用域,这意味着复制初始化也必须完成。这个顺序并不明确。3.7.2和12.8中的其他引语都简明地陈述了与上述相同的内容,但没有明确的顺序。工作修订(2014年11月之后)包括下面的引用来解决这个问题。缺陷报告阐明了变更。
摘自本问题提出之日标准的当前工作草案(N4527)
6.6.3返回语句
- 返回实体的复制初始化在最后销毁临时对象之前进行排序由返回语句的操作数建立的完整表达式,而该操作数依次在返回语句所在块的局部变量的销毁(6.6)。
注意,这个引用直接引用了6.6。因此,我认为可以安全地假设,在返回表达式复制初始化返回值之后,互斥对象将始终被销毁。
虽然我不是标准的专家,但似乎很明显应该在复制之后调用析构函数-否则您正在复制的对象将在复制之前被销毁…:)
对rolen 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 @_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
所关注的复制构造函数似乎是11: return s;
之后的第一个call
。可以看到,该调用在任何析构函数之前执行(析构函数的顺序与构造函数的顺序相反)。
记住销毁顺序的最简单方法是,在离开代码块时按照与创建顺序相反的顺序进行销毁,在返回代码块后离开代码块。
如果你仔细想想,最新构造的是在堆栈的顶部,即。返回语句所需的临时语句,然后是自动的,它们的顺序相反。
在这种情况下,返回语句可能是RVO或NRVO(命名返回值优化),这实际上是一个移动。但由于SSO(小字符串优化),这可能导致它是一个新的结构,因此即使这样也不确定。
返回值放在return语句末尾的"返回堆栈"中,在销毁之前。最初,它被放置在堆栈上,然后复制,也许几次之前被分配给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&结果)
*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
- 关于:C++中异常对象的范围:为什么我没有得到副本?
- 如何从构造函数副本 T(const T&)调用对象 T?
- C ++引用函数参数似乎包含原始对象的副本,而不是充当"real reference"
- 将(临时的?)std::string传递给使用它来构造一个接受副本的对象的函数的最佳方法是什么?
- 将 Eigen::MatrixXd 转换为 arma::mat 并在新对象上制作副本
- C++ 在其自己的类中创建对象的修改副本
- 如何不删除基类对象的副本?
- 属于副本被推送到矢量的对象的指针会发生什么情况?
- 如何确保将对象通过许多层的组件时,不会制作副本
- C++ 如何创建链表的副本作为类对象
- 保留对象成员变量的本地副本
- 如何为同一类对象的成员函数保留单独的变量副本?
- 是 std::make_pair 在将对象添加到地图时创建副本
- 阻止用户创建班级对象的副本,但允许动态的对象有什么好处
- 替代STD ::向量存储参考而不是对象的副本
- 使用矢量push_back代码创建对象副本时遇到问题
- C++ 如何处理带有破坏状态的析构函数的对象副本
- 将对象副本添加到容器的更好方法
- 带有抽象类指针的c++对象副本
- 调用接收对象副本的函数后,原始函数将重置