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

时间:2017-11-08 14:20:30

标签: c++ arrays containers strict-aliasing placement-new

我试图实现一个类似数组的容器,它有一些特殊要求和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规则。它也适用于下标操作符,迭代器等的天真实现。

那么在不破坏严格别名规则的情况下,实现由char数组支持的容器的正确方法是什么?据我所知,使用std::aligned_storage只能提供正确的对齐方式,但不会保护代码免受依赖于严格别名的编译器优化的影响。另外,由于性能方面的考虑,我不想使用-fno-strict-aliasing和类似的标记。

例如,考虑下标运算符(简称为nonconstant),这是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 个答案:

答案 0 :(得分:1)

  

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

我不同意。

  

char* storagedata()返回的指针都指向同一个内存区域。

这是无关紧要的。指向同一对象的多个指针不违反别名规则。

  

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

但该对象不是不兼容的类型。在emplace_back中,您使用placement new将Type的对象构造到内存中。假设没有代码路径可以避免这个放置新的,因此假设下标运算符返回指向这些对象之一的指针,那么解除引用Type*的指针是明确定义的,因为它指向{{1}的对象1}},这是兼容的。

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

请注意,析构函数不会调用Type中构造的对象的析构函数,因此如果storage不是简单的可破坏的,那么行为是未定义的。

Type

Type* ptr = reinterpret_cast<Type*>(storage + idx * sizeof(ptr)); 错了。您需要的是sizeofsizeof(Type)。或者更简单地说

sizeof *ptr
  

有时我通过auto ptr = reinterpret_cast<Type*>(storage) + idx; 看到两个静态强制转换的代码而不是一个重新解释强制转换:它比重新解释强制转换更好吗?

我无法想到行为会有所不同的任何情况。