【问题标题】:C++ reinterpret_cast safety with array references and move/copy assignment具有数组引用和移动/复制分配的 C++ reinterpret_cast 安全性
【发布时间】:2019-07-20 13:50:42
【问题描述】:

我的队友正在为安全关键型应用程序编写一个固定大小的 std::vector 实现。我们不允许使用堆分配,所以他们创建了一个简单的数组包装器,如下所示:

template <typename T, size_t NUM_ITEMS>
class Vector
{
public:
    void push_back(const T& val);

    ...more vector methods

private:
    // Internal storage
    T storage_[NUM_ITEMS];

    ...implementation
};

我们在这个实现中遇到的一个问题是它需要元素存在默认构造函数(这不是std::vector 的要求,并且造成了移植困难)。我决定破解他们的实现,使其表现得更像std::vector 并想出了这个:

template <typename T, size_t NUM_ITEMS>
class Vector
{
public:
    void push_back(const T& val);

    ...more vector methods
private:
    // Internal storage
    typedef T StorageType[NUM_ITEMS];
    alignas(T) char storage_[NUM_ITEMS * sizeof(T)];

    // Get correctly typed array reference
    StorageType& get_storage() { return reinterpret_cast<T(&)[NUM_ITEMS]>(storage_); }
    const StorageType& get_storage() const { return reinterpret_cast<const T(&)[NUM_ITEMS]>(storage_); }
};

然后我可以搜索并用get_storage() 替换storage_,一切正常。 push_back 的示例实现可能如下所示:

template <typename T, size_t NUM_ITEMS>
void Vector<T, NUM_ITEMS>::push_back(const T& val)
{
    get_storage()[size_++] = val;
}

事实上,它工作得如此简单,以至于让我开始思考.. 这是对reinterpret_cast 的良好/安全使用吗?直接在上面的代码是放置 new 的合适替代方案,还是存在与复制/移动分配给未初始化对象相关的风险?

编辑:针对 NathanOliver 的评论,我应该补充一点,我们不能使用 STL,因为我们不能为我们的目标环境编译它,也不能证明它。

【问题讨论】:

  • get_storage()[size_++] = val; 不合法。该位置没有实际对象,因此您无法分配给它。您需要使用placement new 在正确的位置构造对象。为了让你的生活更轻松,为什么不直接使用 std::vector 并给它一个堆栈分配器,它有一个固定大小的缓冲区,它提供了要使用的向量?
  • @NathanOliver 我们无法使用 STL,因为我们无法为我们的目标环境验证或编译它。我同意在这里放置 new 是一种更好的方法,但应该注意的是,现有代码使用三个独立的编译器构建并通过了严格的单元测试。
  • @AdamBurroughs 那么你错过了一些重要的单元测试,因为容器从不创建或销毁它声称拥有的对象。最简单的方法是,在析构函数有副作用的地方使用T,并在非空Vector&lt;T,N&gt; 的生命周期结束时预期这些副作用。
  • 您使用的是什么类型的T?如果不使用placement new,你实际上不会创建任何对象,如果它们在你的UB土地上不是微不足道的
  • @aschepler -- 这是一个很好的观点,但是 AFAIK,placement new 并不能解决这个问题。无论哪种方式都需要手动销毁。

标签: c++ arrays vector reference reinterpret-cast


【解决方案1】:

这是对 reinterpret_cast 的良好/安全使用吗?

直接在上面的代码是否是放置新的合适替代方法

没有。没有。

或者是否存在与将分配复制/移动到未初始化对象相关的风险?

是的。行为未定义。

  1. 假设内存未初始化,复制向量的行为未定义。
  2. T 类型的对象没有在内存位置开始其生命周期。当T 不是微不足道的时候,这非常糟糕。
  3. 重新解释违反了严格的别名规则。

首先通过值初始化存储来修复。或者通过使向量不可复制和不可移动。

第二个是通过使用placement new来固定的。

在技术上通过使用placement new返回的指针修复了第三个问题,但您可以避免在重新解释存储后通过std::laundering 存储该指针。

【讨论】:

    【解决方案2】:

    您显示的代码仅对 POD 类型(普通旧数据)是安全的,其中对象的表示是微不足道的,因此可以分配给未构造的对象。

    如果您希望它在所有通用性中起作用(我假设您由于使用模板而这样做),那么对于类型T,在构造它之前使用该对象是未定义的行为。也就是说,您必须在例如之前构造对象。分配到该位置。这意味着您需要根据需要显式调用构造函数。下面的代码块演示了一个例子:

    template <typename T, size_t NUM_ITEMS>
    void Vector<T, NUM_ITEMS>::push_back(const T& val)
    {
        // potentially an overflow test here
    
        // explicitly call copy constructor to create the new object in the buffer
        new (reinterpret_cast<T*>(storage_) + size_) T(val);
    
        // in case that throws, only inc the size after that succeeds
        ++size_;
    }
    

    上面的例子演示了placement new,它采用new (void*) T(args...) 的形式。它调用构造函数,但实际上并不执行分配。视觉上的区别在于 operator new 本身包含了 void* 参数,它是要作用和调用构造函数的对象的地址。

    当然,当你删除一个元素时,你也需要明确地销毁它。要对T 类型执行此操作,只需在对象上调用伪方法~T()。在模板化上下文下,编译器将计算出这意味着什么,无论是实际的析构函数调用,还是无操作,例如整数或双精度。如下所示:

    template<typename T, size_t NUM_ITEMS>
    void Vector<T, NUM_ITEMS>::pop_back()
    {
        if (size_ > 0) // safety test, you might rather this throw, idk
        {
            // explicitly destroy the last item and dec count
            // canonically, destructors should never throw (very bad)
            reinterpret_cast<T*>(storage_)[--size_].~T();
        }
    }
    

    另外,我会避免在您的 get_storage() 方法中返回对数组的引用,因为它具有长度信息,并且似乎暗示所有元素都是有效(构造)对象,当然它们不是。我建议您提供获取指向构造对象的连续数组开头的指针的方法,以及获取构造对象数量的另一种方法。这些是 .data().size() 方法,例如std::vector&lt;T&gt;,这将使您的课程对经验丰富的 C++ 用户不那么刺耳。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-06-21
      • 1970-01-01
      • 2016-01-04
      • 1970-01-01
      • 2018-09-14
      相关资源
      最近更新 更多