我的队友正在为安全关键型应用程序编写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,因为我们不能针对目标环境对其进行编译,也无法对其进行认证。
答案 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的良好/安全使用吗?
代码是否正好位于放置新代码的合适选择之上
不。不。
还是将复制/移动分配给未初始化对象有风险吗?
是的。该行为是不确定的。
T
类型的对象在内存位置开始其生存期。如果T
不平凡,那就太糟糕了。首先通过对存储进行值初始化来解决此问题。或通过使向量不可复制和不可移动来实现。
第二个是通过使用新的展示位置来固定的。
在技术上,第三种方法是使用由placement new返回的指针来解决,但是您可以避免在重新解释存储空间之后通过std::launder
ing来存储该指针。