代码之家  ›  专栏  ›  技术社区  ›  Sergey

类数组容器实现与严格别名

  •  0
  • Sergey  · 技术社区  · 7 年前

    std::vector

    template<typename Type>
    class MyArray
    {
    public:
        explicit MyArray(const uint32_t size) : storage(new char[size * sizeof(Type)]), maxElements(size) {}
        MyArray(const MyArray&) = delete;
        MyArray& operator=(const MyArray&) = delete;
        MyArray(MyArray&& op) { /* some code */ }
        MyArray& operator=(MyArray&& op) { /* some code */ }
        ~MyArray() { if (storage != nullptr) delete[] storage; /* No explicit destructors. Let it go. */  }
    
        Type* data() { return reinterpret_cast<Type*>(storage); }
        const Type* data() const { return reinterpret_cast<const Type*>(storage); }
    
        template<typename... Args>
        void emplace_back(Args&&... args)
        {
            assert(current < maxElements);
            new (storage + current * sizeof(Type)) Type(std::forward<Args>(args)...);
            ++current;
        }
    
    private:
        char* storage = nullptr;
        uint32_t maxElements = 0;
        uint32_t current = 0;
    };
    

    data 似乎违反了 strict aliasing 规则。这也是下标算子、迭代器等的简单实现。

    std::aligned_storage 将只提供正确的对齐,但不会使代码免受依赖于严格别名的编译器优化的破坏。还有,我不想使用 -fno-strict-aliasing 以及出于性能考虑的类似标志。

    例如,考虑下标运算符(为简洁起见为非常量),这是C++中关于UB的文章中的经典代码片段:

    Type& operator[](const uint32_t idx)
    {
        Type* ptr = reinterpret_cast<Type*>(storage + idx * sizeof(ptr)); // Cast is OK.
        return *ptr; // Dereference is UB.
    }
    

    在没有发现我的程序被破坏的任何风险的情况下,实现它的正确方法是什么?标准容器是如何实现的?在所有的编译器中,是否存在对未记录的编译器内部函数的欺骗?

    有时我会看到代码有两个静态转换 void* 而不是重新解释演员阵容:

    Type* ptr = static_cast<Type*>(static_cast<void*>(storage + idx * sizeof(ptr)));
    

    这比重新解读演员阵容更好吗?对我来说,它没有解决任何问题,但看起来过于复杂。

    1 回复  |  直到 7 年前
        1
  •  1
  •   eerorika    7 年前

    但是,取消引用数据返回的指针似乎违反了严格的别名规则

    二者都 char* storage 和返回的指针 data() 指向同一记忆区域。

    此外,下标算子将。。。取消引用不兼容类型的指针,即UB。

    但该对象不是不兼容类型。在里面 emplace_back ,您可以使用placement new来构造的对象 Type Type* 定义良好,因为它指向 类型 ,这是兼容的。

    这就是与指针别名相关的内容:内存中对象的类型,以及被取消引用的指针的类型。从中转换解引用指针的任何中间指针与别名无关。


    注意,析构函数不调用在中构造的对象的析构函数 storage ,所以如果 类型 不是微不足道的可破坏性,那么行为是未定义的。


    Type* ptr = reinterpret_cast<Type*>(storage + idx * sizeof(ptr));
    

    这个 sizeof sizeof(Type) sizeof *ptr . 或者更简单

    auto ptr = reinterpret_cast<Type*>(storage) + idx;
    

    有时我会看到代码有两个静态转换 void* 而不是一个重新解释演员:它是如何优于重新解释演员?

    我想不出在什么情况下行为会有所不同。

    推荐文章