【问题标题】:std::make_unique<T> vs reset(new T)std::make_unique<T> 与重置(新 T)
【发布时间】:2017-04-20 21:00:12
【问题描述】:

我想问一个关于构造函数内存泄漏的问题。让我们考虑一个类:

 class Foo
 {
   public:
      Foo(){ throw 500;} 
 };

有什么区别

std::unique_ptr<Foo> l_ptr = std::make_unique<Foo>();

std::unique_ptr<Foo> l_ptr;
l_ptr.reset(new Foo());

在我看来,使用 make_unique 的解决方案应该可以保护我免受内存泄漏,但在这两种情况下,我得到了相同的 valgrind 结果:

$ valgrind --leak-check=full ./a.out
==17611== Memcheck, a memory error detector
==17611== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==17611== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==17611== Command: ./a.out
==17611== 
terminate called after throwing an instance of 'int'
==17611== 
==17611== Process terminating with default action of signal 6 (SIGABRT)
==17611==    at 0x5407418: raise (raise.c:54)
==17611==    by 0x5409019: abort (abort.c:89)
==17611==    by 0x4EC984C: __gnu_cxx::__verbose_terminate_handler() (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
==17611==    by 0x4EC76B5: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
==17611==    by 0x4EC7700: std::terminate() (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
==17611==    by 0x4EC7918: __cxa_throw (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
==17611==    by 0x40097B: Foo::Foo() (in /home/rungo/Repositories/test/a.out)
==17611==    by 0x4008DC: main (in /home/rungo/Repositories/test/a.out)
==17611== 
==17611== HEAP SUMMARY:
==17611==     in use at exit: 72,837 bytes in 3 blocks
==17611==   total heap usage: 4 allocs, 1 frees, 72,841 bytes allocated
==17611== 
==17611== 132 bytes in 1 blocks are possibly lost in loss record 2 of 3
==17611==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==17611==    by 0x4EC641F: __cxa_allocate_exception (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
==17611==    by 0x400963: Foo::Foo() (in /home/rungo/Repositories/test/a.out)
==17611==    by 0x4008DC: main (in /home/rungo/Repositories/test/a.out)
==17611== 
==17611== LEAK SUMMARY:
==17611==    definitely lost: 0 bytes in 0 blocks
==17611==    indirectly lost: 0 bytes in 0 blocks
==17611==      possibly lost: 132 bytes in 1 blocks
==17611==    still reachable: 72,705 bytes in 2 blocks
==17611==         suppressed: 0 bytes in 0 blocks
==17611== Reachable blocks (those to which a pointer was found) are not shown.
==17611== To see them, rerun with: --leak-check=full --show-leak-kinds=all
==17611== 
==17611== For counts of detected and suppressed errors, rerun with: -v
==17611== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
[1]    17611 abort (core dumped)  valgrind --leak-check=full ./a.out

我用clang++和g++的时候也是这样。 我在这里找到:https://isocpp.org/wiki/faq/exceptions#ctors-can-throw这句话:

注意:如果构造函数通过抛出异常结束,则与对象本身关联的内存将被清理——没有内存泄漏。

我的问题是为什么我们在这种情况下会发生泄漏以及为什么 make_unique 不能防止泄漏(这是否意味着 make_unique 和 reset(new ...) 之间没有差异?

【问题讨论】:

  • 你发现异常了吗?

标签: c++ c++11 memory-leaks smart-pointers unique-ptr


【解决方案1】:

make_unique 可用于在某些情况下避免内存泄漏,例如:

int foo();
void bar(unique_ptr<int> a, int b);

int main()
{
    try
    {
         bar(unique_ptr<int>(new int(5)), foo());
    }
    catch (...) {/*TODO*/}
}

这里可能首先调用new,然后调用foo() 可能在unique_ptr&lt;int&gt; 构造之前发生。如果foo() 抛出,那么int 将被泄露。如果改用make_unique,这是不可能的:

int foo();
void bar(unique_ptr<int> a, int b);

int main()
{
    try
    {
        bar(make_unique<int>(5), foo());
    }
    catch (...) {/*TODO*/}
}

在这种情况下,要么首先调用foo(),如果它抛出则不创建int,或者首先调用make_unique&lt;int&gt;(5),并允许完成。如果foo() 然后抛出,int 将在堆栈展开期间通过临时unique_ptr&lt;int&gt; 的析构函数被删除。

如果您没有在同一语句中添加任何其他内容,例如对l_ptr.reset(new Foo()); 的调用,那么make_unique 不会提高安全性。不过,它可能仍然更方便。

如果您没有捕获抛出的异常,堆栈可能会或可能不会展开。在多线程程序中,您甚至可以通过让异常从线程中“逃逸”来触发未定义的行为。简而言之,不要那样做。

更新在 C++17 中,上述 bar(unique_ptr&lt;int&gt;(new int(5)), foo()); 也是异常安全的,因为函数参数的求值不再无序,而是现在不确定排序。这意味着编译器必须保证有一个顺序,它只是不必告诉你哪个顺序。请参阅 Barry 对 this question 的回复。

【讨论】:

  • 您似乎在说函数调用中一个参数的求值可能发生在函数调用中另一个参数求值的中间。这实际上是标准允许的吗?
  • 是的,这是允许的。未指定函数参数评估的副作用发生的顺序(它们必须在进入函数之前完成)。这么晚才回复很抱歉。见stackoverflow.com/questions/2934904/…
【解决方案2】:

正如在其他回复中提到的,valgrind 抱怨您由于未捕获的异常而泄漏内存,该异常又调用std::terminate,这反过来又使一切保持原样。如果可执行文件没有终止(例如,您在某处捕获了异常),内存将由语言对 new 行为方式的定义自动释放。

您似乎在问一个更深层次的问题,即std::make_unique 为何存在。这适用于以下情况:

some_func(new Foo, new Foo);

在这种情况下,语言无法保证何时调用 new 与何时调用 Foo::Foo()。您可能会让编译器稍微组织一些事情,以便new 被调用两次,一次为第一个参数分配空间,一次为第二个参数分配空间。然后Foo() 被构造,抛出异常。第一个分配被清除,但第二个分配将泄漏,因为没有构造(或异常)!排队make_unique

some_func(std::make_unique<Foo>(), std::make_unique<Foo>());

现在我们正在调用一个函数,new 将在每个参数的构造函数之前按此顺序调用如果第一个参数抛出异常,第二个分配甚至不会已经发生了。

对比。 unique_ptr::reset 我们只是方便。由于定义的 new 行为引发异常,您应该不会看到任何内存泄漏问题。

【讨论】:

    【解决方案3】:

    你发现异常了吗?捕获异常时,valgrind (使用 g++ 6.2 -g 编译) 检测到 make_uniquereset 都没有泄漏。

    int main() try
    {
    #if TEST_MAKE_UNIQUE
        std::unique_ptr<Foo> l_ptr = std::make_unique<Foo>();   
    #else
        std::unique_ptr<Foo> l_ptr;
        l_ptr.reset(new Foo());
    #endif
    }
    catch(...)
    {
    
    }
    

    AddressSanitizer 也没有报告任何问题。

    (P.S. 这是一个展示鲜为人知的 function-try-block 语言功能的好机会。)


    “为什么内存没有泄露?”

    标准保证如果在构造过程中抛出异常,使用new表达式”分配的内存将被自动释放。

    From $15.2.5:

    如果对象是由 new 表达式 ([expr.new]) 分配的,则调用匹配的释放函数 ([basic.stc.dynamic.deallocation])(如果有)以释放对象占用的存储空间.


    相关问题:

    【讨论】:

    • 捕获异常对 valgrind 有帮助,但我的问题是这两种方法之间的区别?在这种情况下他们是否平等?
    • 您可能想提一下,该标准明确允许堆栈展开在异常仍未捕获时发生。
    • @rungus2:你能否更新一下你的问题,问你想问什么?
    • 我想知道 make_unique 和 reset(new T) 之间的区别。我认为 make_unique 是内存泄漏的解决方案,但它似乎不是真的
    • Angew 是对的,如果异常不会被捕获,通常会调用abort(),这有利于调试但不会清理任何东西。
    猜你喜欢
    • 1970-01-01
    • 2019-05-27
    • 2016-01-23
    • 1970-01-01
    • 2017-09-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-07-23
    相关资源
    最近更新 更多