【问题标题】:How to let a `std::vector<int32_t>` take memory from a `std::vector<uint32_t>&&`?如何让`std::vector<int32_t>`从`std::vector<uint32_t>&&`中获取内存?
【发布时间】:2018-12-13 05:09:37
【问题描述】:
template<typename T>
struct raster {
    std::vector<T> v;

    template<typename U, typename = std::enable_if_t<sizeof(T) == sizeof(U)>>
    raster(raster<U>&& other){
        // What code goes here?
    }
}

假设我们有raster&lt;uint32_t&gt; r 使得r.v.size() 有数百万,并保证它的所有元素都在int32_t 的范围内。 raster&lt;int32_t&gt;::raster(raster&lt;uint32_t&gt;&amp;&amp; other) 是否可以避免复制内存支持other.v

或者我应该只执行*reinterpret_cast&lt;raster&lt;int32_t&gt;*&gt;(&amp;r) 之类的操作而不是调用该构造函数?

【问题讨论】:

  • @FrançoisAndrieux 实际上我想保留每个元素的价值。但是int32_tuint32_t 对可相互表达的整数有相同的表示吗? (即从 [0 2^31-1])
  • 有保证吗? [2^31, 2^32-1] 范围内什么都没有?
  • @JHBonarius 期望的行为是让被盗的内存按原样重新解释。
  • 如果要窃取的对象的size()(而不是capacity())为零,那么关于类型的讨论是否重要?在这种情况下,甚至 sizeof 类型都不重要(但对齐确实如此):在这种情况下,所有 vector&lt;T&gt; 持有的是原始内存。但是没有合法的途径获得它。
  • @C.M.除非TU 是同一类型,否则您不能reinterpret_cast 一个std::vector&lt;T&gt; 指针或引用另一个std::vector&lt;U&gt; 指针或引用。取消引用结果指针始终是未定义的行为。

标签: c++ c++14 stdvector move-semantics reinterpret-cast


【解决方案1】:

在 C++ 中没有合法的方法可以做到这一点;您只能将缓冲区从一个 std::vector 移动到另一个完全相同类型的 std::vector

有多种方法可以破解它。最违法最邪恶的应该是

std::vector<uint32_t> evil_steal_memory( std::vector<int32_t>&& in ) {
  return reinterpret_cast< std::vector<uint32_t>&& >(in);
}

或类似的东西。

一种不那么邪恶的方法是完全忘记它是std::vector

template<class T>
struct buffer {
  template<class A>
  buffer( std::vector<T,A> vec ):
    m_begin( vec.data() ),
    m_end( m_begin + vec.size() )
  {
    m_state = std::unique_ptr<void, void(*)(void*)>(
      new std::vector<T,A>( std::move(vec) ),
      [](void* ptr){
        delete static_cast<std::vector<T,A>*>(ptr);
      }
    );
  }
  buffer(buffer&&)=default;
  buffer& operator=(buffer&&)=default;
  ~buffer() = default;
  T* begin() const { return m_begin; }
  T* end() const { return m_end; }
  std::size_t size() const { return begin()==end(); }
  bool empty() const { return size()==0; }
  T* data() const { return m_begin; }
  T& operator[](std::size_t i) const {
    return data()[i];
  }
  explicit operator bool() const { return (bool)m_state; }

  template<class U>
  using is_buffer_compatible = std::integral_constant<bool,
    sizeof(U)==sizeof(T)
    && alignof(U)==alignof(T)
    && !std::is_pointer<T>{}
  >;
  template<class U,
    std::enable_if_t< is_buffer_compatible<U>{}, bool > = true
  >
  buffer reinterpret( buffer<U> o ) {
    return {std::move(o.m_state), reinterpret_cast<T*>(o.m_begin()),reinterpret_cast<T*>(o.m_end())};
  }
private:
  buffer(std::unique_ptr<void, void(*)(void*)> state, T* b, T* e):
    m_state(std::move(state)),
    m_begin(begin),
    m_end(end)
  {}
  std::unique_ptr<void, void(*)(void*)> m_state;
  T* m_begin = 0;
  T* m_end = 0;
};

live example:此类型擦除T 的缓冲区。

template<class T>
struct raster {
  buffer<T> v;

  template<typename U, typename = std::enable_if_t<sizeof(T) == sizeof(U)>>
  raster(raster<U>&& other):
    v( buffer<T>::reinterpret( std::move(other).v ) )
  {}
};

请注意,我的buffer 中有一个内存分配;与便宜的数百万个元素相比。它也是只能移动的。

可以通过仔细使用小缓冲区优化来消除内存分配。

我会让它只移动(谁想不小心复制一百万个元素?),也许写

buffer clone() const;

这会创建一个具有相同内容的新缓冲区。

请注意,在上述设计下,您应该使用buffer&lt;const int&gt; 而不是const buffer&lt;int&gt;。您可以通过复制 begin() const 方法来更改它,使其具有 const 和非 const 版本。

此解决方案依赖于您的信念,即将int32_ts 的缓冲区重新解释为uint32_ts 的缓冲区(反之亦然)不会造成任何问题。您的编译器可能会提供这种保证,但 C++ 标准没有

【讨论】:

  • buffer&lt;T&gt;的构造函数的模板参数T不应该命名不同吗?还是根本不在场?
  • 我在这里看不到类型擦除的好处,您使用 std::unique_ptr&lt;void, void(*)(void*)&gt; 来模拟重新解释演员表,它不会提供更多安全性。而通常的reinterpret_cast 方式也很常见。
  • @liliscent 我不会重新解释 vector 本身,而是重新解释向量拥有的缓冲区。缓冲区是一堆普通的旧数据;向量是由 C++ 标准库创建的不透明类型。我个人发现重新解释缓冲区远没有重新解释 std 中的类型那么疯狂。 std::vector&lt;uint32_t&gt;std::vector&lt;int32_t&gt; 的二进制布局似乎可能相似,这就是它可能工作的原因,但 没有人 将保证这一点。另一方面,相邻的int32_tuint32_t 的缓冲区相同是合理的。
  • @Walter 谢谢,已修复。可能我应该编译我的代码。 ;)
  • @Museful 深UB;可能会发生时间旅行。您的代码是内联的,编译器证明没有人修改过std::vector&lt;int32_t&gt;。毕竟evil_steal_memory只修改了vector&lt;uint32_t&gt;。因此,当您在 10 行前检查 int32_t 向量上的 .empty() 时,我们在 5 行中再次检查,我们可以跳过该检查;从逻辑上讲,如果不执行未定义的行为,它就无法变为空。简而言之,这是对类型系统的恶意破坏,而且是危险的
【解决方案2】:

问题在于向量模板本身的实现可能是专门针对该类型的。由于一些我们今天不明白的奇怪原因,顶级向量可能需要向量中未提供的额外成员,因此简单地重新解释强制转换不会安全地工作。

另一种邪恶的方法可能是查看两个向量的分配器。 如果这是

  • 您编写的自定义分配器,两者都是向量的派生类
  • 并且您在类中为 swap 和 =&& 编写了一个重载
  • 您在其中创建了要与之交换的包装 tmp 向量
  • 并检测到分配器在这些临时对象的构造函数/析构函数中被调用,并且在同一个线程上
  • 释放和重新分配相同大小的数组

那么,也许您可​​以在不重置内容的情况下合法地在它们之间传递内存缓冲区。

但这是很多脆弱的工作!

【讨论】:

    猜你喜欢
    • 2018-12-02
    • 2023-03-07
    • 1970-01-01
    • 2015-01-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-10-23
    • 2015-04-08
    相关资源
    最近更新 更多