在哪个先发生C++,返回对象的副本或本地对象的析构函数?

in C++ which happens first, the copy of a return object or local object's destructors?

本文关键字:对象 副本 析构函数 返回 C++      更新时间:2023-10-16

我想在某个地方有一个答案,但是我找不到它,因为有很多线程问题,相比之下,我的相当简单。

我不是要创建一个线程安全的复制或赋值构造函数或类似的东西。

我想知道的是,如果我有一个代表互斥锁的类,并且我从一个实例化它的函数返回,这首先发生,我的互斥锁的析构函数(从而解锁它)或返回值的复制构造函数。下面是我的例子:

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.212.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