带有数组引用和移动/复制分配的C ++ reinterpret_cast安全性

时间:2019-02-26 22:39:35

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

我的队友正在为安全关键型应用程序编写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_); }
};

然后,我可以搜索storage_并将其替换为get_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的良好/安全使用吗?该代码正上方的代码是替代放置的合适替代方法,还是存在与未初始化对象的复制/移动分配相关的风险?

编辑:针对NathanOliver的评论,我要补充一点,我们不能使用STL,因为我们不能针对目标环境对其进行编译,也无法对其进行认证。

2 个答案:

答案 0 :(得分: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_;
}

上面的示例演示了新的展示位置,其格式为new (void*) T(args...)。它调用构造函数,但实际上不执行分配。视觉上的区别是,运算符new本身包含void*参数,这是要作用并为其调用构造方法的对象的地址。

当然,当您删除元素时,也需要明确销毁该元素。要针对类型T执行此操作,只需在对象上调用伪方法~T()。在模板化上下文中,编译器将解决这意味着什么,无论是实际的析构函数调用,还是例如no-op。 int或double。如下所示:

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<T>,这将减少对经验丰富的C ++用户的使用。

答案 1 :(得分:1)

  

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

     

代码是否正好位于放置新代码的合适选择之上

不。不。

  

还是将复制/移动分配给未初始化对象有风险吗?

是的。该行为是不确定的。

  1. 假设内存未初始化,则复制向量具有不确定的行为。
  2. 没有T类型的对象在内存位置开始其生存期。如果T不平凡,那就太糟糕了。
  3. 重新解释违反了严格的别名规则。

首先通过对存储进行值初始化来解决此问题。或通过使向量不可复制和不可移动来实现。

第二个是通过使用新的展示位置来固定的。

在技术上,第三种方法是使用由placement new返回的指针来解决,但是您可以避免在重新解释存储空间之后通过std::launder ing来存储该指针。