通过使用 pybind11 的虚函数通过引用传递 std::vector 的问题

Posted

技术标签:

【中文标题】通过使用 pybind11 的虚函数通过引用传递 std::vector 的问题【英文标题】:Problems passing a std::vector by reference though virtual functions using pybind11 【发布时间】:2019-12-13 21:55:18 【问题描述】:

我有一些 C++ 代码通过pybind11 绑定到 Python,我们遇到了一个奇怪的问题,即通过虚拟方法通过引用传递 std::vector 并且对该 std::vector 的更改在该方法之外持续存在.

我们想要做的是提供一个 C++ 纯虚函数,它通过引用接受 std::vector,以便重写此方法的 Python 类可以修改该 std::vector,并将其交还给其他 C++代码。我们正在关注pybind11 documentation 关于制作the STL code opaque,但在我们的测试中,我们发现在 Python 中创建 STL 向量时,STL 向量被正确修改并通过引用传递,但是如果我们在C++ 不是。

这是一个独立的示例来演示该问题。首先是 C++ pybind11 代码,我们在其中创建一个抽象基类 A,其中包含一个纯虚方法 A::func,我们希望从 Python 中覆盖它:

#include "pybind11/pybind11.h"
#include "pybind11/stl_bind.h"
#include "pybind11/stl.h"
#include "pybind11/functional.h"
#include "pybind11/operators.h"

#include <iostream>
#include <vector>

namespace py = pybind11;
using namespace pybind11::literals;

PYBIND11_MAKE_OPAQUE(std::vector<int>)

//------------------------------------------------------------------------------
// The C++ types and functions we're binding
//------------------------------------------------------------------------------
struct A 
  virtual void func(std::vector<int>& vec) const = 0;
;

// In this function the std::vector "x" is not modified by the call to a.func(x)
// The difference seems to be that in this function we create the std::vector
// in C++
void consumer(A& a) 
  std::vector<int> x;
  a.func(x);
  std::cerr << "consumer final size: " << x.size() << std::endl;


// Whereas here, with the std::vector<int> created in Python and passed in, the
// std::vector "x" is modified by the call to a.func(x).
// The only difference is we create the std::vector in Python and pass it in here.
void another_consumer(A& a, std::vector<int>& x) 
  std::cerr << "another_consumer initial size: " << x.size() << std::endl;
  a.func(x);
  std::cerr << "another_consumer final size  : " << x.size() << std::endl;


//------------------------------------------------------------------------------
// Trampoline class for A
//------------------------------------------------------------------------------
class PYB11TrampolineA: public A 
public:
  using A::A;
  virtual void func(std::vector<int>& vec) const override  PYBIND11_OVERLOAD_PURE(void, A, func, vec); 
;

//------------------------------------------------------------------------------
// Make the module
//------------------------------------------------------------------------------
PYBIND11_MODULE(example, m) 
  py::bind_vector<std::vector<int>>(m, "vector_of_int");

  
    py::class_<A, PYB11TrampolineA> obj(m, "A");
    obj.def(py::init<>());
    obj.def("func", (void (A::*)(std::vector<int>&) const) &A::func, "vec"_a);
  

  m.def("consumer", (void (*)(A&)) &consumer, "a"_a);
  m.def("another_consumer", (void (*)(A&, std::vector<int>&)) &another_consumer, "a"_a, "x"_a);

我们还创建了两个 C++ 独立函数:consumeranother_consumer,它们显示了尝试通过此接口传递和修改 std::vector&lt;int&gt; 的示例。在consumer 的情况下,这将失败,并且它的行为就像A::func 的参数正在按值传递。但是,当我们在 Python 中创建 std::vector&lt;int&gt; 并将其传递给 another_consumer 时,事情会按预期进行,vector&lt;int&gt; 会被 A::func 修改到位。这是演示差异的 Python 代码(假设上面的 C++ 被编译成一个名为 example 的模块:

from example import *

class B(A):
    def __init__(self):
        A.__init__(self)
        return
    def func(self, vec):
        print "B.func START: ", vec
        vec.append(-1)
        print "B.func STOP : ", vec
        return

b = B()
print "--------------------------------------------------------------------------------"
print "consumer(b) -- This one seems to fail to pass back the modified std::vector<int> from B.func"
consumer(b)
print "--------------------------------------------------------------------------------"
print "another_consumer(b, x) -- This one works as expected"
x = vector_of_int()
another_consumer(b, x)
print "x : ", x

执行 Python 示例会导致:

--------------------------------------------------------------------------------
consumer(b) -- This one seems to fail to pass back the modified std::vector<int> from B.func
B.func START:  vector_of_int[]
B.func STOP :  vector_of_int[-1]
consumer final size: 0        
--------------------------------------------------------------------------------
another_consumer(b, x) -- This one works as expected
another_consumer initial size: 0
B.func START:  vector_of_int[]
B.func STOP :  vector_of_int[-1]
another_consumer final size  : 1
x :  vector_of_int[-1]            

那么我们在这里误解了什么?为什么第一个示例,函数consumer,未能将其本地副本 std::vector 修改到位并通过 Python 方法 B.func 的引用返回?

【问题讨论】:

第二个工作 b/c 对象身份保存找到现有的 Python x,将其重用于发送到 B.func 的绑定对象。我不明白的是,在第一种情况下复制发生在哪里(您使用 PYBIND11_MAKE_OPAQUE 应该可以防止这种情况发生)。 【参考方案1】:

以下内容可能不令人满意,但它是如此简单的“作弊”并且让你继续前进,我想我不妨发布它。正如上面评论中所说,第二种情况适用于 b/c,python 代理对象被发现已经存在,因此被重用。您可以自己玩该游戏,只需将蹦床代码更改为:

class PYB11TrampolineA: public A 
public:
  using A::A;
  virtual void func(std::vector<int>& vec) const override  
    py::object dummy = py::cast(&vec);   // force re-use in the following call
    PYBIND11_OVERLOAD_PURE(void, A, func, vec); 
  
;

然后两个消费者函数都会按预期工作。

编辑: 就此而言,由于问题在于传递给可调用对象的参数的转换(这里:B.func),并且因为这是一个 Python 而不是 C++ 接口,所以你也可以首先简单地传递&amp;vec

class PYB11TrampolineA: public A 
public:
  using A::A;
  virtual void func(std::vector<int>& vec) const override  
    PYBIND11_OVERLOAD_PURE(void, A, func, &vec); 
  
;

在 pybind11 内部,它相当于同一件事。

EDIT2:找到了原因。它在 pybind11/2114.h 的第 2114 行:使用 std::forward 删除了引用,动机似乎是阻止创建临时的 python 代理。最好有专门的对象引用。

【讨论】:

嗯。你是对的——我确认在蹦床上添加 py::cast 可以使这个案例有效。原来的行为似乎是意外行为,这实际上是 pybind11 错误吗?感谢您的建议! 可能是错误;绝对不一致......在 cppyy (cppyy.org) 中,我也一直使用“C++ 类型 + 指针”作为对象标识,如果匹配,代理会被重新使用,所以我同意那部分。但是,如果按值或复制(内部或来自 C++),则不会有未完成的 C++ 指针,因此没有代理,并且 cppyy 代码不会搜索可重用的指针。但是由于第二种解决方案(使用&amp;vec)非常简单,可能它隐藏在文档中的某个地方......(虽然我找不到它。) 你说得对,第二个解决方案非常简单,但我必须记住这是一个棘手的细节。如果它不是错误,那么绝对值得在文档中强调。不过,您的回答将使我们再次前进,感谢您的帮助!

以上是关于通过使用 pybind11 的虚函数通过引用传递 std::vector 的问题的主要内容,如果未能解决你的问题,请参考以下文章

使用 Python、C++ 和 pybind11 返回和传递原始 POD 指针(数组)

使用 pybind11 将抽象类作为参数作为参数传递

如何通过pybind11在python中捕获C++的异常?

使用pybind11开发python扩展库

在 pybind11 中引用 C++ 分配的对象

Pybind11 用于 C++ 代码,内部结构通过静态工厂方法创建