【问题标题】:How to convert a std::vector of unique pointers to a std::span of raw pointers?如何将唯一指针的 std::vector 转换为原始指针的 std::span?
【发布时间】:2020-05-19 10:50:57
【问题描述】:

我在某个模块的界面中有如下功能:

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

,其中Span 是我对C++20 的std::span 模板的简化实现。

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

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

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

我的问题是:

  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;
};

【问题讨论】:

  • @JesperJuhl No :) Span 基本上只是一对原始指针和大小。无处可申请std::transform 以从vector 获得Span。其他向量(以及连续存储其数据的不同类型的容器)通过调用它们的 .data() 成员函数简单地转换为 Span——但在我的特定情况下,如果没有显式转换,它将无法工作。
  • 你必须分配std::vector&lt;MyObject*&gt;或类似的。
  • 如果DoSomething 不需要连续内存,那么使用迭代器或范围的更通用接口似乎是合适的。也就是说,如果您可以自己更改方法。我没有使用范围(ranges-v3C++20 Ranges 都没有),但你可以传递你的 std::vector&lt;std::unique_ptr&lt;MyObject&gt;&gt;,就好像它是一个带有转换视图范围的原始指针范围,可以选择使用过滤视图。

标签: c++ std unique-ptr c++20 std-span


【解决方案1】:

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

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

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

没关系。 unique_ptr&lt;T&gt; 不仅仅是带有一些成员函数的T*。这是一个unique_ptr&lt;T&gt;,由于违反了严格别名规则,试图假装一个是另一个是UB。

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

没有。即使P0593 的一种形式以一种允许将存储在unique_ptr&lt;T&gt; 数组中的字节转换为T* 数组的方式进入标准,这也将是一个转换,不是演员表。也就是说,unique_ptr&lt;T&gt;s 的生命周期将结束,T*s 数组的生命周期将开始使用先前结束的对象中的数据。所以你不能再使用vector&lt;unique_ptr&lt;T&gt;&gt;了。

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

【讨论】:

  • 谢谢。但是,如果我 100% 确定在我的平台上 std::unique_ptr 具有与原始指针相同的二进制表示(我可以为此编写自己的 UniquePtr),那么这种可怕的未定义行为的真正表现可能是什么?是?您(或其他人)能否提供任何示例?
  • 例如,如果我生成的Span 对象具有Span&lt;T*&gt; 类型(没有const),我可以想象一个UB。这很容易导致坏事。例如,客户端代码可以调用函数void ModifyThosePointers(Span&lt;MyObject *&gt; object_pointers),它实际上会更改参数指针的值(完全允许这样做,现在它的参数不是 const)。没有“真正的”原始指针(从 C++ 生命周期的角度来看),因此写入被“假”原始指针占用且实际上属于 unique_ptrs 向量的内存很容易搞砸起来。
  • 等等,等等。严格的别名在那里,但原始指针也在那里。 unique_ptr&lt;T&gt; 是标准布局对象,pointer-interconvertible 也是其唯一成员。也就是说,在指向unique_ptr 的指针上使用reinterpret_cast 可以获得指向底层原始指针的指针。瞧!现在仔细检查指针算法的规则......原始指针是紧密打包的(由于标准布局),所以它们那里,所以数组访问也可能没问题,不确定。
  • 根据 cppreference 这种技巧实际上是禁止数组使用的:“如果指向的类型与数组元素类型不同,<...>,指针运算的行为是未定义的。”订阅是根据指针算法定义的。目前还不清楚使用uintptr_t 是否合法。
  • @numzero: "unique_ptr 是标准布局对象" 是吗?标准哪里有这么说的?我刚刚做了一个检查,unique_ptr 的定义中没有任何地方说它是标准布局。
猜你喜欢
  • 2011-09-23
  • 1970-01-01
  • 2016-02-19
  • 1970-01-01
  • 1970-01-01
  • 2019-12-17
  • 2021-03-02
  • 2014-04-02
  • 2022-01-25
相关资源
最近更新 更多