【问题标题】:Use std::vector<double> to access data managed by std::unique_ptr<double[2]>使用 std::vector<double> 访问由 std::unique_ptr<double[2]> 管理的数据
【发布时间】:2020-10-09 12:40:04
【问题描述】:

我有一个复杂的类,它包含一大块由智能指针管理的double[2] 类型数据,例如:std::unique_ptr&lt;double[2]&gt; m_data; 我无法更改数据结构的类型。

我正在使用一个库,它为我提供了一个具有以下签名的函数:bool func_in_lib(std::vector<double>& data, double& res)。我无法更改此函数的签名。

我想将unique_ptr 管理的数据传递给期望vector&lt;double&gt;&amp; 的函数,而不会中断与我的复杂类的连接。我希望该函数直接在我的 m_data 上工作,而不是将数据复制到 std::vector&lt;double&gt; 并将其复制回我的复杂类,因为我必须这样做很多次。

有什么办法吗?


这是一些涵盖我想要的语义的代码。我关心的代码行是

vector&lt;double&gt; access_vec = /* give access to my_data via vector interface */;


#include <iostream>
#include <memory>
#include <vector>

using namespace std;

//--------------------------------------------------------------------------//
//--- This function is given, I cannot change its signature.
bool
func_in_lib(std::vector<double>& data, double& res) {
  //--- check some properties of the vector
  if (data.size() < 10)
    return false;
  //--- do something magical with the data
  for (auto& d : data)
    d *= 2.0;
  res = 42.0;
  return true;
}

//--------------------------------------------------------------------------//
struct DataType {
  double a = 1.0;
  double b = 2.0;
  double c = 3.0;
};

//--------------------------------------------------------------------------//
ostream&
operator<<(ostream& out, const DataType& d) {
  out << d.a << " " << d.b << " " << d.c << endl;
  return out;
}

//--------------------------------------------------------------------------//
int
main(int argc, char const* argv[]) {
  int count = 20;
  //--- init and print my data
  unique_ptr<DataType[]> my_data = make_unique<DataType[]>(count);
  for (int i = 0; i < count; ++i)
    cout << my_data.get()[i];
  //---
  double         result     = 0.0;
  vector<double> access_vec = /* give access to my_data via vector interface */;
  func_in_lib(access_vec, result);

  return 0;
}

【问题讨论】:

  • 如果其他变量或指针改变了它的值,unique_ptr 的用途将失效。为什么不对这种特殊情况使用原始指针或共享指针。
  • "我希望该函数直接在我的 m_data 上工作,而不是将数据复制到 std::vector" - std::vector 是一个类,并且存在除了复制它之外,没有任何方法可以将原始 unique_ptr 数组转换为 std::vector。 (它也不通过引用获取值)也许您可以使用std::vector&lt;std::reference_wrapper&lt;double&gt;&gt;,但这需要您更改签名,但据我所知,std::vector&lt;double&gt; 将无法实现您想要的。跨度>
  • @Umar:我不想传递unique_ptr,我只想传递数据。我可以使用my_data.get() 访问原始指针。下一步是我的问题。我无法将此原始指针传递给函数,因为它需要对向量的引用。我不知道如何“转换”或将此指针转换为 std::vector 而不复制它。
  • @Ruks:恐怕你是对的。
  • 为什么通过non-const引用传递给func_in_lib的向量?该函数是否应该填充/更新向量?

标签: c++ stl unique-ptr


【解决方案1】:

这不是一个合理的答案,但没有人提到(因为它肯定不会直接回答您的问题)C++17 polymorphic allocator 与 std::pmr::vector 在某种意义上他们可以轻松完成一半的工作.

但是不幸的是,不可能回到通常的 std::vector

我还看到了Bartek's coding blog的一篇文章,我从中偷了下面的代码sn-p:

#include <iostream>
#include <memory_resource>   // pmr core types
#include <vector>            // pmr::vector
#include <cctype>

template <typename T> void MyToUpper(T& vec)    {
    for(auto & cr:vec)
        cr = std::toupper(cr);
}

//https://www.bfilipek.com/2020/06/pmr-hacking.html

int main() {
    char buffer[64] = {}; // a small buffer on the stack
    std::fill_n(std::begin(buffer), std::size(buffer) - 1, '_');
    std::cout << buffer << "\n\n";

    std::pmr::monotonic_buffer_resource pool{std::data(buffer), std::size(buffer)};

    std::pmr::vector<char> vec{ &pool };
    for (char ch = 'a'; ch <= 'z'; ++ch)
        vec.push_back(ch);
        
    std::cout << buffer << "\n\n";
    
    MyToUpper(vec);
    
    std::cout << buffer << '\n';
}

coliru 下的潜在结果(注:c++17)

_______________________________________________________________

aababcdabcdefghabcdefghijklmnopabcdefghijklmnopqrstuvwxyz______

aababcdabcdefghabcdefghijklmnopABCDEFGHIJKLMNOPQRSTUVWXYZ______

文章提到垃圾部分(aababcdabcdefghabcdefghijklmnop)是由于矢量数据在增长时重新分配。

但这里有趣的是,对向量执行的操作确实是在原始缓冲区上完成的(abcdefghijklmnopqrstuvwxyz => ABCDEFGHIJKLMNOPQRSTUVWXYZ)

很遗憾,std::pmr::vector 不适合您的功能 func_in_lib(std::vector&lt;double&gt;&amp; data, double&amp; res)

我认为您购买了该库并且无法访问代码并且无法重新编译它,但相反您可以使用 templates 或者只是告诉您的提供商使用 std 添加 ::pmr::vector; 在其代码的开头...

【讨论】:

    【解决方案2】:

    我和我的一位同事找到了两个解决方案,解决了我的问题。

    解决方案 1 - hacky 一个

    这个想法是使用std::vector&lt;double&gt;的底层实现的结构,它由在我的例子中的3个成员组成,其中包含3个指向向量数据的指针。

    1. 数据段的起始地址
    2. 数据段的结束地址
    3. 数据段当前最大容量的地址

    所以我构建了一个包含这三个地址的struct,并将reinterpret_cast 用于std::vector。这适用于我机器上std::vector 的当前实现。此实现可能会有所不同,具体取决于已安装的 STL 版本。

    这里的好处是,我可以使用std::vector 的界面而无需创建它。我也不必将数据复制到std::vector。我也可以从存储在我的复杂类中的初始数据中获取一部分。我可以通过发送给结构的指针来控制被操作的部分。


    这解决了 我的问题,但它是一个 hack。我可以使用它,因为代码只与我自己相关。我仍然发布它,因为它可能会引起其他人的兴趣。

    #include <iostream>
    #include <memory>
    #include <vector>
    
    using namespace std;
    
    //--------------------------------------------------------------------------//
    //--- This function is given, I cannot change its signature.
    bool
    func_in_lib(std::vector<double>& data, double& res) {
      //--- check some properties of the vector
      if (data.size() < 10)
        return false;
      //--- do something magical with the data
      for (auto& d : data)
        d *= 2.0;
    
      res = 42.0;
      return true;
    }
    
    //--------------------------------------------------------------------------//
    struct DataType {
      double a = 1.0;
      double b = 2.0;
      double c = 3.0;
    };
    
    //--------------------------------------------------------------------------//
    ostream&
    operator<<(ostream& out, const DataType& d) {
      out << d.a << " " << d.b << " " << d.c << endl;
      return out;
    }
    
    //--------------------------------------------------------------------------//
    int
    main(int argc, char const* argv[]) {
      int count = 20;
      //--- init and print my data
      unique_ptr<DataType[]> my_data = make_unique<DataType[]>(count);
      for (int i = 0; i < count; ++i)
        cout << my_data.get()[i];
      
      //--------------------------------------------------------------------------//
      // HERE STARTS THE UGLY HACK, THAT CAN BE ERROR-PRONE BECAUSE IT DEPENDS ON
      // THE UNDERLYING IMPLEMENTATION OF std::vector<T>
      //--------------------------------------------------------------------------//
      struct VecAccess {
        double* start = nullptr; // address to the start of the data
        double* stop0 = nullptr; // address to the end of the data
        double* stop1 = nullptr; // address to the capacity of the vector
      };
    
      //---
      DataType*       p_data = my_data.get();
      VecAccess       va{ &(p_data[0].a),                //points at the 'front' of the vector
                          &(p_data[count - 1].c) + 1,    //points at the 'end' of the vector
                          &(p_data[count - 1].c) + 1 };
      vector<double>* p_vec_access = reinterpret_cast<vector<double>*>(&va);
      //--------------------------------------------------------------------------//
      // HERE ENDS THE UGLY HACK.
      //--------------------------------------------------------------------------//
    
      //---
      double dummy = 0.0;   // this is only relevant for the code used as minimum example
      func_in_lib(*p_vec_access, dummy);
    
      //--- print the modified data
      for (int i = 0; i < count; ++i)
        cout << my_data.get()[i];
    
      return 0;
    }
    
    

    更新:分析第二个解决方案的汇编代码表明,即使未调用数据对象的复制构造函数,也会执行内容的副本。复制过程发生在机器代码级别。

    解决方案 2 - 移动语义

    对于这个解决方案,我必须用noexcept 标记DataType 的移动构造函数。 关键思想是不将DataType[] 数组视为std::vector&lt;double&gt;。 相反,我们将std::vector&lt;double&gt; 视为std::vector&lt;DataType&gt;。我们可以 然后将数据 移动 到这个向量中(不复制),将其发送给函数, 然后再把它移回来。

    数据不是复制而是移动std::vector,速度更快。同样与我的案例相关,我可以再次从存储在我的复杂类中的初始数据中获取一部分。 此解决方案的缺点是我必须为 以正确的大小移动数据。

    #include <iostream>
    #include <memory>
    #include <utility>
    #include <vector>
    
    using namespace std;
    
    //--------------------------------------------------------------------------//
    //--- This function is given, I cannot change its signature.
    bool
    func_in_lib(std::vector<double>& data, double& res) {
      //--- check some properties of the vector
      if (data.size() < 10)
        return false;
      //--- do something magical with the data
      for (auto& d : data)
        d *= 2.0;
    
      res = 42.0;
      return true;
    }
    
    //--------------------------------------------------------------------------//
    class DataType {
    public:
      double a = 1.0;
      double b = 2.0;
      double c = 3.0;
    
      // clang-format off
      DataType() = default;
      DataType(DataType const&) = default;
      DataType(DataType&&) noexcept = default;
      DataType& operator=(DataType const&) = default;
      DataType& operator=(DataType&&) noexcept  = default;
      ~DataType()  = default;
      // clang-format on
    };
    
    //--------------------------------------------------------------------------//
    ostream&
    operator<<(ostream& out, const DataType& d) {
      out << d.a << " " << d.b << " " << d.c << endl;
      return out;
    }
    
    //--------------------------------------------------------------------------//
    int
    main(int argc, char const* argv[]) {
      int count = 20;
      //--- init and print my data
      unique_ptr<DataType[]> my_data = make_unique<DataType[]>(count);
      for (int i = 0; i < count; ++i)
        cout << my_data.get()[i];
      //---
      vector<double> double_vec;
      double_vec.reserve(count * 3);
      //--- here starts the magic stuff
      auto& vec_as_datatype = *reinterpret_cast<vector<DataType>*>(&double_vec);
      auto* start_mv        = &(my_data.get()[0]);
      auto* stop_mv         = &(my_data.get()[count]) + 1;
      //--- move the content to the vec
      move(start_mv, stop_mv, back_inserter(vec_as_datatype));
      //--- call the external func in the lib
      double dummy = 0.0; // is only needed for the code of the example
      func_in_lib(double_vec, dummy);
      //--- move the content to back
      move(begin(vec_as_datatype), end(vec_as_datatype), start_mv);
      //--- print modified the data
      for (int i = 0; i < count; ++i)
        cout << my_data.get()[i];
    }
    

    【讨论】:

      【解决方案3】:

      tl;dr:不可能以符合标准的方式。

      实际上几乎是可能的,但std::allocator 的限制阻碍了你的前进。让我解释一下。

      • std::vector“拥有”它用于存储元素的内存:向量有权delete[] 内存(例如在销毁时,或在移动后销毁,或.resize(),或@ 987654330@ 等)并在其他地方重新分配。如果您想保持unique_ptr 的所有权,您不能允许这种情况发生。虽然你的 func_in_lib() 的模拟实现确实没有做任何这些 - 你的代码不能做出这些假设,因为它必须迎合函数的声明,而不是它的主体。

      但是假设您愿意稍微改变规则,并假设向量在运行时不会替换其分配的内存。这是合法的,从某种意义上说 - 如果您能够传递内存以供矢量以某种方式使用,并且它替换了内存区域,您可以检测到 func_in_lib() 返回时,然后在 unique_ptr 或抛出异常(取决于代码中的其他位置是否包含指向废弃内存的指针)。或者 - 让我们假设 func_in_lib() 采用 const std::vector&lt;double[2]&gt;&amp; 而不是非常量引用。我们的路还是会被堵死。为什么?

      • std::vector 通过分配器对象管理内存。分配器是一个模板,所以理论上你可以使用一个向量,分配器可以做任何你想做的事情 - 例如,从预先分配的内存(你给它 - 从unique_ptr::get()开始,并拒绝重新分配任何内存,例如,通过抛出异常。由于其中一个 std::vector 构造函数采用适当类型的分配器 - 您可以构建所需的分配器,用它创建一个向量,然后传递对该向量的引用。

      但是很可惜——你的图书馆太残忍了。 func_in_lib 没有模板化,只能为其分配器采用默认模板参数:std::allocator。

      • 用于std::vector 和其他标准库容器的默认分配器是std::allocator。现在,在我看来,分配器通常是一个错误的想法。但std::allocator 特别烦人。具体来说,不能使用预先存在的内存区域来构造它以供它使用;它只保存它自己分配的内存——绝不是你给它的内存。

      所以,你永远不会得到std::vector 来使用你想要的内存。

      那该怎么办?

      1. 选项 1:你的 hack:

        • 找出系统上std::vector的具体布局
        • 手动将字段值设置为有用的值
        • 对您的原始数据使用reinterpret_cast&lt;std::vector&gt;()。
      2. 选项 2:malloc() 和 free() 钩子(如果您在类 Unix 系统上和/或使用使用 libc 的编译器)

        • 见:Using Malloc Hooks

          这个想法是从你创建的std::vector 中检测new[] 调用,并给它你自己的unique_ptr 控制的内存,而不是实际分配任何东西。当向量要求释放内存时(例如在销毁时),你什么也不做。

      3. 切换库。暴露func_in_lib 的库写得不好。除非它是一个非常利基的库,否则我相信会有更好的选择。事实上,你可以自己编写它来做得更好。

      4. 不要在库中使用该特定函数;坚持使用库中较低级别的简单原语并使用它们实现func_in_lib()。并不总是可行,但可能值得做空。

      【讨论】:

      • 感谢您的详细解答。以下是我的一些想法:2)这可能是一个解决方案,因为我在 Linux 系统上工作。 3) 仅供参考,它是"NonLinear Optimization" 库。这些家伙在实现优化算法方面做得很好。我目前使用 C++ 接口。还有一个 C 接口,它提供了一个基于指针/内存的接口。 4)我想过。不幸的是,大部分代码都放在一个巨大的对象类型中,有很多私有成员。重新实现 my 类更容易。
      • @ThomasWilde:因此,库本身可能很棒,但 C++ 包装器很差。一个已知的现象。但是......似乎the library is FOSS,所以你可以使用它的自定义版本。或者您可以要求他们将该接口更改为采用一对迭代器,或者一个std::span,或者如果他们不需要模板但与旧 C++ 版本兼容,甚至可以使用指针+大小。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-01-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-09-14
      • 2017-12-27
      • 2012-04-26
      相关资源
      最近更新 更多