【发布时间】:2020-08-02 12:06:37
【问题描述】:
有一个项目专注于使用 C++ 98 没有额外的依赖,但它需要维护动态分配的内存。智能指针不可用,因此添加了手动清理的代码。该方法是在 CTOR 中将变量显式设置为NULL,读取一些可能动态分配内存的数据,捕获任何发生的异常并通过手动调用 DTOR 清理内存。无论如何,这都需要实现释放内存,以防万一一切成功,并且只是通过检查是否已分配内存的安全措施来增强。
以下是与此问题最相关的available code:
default_endian_expr_exception_t::doc_t::doc_t(kaitai::kstream* p__io, default_endian_expr_exception_t* p__parent, default_endian_expr_exception_t* p__root) : kaitai::kstruct(p__io) {
m__parent = p__parent;
m__root = p__root;
m_main = 0;
try {
_read();
} catch(...) {
this->~doc_t();
throw;
}
}
void default_endian_expr_exception_t::doc_t::_read() {
m_indicator = m__io->read_bytes(2);
m_main = new main_obj_t(m__io, this, m__root);
}
default_endian_expr_exception_t::doc_t::~doc_t() {
if (m_main) {
delete m_main; m_main = 0;
}
}
标题中最相关的部分如下:
class doc_t : public kaitai::kstruct {
public:
doc_t(kaitai::kstream* p__io, default_endian_expr_exception_t* p__parent = 0, default_endian_expr_exception_t* p__root = 0);
private:
void _read();
public:
~doc_t();
private:
std::string m_indicator;
main_obj_t* m_main;
default_endian_expr_exception_t* m__root;
default_endian_expr_exception_t* m__parent;
};
代码在three different environments、clang3.5_linux、clang7.3_osx 和msvc141_windows_x64 中进行了测试,以在读取数据时显式抛出异常以及在这些情况下是否泄漏内存。问题是这仅在 CLANG 3.5 for Linux 上触发 SIGABRT。最有趣的stack frames 如下:
<frame>
<ip>0x577636E</ip>
<obj>/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19</obj>
<fn>std::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string()</fn>
</frame>
<frame>
<ip>0x5ECFB4</ip>
<obj>/home/travis/build/kaitai-io/ci_targets/compiled/cpp_stl_98/bin/ks_tests</obj>
<fn>default_endian_expr_exception_t::doc_t::doc_t(kaitai::kstream*, default_endian_expr_exception_t*, default_endian_expr_exception_t*)</fn>
<dir>/home/travis/build/kaitai-io/ci_targets/tests/compiled/cpp_stl_98</dir>
<file>default_endian_expr_exception.cpp</file>
<line>51</line>
</frame>
[...]
<frame>
<ip>0x577636E</ip>
<obj>/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19</obj>
<fn>std::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string()</fn>
</frame>
<frame>
<ip>0x5ED17E</ip>
<obj>/home/travis/build/kaitai-io/ci_targets/compiled/cpp_stl_98/bin/ks_tests</obj>
<fn>default_endian_expr_exception_t::doc_t::~doc_t()</fn>
<dir>/home/travis/build/kaitai-io/ci_targets/tests/compiled/cpp_stl_98</dir>
<file>default_endian_expr_exception.cpp</file>
<line>62</line>
</frame>
第 51 行和第 62 行是上面提供的 CTOR 和 DTOR 的最后一行,所以实际上是右括号。这看起来像是编译器添加的一些代码只是试图释放维护的std::string 两次,一次在 DTOR 中,另一次在 CTOR 中,很可能仅在引发异常时。
这个分析完全正确吗?
如果是这样,这是 C++ 的一般预期行为还是仅这个具体的编译器?我想知道,因为其他编译器不SIGABRT,即使代码对所有人都是一样的。这是否意味着不同的编译器会以不同的方式清理像std::string 这样的非指针?如何知道每个编译器的行为方式?
看看C++-standard says 是什么,我原以为std::string 由于异常仅由CTOR 释放:
C++11 15.2 构造函数和析构函数(二)
初始化或销毁被异常终止的任何存储持续时间的对象都将为其所有完全构造的子对象(不包括类联合类的变体成员)执行析构函数,也就是说,对于那些主构造函数(12.6.2)已完成执行,而析构函数尚未开始执行。
在这种情况下,破坏不会被异常终止,只有构造。但是因为 DTOR 是一个 DTOR,它也被设计为自动清理东西?如果是这样,一般是所有编译器还是只有这个?
手动调用 DTOR 可靠吗?
根据我的研究,手动调用 DTOR 应该不会太糟糕。那是不是一个错误的表达方式,因为我现在看到的东西,这是一个很大的禁忌?我的印象是,如果手动调用 DTOR,它只需要兼容才能以这种方式调用。以上应该是我的理解。它只是因为我不知道的编译器自动生成的代码而失败。
如何解决这个问题?
与其手动调用 DTOR 并触发自动生成的代码,不如简单地使用自定义 cleanUp 函数来释放内存并设置指向 NULL 的指针?如果发生异常并且始终在 DTOR 中调用它应该是安全的,对吗?或者有什么方法可以让所有编译器都以兼容的方式调用 DTOR?
谢谢!
【问题讨论】:
-
你有
m_indicator被显式的析构函数调用this->~doc_t()销毁,然后在构造函数通过异常退出时再次销毁。这通过在对象的生命周期结束后访问对象来表现出未定义的行为。修复它的正确方法是让所有资源由 RAII 类管理,并允许异常自然传播。即使标准库中的智能指针不可用,您也可以实现自己的;也许花里胡哨的东西更少。 -
您的程序行为未定义,因为它使用带有双下划线的标识符。这样的标识符是为实现保留的。
-
@IgorTandetnik 但我想知道为什么只有一个编译器会中断,如果所有编译器在理论上在 CTOR 和 DTOR 的相同位置应用相同的魔法释放等。
-
未定义的行为是未定义的。 “似乎有效”是未定义行为的一种可能表现形式。无论如何,您可以通过编写一个简单的类来观察实际行为,该类在其构造函数和析构函数中打印一些内容,否则什么也不做;然后将此类的成员添加到
doc_t。我希望它的析构函数在doc_t构造函数抛出时被调用两次。 -
Here's a simple example 演示了该行为。观察一个构造函数和两个析构函数调用。上一个链接是针对clang的; here's gcc。两者的行为相同。
标签: c++ memory-management memory-leaks clang