【问题标题】:Array-like container implementation vs strict aliasing类数组容器实现与严格别名
【发布时间】:2017-11-08 14:20:30
【问题描述】:

我正在尝试实现一个具有一些特殊要求和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 和类似的标志。

例如,考虑下标运算符(为简洁起见为非常数),这是来自有关 C++ 中的 UB 的文章中的经典代码 sn-p:

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)));

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

【问题讨论】:

  • 一目了然,您似乎将正确类型的对象放置新到存储中,因此应该允许指针为其命名。如果我弄错了,请详细说明您认为自己违反别名规则的原因。
  • 旁注:在你的初始化列表构造函数中,maxElements不应该被初始化为init.size()吗?
  • 但是,缺少析构函数调用是可疑的。你希望什么样的对象可以简单地破坏,但不能简单地构造?
  • 我看不出这违反了严格的别名规则?取消引用指针破坏了严格的别名规则是什么意思?如果不使用aligned_storage,您的代码可能会有点低效,因为有时会发出比所需更多的内存读取。否则我不明白为什么这里会破坏严格的别名规则。
  • std::copy(init.begin(), init.end(), reinterpret_cast&lt;Type*&gt;(storage)) 是错误的。应该在循环中调用构造。

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


【解决方案1】:

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

我不同意。

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

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

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

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

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


请注意,您的析构函数不会调用在storage 中构造的对象的析构函数,因此如果Type 不是可简单销毁的,则行为未定义。


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

sizeof 是错误的。您需要的是sizeof(Type)sizeof *ptr。或者更简单

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

有时我通过void* 看到带有两个静态转换的代码,而不是一个重新解释转换:它比重新解释转换更好吗?

我想不出任何行为会有所不同的情况。

【讨论】:

  • 感谢您的澄清!此外,我还删除了下标和固定析构函数以使用 sfinae,它仅在 Type 可轻易破坏时执行优化的释放。
猜你喜欢
  • 2014-02-08
  • 2013-10-01
  • 2012-05-20
  • 2023-03-17
  • 1970-01-01
  • 1970-01-01
  • 2019-02-09
  • 2020-08-01
  • 2015-01-17
相关资源
最近更新 更多