我的队友正在为一个安全关键型应用程序编写一个固定大小的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的好用法/安全用法吗?直接在上面的代码是放置新的合适的替代方案,还是存在与对未初始化对象的复制/移动赋值相关的风险?
编辑:作为对NathanOliver的评论的回应,我应该补充说,我们不能使用STL,因为我们不能为我们的目标环境编译它,也不能证明它。
发布于 2019-02-27 07:42:38
您所展示的代码只对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()。在模板化的上下文下,编译器会弄清楚这意味着什么,要么是实际的析构函数调用,要么是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()方法中返回对数组的引用,因为它具有长度信息,并且似乎暗示所有元素都是有效的(构造的)对象,但事实并非如此。我建议您提供一些方法,用于获取指向连续构造对象数组开头的指针,另一个方法用于获取构造对象的数量。这些是例如std::vector<T>的.data()和.size()方法,对于经验丰富的C++用户来说,这将使您的类的使用不那么刺耳。
发布于 2019-02-27 07:24:11
这是reinterpret_cast的一个好的/安全的用法吗?
直接位于代码上方的代码是放置新的
不是的。不是的。
或者是否存在与对未初始化对象的复制/移动分配相关的风险?
是。这种行为是未定义的。
T类型的对象没有在内存位置开始其生命周期。当T不是微不足道的时候,这是非常糟糕的。第一个是通过值初始化存储来修复的。或者通过使矢量不可复制和不可移动来实现。
第二个是使用placement new修复的。
Third通过使用placement new返回的指针在技术上是固定的,但是在重新解释存储之后,您可以避免通过std::laundering来存储该指针。
https://stackoverflow.com/questions/54895340
复制相似问题