首页 > 解决方案 > 如何将唯一指针的 std::vector 转换为原始指针的 std::span?

问题描述

我在某些模块的界面中有以下功能:

void DoSomething(Span<MyObject *const> objects);

,我对 C++20模板Span的简化实现在哪里。std::span

这个函数只是遍历指向对象的连续指针序列并调用它们的一些函数,而不试图修改指针(因此const在签名中)。

在呼叫者方面,我有一个std::vector<std::unique_ptr<MyObject>>. 而且我想将该向量传递给DoSomething函数而不分配额外的内存(对于像临时的任何东西std::vector<MyObject*>)。我只想在恒定时间内将unique_ptrs的左值向量转换Span为不可变的原始指针。

这一定是可能的,因为std::unique_ptr<T>带有无状态删除器的 a 具有与原始指针相同的大小和对齐方式T*,并且它存储在里面的只是原始指针本身。因此,按字节计算,std::vector<std::unique_ptr<MyObject>>必须具有与 -- 相同的表示形式std::vector<MyObject*>,因此必须可以将其传递给期望 a 的函数Span<MyObject *const>

我的问题是:

  1. std::span在不引起未定义行为和依赖肮脏黑客的当前提议下,这样的演员阵容是否可行?

  2. 如果不是,在以下标准(例如,C++23)中是否可以预期?

  3. 使用我在我的版本中实现的强制Span转换,使用肮脏的技巧有什么危险memcpy?它在实践中似乎工作正常,但我想其中可能存在一些未定义的行为。如果有,在哪些情况下,这种未定义的行为会在 MSVC、GCC 或 Clang/LLVM 上让我措手不及,具体情况如何?如果可能的话,我将不胜感激这些场景的一些真实例子。

我的代码是这样的:

namespace detail
{
  constexpr std::size_t dynamic_extent = static_cast<std::size_t>(-1);

  template<typename SourceSmartPointer, typename SpanElement, typename = void>
  struct is_smart_pointer_type_compatible_impl
    : std::false_type
  {
  };

  template<typename SourceSmartPointer, typename SpanElement>
  struct is_smart_pointer_type_compatible_impl<SourceSmartPointer, SpanElement,
                                               decltype((void)(std::declval<SourceSmartPointer&>().get()))>
    : std::conjunction<
        std::is_pointer<SpanElement>,
        std::is_const<SpanElement>,
        std::is_convertible<std::add_pointer_t<decltype(std::declval<SourceSmartPointer&>().get())>,
                            SpanElement*>,
        std::is_same<std::remove_cv_t<std::remove_pointer_t<decltype(std::declval<SourceSmartPointer&>().get())>>,
                     std::remove_cv_t<std::remove_pointer_t<SpanElement>>>,
        std::bool_constant<(sizeof(SourceSmartPointer) == sizeof(SpanElement)) &&
                           (alignof(SourceSmartPointer) == alignof(SpanElement))>>
  {
  };

  // Helper type trait which detects whether a contiguous range of smart pointers of the source type
  // can be used to initialize a span of respective immutable raw pointers using a memcpy-based hack.
  template<typename SourceSmartPointer, typename SpanElement>
  struct is_smart_pointer_type_compatible
    : is_smart_pointer_type_compatible_impl<SourceSmartPointer, SpanElement>
  {
  };

  template<typename T, typename R>
  inline T* cast_smart_pointer_range_data_to_raw_pointer(R& source_range)
  {
    T* result = nullptr;

    auto* source_range_data = std::data(source_range);
    std::memcpy(&result, &source_range_data, sizeof(T*));

    return result;
  }
}

template<typename T, std::size_t Extent = detail::dynamic_extent>
class Span final
{
public:
  // ...

  // Non-standard extension.
  // Allows, e.g., to convert `std::vector<std::unique_ptr<Object>>` to `Span<Object *const>`
  // by using the fact that such smart pointers are bytewise equal to the resulting raw pointers;
  // `const` is required on the destination type to ensure that the source smart pointers
  // will be read-only for the users of the resulting Span.
  template<typename R,
           std::enable_if_t<std::conjunction<
             std::bool_constant<(Extent == detail::dynamic_extent)>,
             detail::is_smart_pointer_type_compatible<std::remove_reference_t<decltype(*std::data(std::declval<R&&>()))>, T>,
             detail::is_not_span<R>,
             detail::is_not_std_array<R>,
             std::negation<std::is_array<std::remove_cv_t<std::remove_reference_t<R>>>> >::value, int> = 0>
  constexpr Span(R&& source_range)
    : _data(detail::cast_smart_pointer_range_data_to_raw_pointer<T>(source_range))
    , _size(std::size(source_range))
  {
  }

  // ...

private:
  T* _data = nullptr;
  std::size_t _size = 0;
};

标签: c++stdunique-ptrc++20std-span

解决方案


当前的 std::span 提案是否可以进行这样的转换,而不会导致未定义的行为并依赖于肮脏的黑客攻击?

不。即使这个陈述是真的(而且我知道标准中没有要求强制这是真的):

带有std::unique_ptr<T>无状态删除器的 a 具有与原始指针相同的大小和对齐方式T*,它内部存储的只是原始指针本身。

那没关系。Aunique_ptr<T>不仅仅是T*带有一些成员函数的 a 。它是unique_ptr<T>, 并试图假装一个是另一个是 UB,因为它违反了严格的混叠规则。

如果不是,在以下标准(例如,C++23)中是否可以预期?

不。即使P0593的一种形式以一种允许将存储在数组中的字节unique_ptr<T>转换为 的数组的方式进入标准T*,这也将是一种转换,而不是强制转换。也就是说,这些unique_ptr<T>s 的生命周期将结束,而一个 s 数组的生命周期T*将开始使用先前结束的对象中的数据。所以你做完之后就不能再使用了vector<unique_ptr<T>>

任何这样的转变,如果允许的话,肯定是单向的。P0593 在字节存储中隐式创建对象的能力仅限于本质上只是数据字节的类型,并且unique_ptr不符合该限制。


推荐阅读