首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >数组引用和移动/复制赋值的C++ reinterpret_cast安全性

数组引用和移动/复制赋值的C++ reinterpret_cast安全性
EN

Stack Overflow用户
提问于 2019-02-27 06:39:35
回答 2查看 205关注 0票数 0

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

代码语言:javascript
复制
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,并提出了以下建议:

代码语言:javascript
复制
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的示例实现可能如下所示:

代码语言:javascript
复制
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,因为我们不能为我们的目标环境编译它,也不能证明它。

EN

回答 2

Stack Overflow用户

发布于 2019-02-27 07:42:38

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

如果你想让它在所有的通用性上工作(我假设你是因为使用了一个模板),那么对于一个类型T来说,在构造对象之前使用它是未定义的行为。也就是说,你必须在赋值到那个位置之前构造对象。这意味着您需要按需显式调用构造函数。下面的代码块演示了一个这样的示例:

代码语言:javascript
复制
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()。在模板化的上下文下,编译器会弄清楚这意味着什么,要么是实际的析构函数调用,要么是int或double的无操作。这一点演示如下:

代码语言:javascript
复制
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()方法中返回对数组的引用,因为它具有长度信息,并且似乎暗示所有元素都是有效的(构造的)对象,但事实并非如此。我建议您提供一些方法,用于获取指向连续构造对象数组开头的指针,另一个方法用于获取构造对象的数量。这些是例如std::vector<T>.data().size()方法,对于经验丰富的C++用户来说,这将使您的类的使用不那么刺耳。

票数 2
EN

Stack Overflow用户

发布于 2019-02-27 07:24:11

这是reinterpret_cast的一个好的/安全的用法吗?

直接位于代码上方的代码是放置新的

不是的。不是的。

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

是。这种行为是未定义的。

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

第一个是通过值初始化存储来修复的。或者通过使矢量不可复制和不可移动来实现。

第二个是使用placement new修复的。

Third通过使用placement new返回的指针在技术上是固定的,但是在重新解释存储之后,您可以避免通过std::laundering来存储该指针。

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/54895340

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档