通过使用 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++ 独立函数:consumer
和 another_consumer
,它们显示了尝试通过此接口传递和修改 std::vector<int>
的示例。在consumer
的情况下,这将失败,并且它的行为就像A::func
的参数正在按值传递。但是,当我们在 Python 中创建 std::vector<int>
并将其传递给 another_consumer
时,事情会按预期进行,vector<int>
会被 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 对象身份保存找到现有的 Pythonx
,将其重用于发送到 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++ 接口,所以你也可以首先简单地传递&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 代码不会搜索可重用的指针。但是由于第二种解决方案(使用&vec
)非常简单,可能它隐藏在文档中的某个地方......(虽然我找不到它。)
你说得对,第二个解决方案非常简单,但我必须记住这是一个棘手的细节。如果它不是错误,那么绝对值得在文档中强调。不过,您的回答将使我们再次前进,感谢您的帮助!以上是关于通过使用 pybind11 的虚函数通过引用传递 std::vector 的问题的主要内容,如果未能解决你的问题,请参考以下文章