C++自定义BQueue::Pop开启-O1优化后析构函数重复调用问题
问题现象
自定义C++环形队列BQueue的Pop()方法逻辑为:调用_First索引标记的队首元素析构函数,之后自增_First索引、递减队列长度计数_Length。无优化编译时程序运行符合预期,添加-O1编译优化选项后出现异常:
- 析构函数内的打印语句可正常执行,但元素的
Id成员未被正确设置为-1 - 队列离开作用域执行
delete[]释放内存时,会再次调用已出队元素的析构函数,触发未定义行为,若元素持有文件描述符类资源,将出现资源重复关闭的错误
实际运行输出:
1 Constructed 2 Constructed 1 Destructed 2 Destructed Finished 2 Destructed 1 Destructed
预期行为为打印Finished后不应产生任何额外输出,但实际运行时打印完成后仍出现两次析构日志。
问题复现代码
#include <iostream> #include <tuple> #include <memory> #include <sys/uio.h> #include <initializer_list> namespace Core { namespace Iterable { template <typename T> class Span { private: size_t _Length = 0; T *_Content = nullptr; inline T &_ElementAt(size_t Index) { return this->_Content[Index]; } inline const T &_ElementAt(size_t Index) const { return this->_Content[Index]; } public: Span() = default; Span(size_t Size) : _Length(Size), _Content(new T[Size]) {} Span(size_t Size, const T &Value) : _Length(Size), _Content(new T[Size]) { for (size_t i = 0; i < _Length; i++) { _Content[i] = Value; } } Span(Span &&Other) : _Length(Other._Length), _Content(Other._Content) { Other._Content = nullptr; Other._Length = 0; } Span(const Span &Other) : _Length(Other._Length), _Content(new T[Other._Length]) { for (size_t i = 0; i < Other._Length; i++) { _Content[i] = Other._Content[i]; } } Span(const T *Array, size_t Size) : _Length(Size), _Content(new T[Size]) { for (size_t i = 0; i < Size; i++) { _Content[i] = Array[i]; } } Span(std::initializer_list<T> list) : _Length(list.size()), _Content(new T[list.size()]) { size_t i = 0; for (auto &item : list) { _Content[i] = item; i++; } } ~Span() { delete[] _Content; _Content = nullptr; } inline T *Content() { return _Content; } inline const T *Content() const { return _Content; } inline size_t Length() const { return _Length; } T &operator[](const size_t &Index) { if (Index >= _Length) throw std::out_of_range(""); return _ElementAt(Index); } const T &operator[](const size_t &Index) const { if (Index >= _Length) throw std::out_of_range(""); return _ElementAt(Index); } Span &operator=(const Span &Other) { if (this != &Other) { _Length = Other._Length; delete[] _Content; _Content = new T[_Length]; for (size_t i = 0; i < _Length; i++) { _Content[i] = Other._Content[i]; } } return *this; } Span &operator=(Span &&Other) { if (this != &Other) { delete[] _Content; _Content = Other._Content; _Length = Other._Length; Other._Content = nullptr; Other._Length = 0; } return *this; } }; template <typename T> class BQueue final { public: // Constructors BQueue() = default; BQueue(size_t Size, bool Growable = true) : _Content(Size), _First(0), _Length(0), _Growable(Growable) {} BQueue(std::initializer_list<T> list) : _Content(list), _First(0), _Length(list.size()), _Growable(true) {} BQueue(const BQueue &Other) : _Content(Other._Content), _First(Other._First), _Length(Other._Length), _Growable(Other._Growable) {} BQueue(BQueue &&Other) : _Content(Other._Content), _First(Other._First), _Length(Other._Length), _Growable(Other._Growable) { Other._First = 0; Other._Length = 0; Other._Growable = true; } // Operators BQueue &operator=(const BQueue &Other) { if (this != &Other) { _Content = Other._Content; _First = Other._First; _Length = Other._Length; _Growable = Other._Growable; } return *this; } BQueue &operator=(BQueue &&Other) { if (this != &Other) { _Content = std::move(Other._Content); _First = std::move(Other._First); _Length = std::move(Other._Length); _Growable = std::move(Other._Growable); Other._First = 0; Other._Length = 0; Other.Growable = true; } return *this; } T &operator[](size_t Index) { if (Index >= _Length) throw std::out_of_range("Index out of range"); return _Content.Content()[(_First + Index) % Capacity()]; } T const &operator[](size_t Index) const { if (Index >= _Length) throw std::out_of_range("Index out of range"); return _Content.Content()[(_First + Index) % Capacity()]; } // Peroperties size_t Capacity() const { return _Content.Length(); } size_t Length() const { return _Length; } bool Growable() const { return _Growable; } T *Content() { return _Content.Content(); } T const *Content() const { return _Content.Content(); } inline bool IsWrapped() const { return _First + _Length > Capacity(); } inline bool IsEmpty() noexcept { return _Length == 0; } inline bool IsFull() noexcept { return _Length == Capacity(); } inline size_t IsFree() noexcept { return Capacity() - _Length; } // Helper functions T &Head() { AssertNotEmpty(); return _Content.Content()[_First]; } T const &Head() const { AssertNotEmpty(); return _Content.Content()[_First]; } // Remove functionality void Pop() { std::destroy_at(std::addressof(Head())); --_Length; _First = (_First + 1) % Capacity(); } private: Iterable::Span<T> _Content; size_t _First = 0; size_t _Length = 0; bool _Growable = true; inline void AssertNotEmpty() { if (IsEmpty()) throw std::out_of_range("Instance is empty"); } }; } } class Messenger { public: int Id = -1; Messenger() = default; Messenger(size_t id) : Id(id) { std::cout << Id << " Constructed" << std::endl; } Messenger(Messenger &&Other) : Id(Other.Id) { Other.Id = -1; } Messenger(Messenger const &Other) : Id(Other.Id) {} Messenger &operator=(Messenger &&other) { Id = other.Id; other.Id = -1; return *this; } Messenger &operator=(Messenger const &Other) { Id = Other.Id; return *this; } ~Messenger() { if (Id != -1) { std::cout << Id << " Destructed" << std::endl; Id = -1; } } }; using namespace Core; int main(int argc, char const *argv[]) { Iterable::BQueue<Messenger> Queue{1, 2}; Queue.Pop(); Queue.Pop(); std::cout << "Finished" << std::endl; return 0; }
问题根因
- 底层
Span类内存逻辑错误:使用new T[Size]申请内存时,会对数组所有位置的元素执行默认构造,Span析构时调用delete[] _Content会遍历数组所有位置执行析构,完全不考虑对应位置的元素是否已经被手动销毁。Pop()手动析构队首元素后,没有标记该位置为无效状态,delete[]执行时仍会把这些位置的内存当作有效对象,触发二次析构。 - 编译器优化导致无效写入被删除:开启O1优化后,编译器严格遵循C++对象生命周期规则判定代码行为:手动调用析构函数后对象生命周期已经结束,析构函数内
Id = -1的写入操作属于对已消亡对象的内存写入,没有可观测的合法副作用,被编译器直接优化删除,所以才会出现打印语句正常执行但Id值没被修改的现象。 - 容器生命周期管理缺失:
BQueue作为上层容器,没有全权接管元素的生命周期管理,既没有跟踪数组槽位的占用/空闲状态,也没有自定义析构逻辑控制元素析构范围,把元素析构逻辑完全丢给底层Span的delete[]操作,必然会析构到已经出队的元素。 - 额外隐患:当前
BQueue的拷贝构造、拷贝赋值逻辑是浅拷贝,两个队列实例会持有同一块内存指针,离开作用域会触发double free。
修复方案
- 改造
Span为原始内存持有类:把内存申请方式从new T[Size]改为::operator new(Size * sizeof(T)),只申请对应大小的原始内存,不执行任何元素默认构造;析构时调用::operator delete(_Content)直接释放原始内存,不做任何元素析构操作,元素的构造、析构完全交由上层BQueue控制。 - 给
BQueue实现完整的生命周期管理:自定义析构函数,遍历队列中当前存在的有效元素(范围由_First和_Length确定)逐个调用析构,完全不碰已经Pop出队的元素内存。 - 修正元素构造逻辑:Push元素时不要直接对数组位置做赋值操作,用
placement new在目标内存位置直接构造新对象;Pop元素时手动析构后不需要额外修改成员值,只要保证该位置内存不会被再次当作有效对象析构即可。 - 修复拷贝、移动构造和赋值逻辑:做深拷贝处理,避免多个实例持有同一块内存。
核心修改代码示例
修改Span的构造和析构逻辑:
// 原始内存申请,不构造元素 Span(size_t Size) : _Length(Size), _Content(static_cast<T*>(::operator new(Size * sizeof(T)))) {} // 其余构造函数对应修改,拷贝数据时用placement new构造 Span(const Span& Other) : _Length(Other._Length), _Content(static_cast<T*>(::operator new(Other._Length * sizeof(T)))) { for (size_t i = 0; i < _Length; i++) { new (&_Content[i]) T(Other._Content[i]); } } // 析构仅释放原始内存,不析构元素 ~Span() { ::operator delete(_Content); _Content = nullptr; }
给BQueue添加自定义析构:
~BQueue() { // 仅析构队列内现存的有效元素 for (size_t i = 0; i < _Length; i++) { std::destroy_at(std::addressof((*this)[i])); } }
内容的提问来源于stack exchange,提问作者Jack Sparrow
相关产品推荐
相关产品推荐

