我自己正在实现一个std::函数。为了进行小对象优化,我需要实现一个可以本地存储对象的存储。据我所知,严格别名规则允许任何类型的数据存储在字节数组中,但禁止从存储数组中提取相应类型的值,除非使用std::launder
。std::launder
是在cpp17中引入的,但我希望我的代码可以在cpp14甚至cpp11中运行。因此我阅读了GCC和CLang的实现。在GCC中,他们定义了一个联合_Any_data
作为存储,它具有属性may_alias
,看起来我应该假设它可以违反严格别名。在CLang中,他们使用一个额外的指针来存储new
的结果,如
if (sizeof(_Fun) <= sizeof(__buf_) &&
is_nothrow_copy_constructible<_Fp>::value &&
is_nothrow_copy_constructible<_FunAlloc>::value)
{
__f_ = ::new ((void*)&__buf_) _Fun(_VSTD::move(__f), _Alloc(__af));
}
else
{
typedef __allocator_destructor<_FunAlloc> _Dp;
unique_ptr<__func, _Dp> __hold(__af.allocate(1), _Dp(__af, 1));
::new ((void*)__hold.get()) _Fun(_VSTD::move(__f), _Alloc(__a));
__f_ = __hold.release();
}
我在SO上搜索了很多问题和答案,像https://stackoverflow.com/a/41625067/11139119这样的答案说使用placement new返回的指针是法律的的,但是存储一个指针必须另外占用8字节的空间,这在某种程度上是一种浪费,尤其是GCC在std::function
的大小上只占用了32字节,所以我的问题是,在cpp14之前,如何避免违反严格的别名,而不需要在额外的指针上花费8字节的空间?
下面的代码是我的小对象优化存储的一部分,在cpp17之前,我不知道如何实现get()
而不引起未定义的行为。
class StoragePool {
public:
StoragePool() = default;
template <class Tp, class... Args, std::enable_if_t<soo::IsSmallObject<Tp>::value, int> = 0>
void emplace(Args&&... args) noexcept(noexcept(Tp(std::forward<Args>(args)...))) {
new (mem_) Tp(std::forward<Args>(args)...);
}
template <class Tp, class... Args, std::enable_if_t<!soo::IsSmallObject<Tp>::value, int> = 0>
void emplace(Args&&... args) {
auto tmp = new (mem_) Tp*;
*tmp = new Tp(std::forward<Args>(args)...);
}
template <class Tp, std::enable_if_t<soo::IsSmallObject<Tp>::value, int> = 0>
Tp* get() noexcept {
#if !(__cplusplus < 201703L)
return std::launder(reinterpret_cast<Tp*>(mem_));
#else
// ...
#endif
}
template <class Tp, std::enable_if_t<!soo::IsSmallObject<Tp>::value, int> = 0>
Tp* get() noexcept {
#if !(__cplusplus < 201703L)
return *std::launder(reinterpret_cast<Tp**>(mem_));
#else
// ...
#endif
}
// other functions are omitted because they are unimportant in this question.
private:
alignas(soo::small_object_alignment) unsigned char mem_[soo::small_object_size];
};
1条答案
按热度按时间u3r8eeie1#
简短的回答是:在C17中,你需要
std::launder
,但在C14及更低版本中,你不需要(只需return reinterpret_cast<Tp*>(mem_);
)。在C14及之前的版本中,指针简单地表示地址,并且
reinterpret_cast
ed指针的值正确时会自动指向对象,所以reinterpret_cast<Tp*>(mem_)
是可以的。p == q
所在的两个指针总是可以互换的。在C17中,指针的语义被改变,这样指针可以有相等的值但仍然指向不同的对象,并且
std::launder
会“修复”一个有正确地址但没有指向正确对象的指针(因为reinterpret_cast<Tp*>(mem_)
只会有相同的地址但不会指向你用placement new创建的对象)。