You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 15:06:08