c++ 如何避免这个类违反cpp17之前的严格别名?

w1jd8yoj  于 2023-02-06  发布在  其他
关注(0)|答案(1)|浏览(142)

我自己正在实现一个std::函数。为了进行小对象优化,我需要实现一个可以本地存储对象的存储。据我所知,严格别名规则允许任何类型的数据存储在字节数组中,但禁止从存储数组中提取相应类型的值,除非使用std::launderstd::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];
};
u3r8eeie

u3r8eeie1#

简短的回答是:在C17中,你需要std::launder,但在C14及更低版本中,你不需要(只需return reinterpret_cast<Tp*>(mem_);)。
在C14及之前的版本中,指针简单地表示地址,并且reinterpret_cast ed指针的值正确时会自动指向对象,所以reinterpret_cast<Tp*>(mem_)是可以的。p == q所在的两个指针总是可以互换的。
在C
17中,指针的语义被改变,这样指针可以有相等的值但仍然指向不同的对象,并且std::launder会“修复”一个有正确地址但没有指向正确对象的指针(因为reinterpret_cast<Tp*>(mem_)只会有相同的地址但不会指向你用placement new创建的对象)。

相关问题