pybind11 - 如果访问了结构的任何成员,则 boost::optional 结构的 std::vector 成员将被清空

Posted

技术标签:

【中文标题】pybind11 - 如果访问了结构的任何成员,则 boost::optional 结构的 std::vector 成员将被清空【英文标题】:pybind11 - std::vector member of boost::optional struct is emptied if any members of the struct are accessed 【发布时间】:2020-07-07 11:18:18 【问题描述】:

我在 C++ 中有一个包含 3 个结构的分层数据结构,我在我的最小工作示例 (mwe) House、Room 和 Objects 中对其进行了重命名。 Room 是 boost::optional 类型,并且有一个 std::vector<Object> 成员,其中包含此房间中的所有对象。对象只是一些数字的容器。

我知道这对于此类信息来说过于复杂,但在原始代码中是必需的,并且不能轻易更改。由于我们不使用 c++17,因此我尝试将其更改为 std::experimental::optional,但这破坏了我们 c++ 代码中的某些部分,我不知道它是否真的能解决问题。

在 C++ 中,我完全没有问题,boost::optional 和所有成员变量都可以正常工作。但是在绑定所有内容后,我遇到了一个奇怪的问题,即只要我访问 Room 的任何成员变量,std::vector 对象就会被清空。这可以是长度、宽度、面积或对象本身,如 Python 示例中所示。如果对象第一次被访问,它们实际上是正常返回的,但是当尝试第二次访问时,它们就像被移动了一样消失了。如果您执行x = myHouse.kitchen.objects.copy(),同样的行为也适用。该列表在x 中,甚至可以多次访问,但厨房中的信息立即丢失。奇怪的是,这也只适用于对象,所有其他双重成员都可以无限期地访问。

为了方便编译所有东西给mwe和调试,我把所有东西都塞进了一个cpp文件中,这在原始代码中显然不是这样。对于 mwe,c++ 代码是通过 cppimport 编译和包含的,但手动编译也不会改变任何内容。

这是mwe:

mwe.cpp:


#include <boost/optional.hpp>
#include <iostream>
#include <map>
#include <string>
#include <vector>
#include <pybind11/complex.h>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>

/* implementing structs */
struct Object 
  Object() = default;
  Object(double price, double height) : price(price), height(height) 
  ;

  double price = 0.0;
  double height = 0.0;
;

struct Room 
  Room() = default;

  double length;
  double width;
  std::vector<Object> objects; // this is the buggy vector

  double area() const 
    return length * width;
  ;
;

struct House 
  House() = default;

  boost::optional<Room> bedroom;
  boost::optional<Room> kitchen;
  boost::optional<Room> livingroom;

  std::map<std::string, std::vector<Object>> getObjects() 
    std::map<std::string, std::vector<Object>> out;
    if (bedroom) 
      out.insert(std::make_pair("bedroom", bedroom->objects));
    
    if (kitchen) 
      out.insert(std::make_pair("kitchen", kitchen->objects));
    
    if (livingroom) 
      out.insert(std::make_pair("livingroom", livingroom->objects));
    
    return out;
  ;
;

/* everything works fine in C++ -> get data this way to have complete object map */
House initSomethingInCpp() 
  auto myHouse = House();
  myHouse.bedroom = Room();
  myHouse.kitchen = Room();

  myHouse.bedroom->length = 10.0;
  myHouse.bedroom->width = 2.0;
  myHouse.kitchen->length = 5.0;
  myHouse.kitchen->width = 3.0;

  std::vector<Object> bedroomObjects;
  std::vector<Object> kitchenObjects;

  Object closet = Object(100.0, 2.5);
  Object bed = Object(200.0, 1.0);
  Object oven = Object(500.0, 1.5);
  Object table = Object(50.0, 1.5);

  bedroomObjects.push_back(closet);
  bedroomObjects.push_back(bed);
  kitchenObjects.push_back(oven);
  kitchenObjects.push_back(table);

  myHouse.bedroom->objects = bedroomObjects;
  myHouse.kitchen->objects = kitchenObjects;

  return myHouse;
;

namespace pybind11 
/* taking care of boost type */
namespace detail 
/* boost::optional */
template<typename T>
struct type_caster<boost::optional<T>> : optional_caster<boost::optional<T>> ;
 // namespace detail
 // namespace pybind11


/* binding structs */
void init_house(pybind11::module& main) 
  pybind11::class_<House> house(main, "House");
  house.def(pybind11::init<>());

  house.def_readwrite("bedroom", &House::bedroom);
  house.def_readwrite("kitchen", &House::kitchen);
  house.def_readwrite("livingroom", &House::livingroom);
  house.def("get_objects", &House::getObjects);
;

void init_room(pybind11::module& main) 
  pybind11::class_<Room> room(main, "Room");
  room.def(pybind11::init<>());
  room.def_readwrite("length", &Room::length);
  room.def_readwrite("width", &Room::width);
  room.def_readwrite("objects", &Room::objects);
  room.def_property_readonly("area", &Room::area);
;

void init_objects(pybind11::module& main) 
  pybind11::class_<Object> object(main, "Object");
  object.def(pybind11::init<>());
  object.def(pybind11::init<double, double>());

  object.def_readonly("price", &Object::price);
  object.def_readonly("heigth", &Object::height);
;

/* define module and bind init_in_cpp function */
PYBIND11_MODULE(mwe, m) 
 init_house(m);
 init_room(m);
 init_objects(m);
 m.def("init_something_in_cpp", &initSomethingInCpp);
;

执行.py:

import cppimport
#cppimport.set_quiet(False)
#cppimport.force_rebuild()
mod = cppimport.imp('mwe')

# get data
myHouse = mod.init_something_in_cpp()

print("\n")
print("all data is here")
objs = myHouse.get_objects()
print(objs)
print(myHouse.kitchen.area) # by accessing kitchen members, the objects list is emptied
print("\n")

print("kitchen objects are now missing")
objs = myHouse.get_objects()
print(objs)
print("but area still works:")
print(myHouse.kitchen.area) # everything but objects still works
print("\n")

print("also works directly with same variable")
print("bedroom objects are accessed:")
print(myHouse.bedroom.objects)
print("bedroom objects are accessed again:")
print(myHouse.bedroom.objects)

执行给出以下输出:

all data is here
'bedroom': [mwe.Object object at 0x7fbc9c2a43f0, mwe.Object object at 0x7fbc9c2a4670], 'kitchen': [mwe.Object object at 0x7fbc9c2a4c30, mwe.Object object at 0x7fbc9c2a4df0]
15.0

kitchen objects are now missing
'bedroom': [mwe.Object object at 0x7fbc9c2a4e70, mwe.Object object at 0x7fbc9c2a4eb0], 'kitchen': []
but area still works:
15.0

also works directly with same variable
bedroom objects are accessed:
[mwe.Object object at 0x7fbc9c2a4c30, mwe.Object object at 0x7fbc9c2a4df0]
bedroom objects are accessed again:
[]

【问题讨论】:

【参考方案1】:

原来这实际上是 pybind11 release 2.5 https://github.com/pybind/pybind11/issues/1919中的一个错误@

在当前的 master 分支和未来的版本中已修复。

【讨论】:

以上是关于pybind11 - 如果访问了结构的任何成员,则 boost::optional 结构的 std::vector 成员将被清空的主要内容,如果未能解决你的问题,请参考以下文章

结构应用举例

构造和析构的次序

ref readonly 的 C# 行为

使用pybind11开发python扩展库

malloc sizeof 结构的各个成员?

有没有办法让一个值只能被嵌套类 VB.NET 的父级访问?