任何可以为 pybind11 导出结构体所有成员变量的 C++ 宏

Jim*_*hen 7 c++ c++11 pybind11

我有一个简单的结构,例如:

struct Config {
  bool option1;
  bool option2;
  int arg1;
};
Run Code Online (Sandbox Code Playgroud)

使用 pybind11,我必须导出成员变量,例如:

py::class_<Config>(m, "Config")
    .def_readwrite("option1", &Config::option1)
    .def_readwrite("option2", &Config::option2)
    .def_readwrite("arg1", &Config::arg1);
Run Code Online (Sandbox Code Playgroud)

当这些结构体比较少的时候,写上面的就可以了。但当我有大量简单的结构时,它就变得乏味了。

有没有一个方便的宏,我可以这样写:

PYBIND_EXPORT_STRUCT(Config1);
PYBIND_EXPORT_STRUCT(Config2);
...
Run Code Online (Sandbox Code Playgroud)

每个扫描并导出所有给定结构的成员变量?

如果我已经以这种形式编写结构会有帮助吗:

struct Config {
    ADD_PROPERTY(bool, option1);
    ADD_PROPERTY(bool, option2);
    ADD_PROPERTY(int, arg1);
};
Run Code Online (Sandbox Code Playgroud)

我的问题涉及两个部分:

  1. 将成员变量反映回其名称字符串。
  2. 遍历成员。struct

我知道自省可以解决第一部分,使用typeid(arg1).name()检索名称字符串。

对于第二部分,C++不直接支持。然而,我试图通过这里的一些答案来弄清楚。

剩下的问题是如何融合上述两部分以获得我想象的PYBIND_EXPORT_STRUCT()功能的有效实现。

也就是说,我不介意以完全不同的表示形式表达我的结构(例如使用宏或元组)。只要我在使用 pybind11 导出结构成员时不必再次枚举它们,任何都可以,并且我仍然可以像config1.option1=true在 C++ 代码中一样使用变量。

kkm*_*kkm 1

1. 不解决问题如何解决

\n

您想到的两种方法都不可行也不实用。

\n
\n

我知道自省可以解决第一部分,使用typeid(arg1).name()检索名称字符串。

\n
\n

这是不正确的。C++ 有 RTTI,即运行时类型信息,但与 C#、Java 或 Python 的 \xe2\x80\x9creflection\xe2\x80\x9d 相差甚远。特别是,成员函数std::type_info::name()\xe2\x80\x9c 返回一个实现定义的包含类型名称的以 null 结尾的字符串。不提供任何保证;特别是,返回的字符串对于多种类型可以是相同的,并且在同一程序的调用之间会发生变化。\xe2\x80\x9d [亮点是我的-kkm]事实上,这个程序

\n
#include <iostream>\n#include <typeinfo>\nstruct Config { int option; };\nint main() { std::cout << typeid(&Config::option).name() << "\\n"; }\n
Run Code Online (Sandbox Code Playgroud)\n

打印,如果在 Linux x64 上使用 GCC 11 编译,

\n
M6Configi\n
Run Code Online (Sandbox Code Playgroud)\n

完全符合标准。你的第 1 部分就这样付诸东流了。类型不包含成员名称,它被称为运行时类型信息不是运行时名称信息是有原因的。您甚至可以进行有根据的猜测并解码打印的字符串:= 指向成员的指针,= 接下来的 6 个字符命名结构类型,= 明显,= 。指向类型本身的成员的指针。但另一个编译器将以不同的方式对类型进行编码(\xe2\x80\x9cmangle\xe2\x80\x9d,它是如何调用的)。M6ConfigiintConfigint

\n

关于第 2 部分,请观看这​​个CppCon 视频演示(来自您链接到的答案),了解它的真正含义:它演示了 C++14 元编程足够强大,可以提取有关 POD 类型的信息。如您所见,演示者为您可能遇到的每个成员类型声明了两个函数(intvolatile intconst intconst volatile intshort...)。我们就到此为止吧。所有这些类型都是不同的。事实上,当我volatile int option;在上面的小测试程序中更改单独结构成员的声明时,它打印了不同的损坏类型名称:M6ConfigVi

\n

CppCon 演示展示的是机器的能力,而不是它的用途。打个比方,这就像航展上飞机的滚桶式滚动对于常规客运航空公司的运营一样。如果我是你,我会避免在生产代码中使用桶滚......

\n

实际上,这对编译器来说是一个很好的测试。我曾经使用更温和的元编程结构来使编译器崩溃。此外,您可能不喜欢所有这些杂文的编译时间。不要惊讶地坐等 10 分钟,直到编译器完成编译,有四种方式之一:崩溃;内部错误报告;成功生成错误代码;或者,祈祷成功生成正确的代码。另外,您需要对元编程、编译器如何选择不同的模板重载、未评估的上下文和 SFINAE 是什么等有深刻的理解。简单来说,就是不要。它可能有效,但不值得为会议演示工作所需的大量“框架”代码,以及对于如此极其复杂的元程序的编译器正确性的不确定性。

\n

2. 如何解决问题

\n

有一种非常传统的方法可以完成您想要做的事情,即依赖于普通的旧式 C 预处理器宏。核心思想是这样的:您将结构的定义编写为类似函数的预处理器宏在一个单独的文件中,该文件不包含这些宏的定义(我们称其为“抽象定义文件”,或 ADF,因为缺少一个公认的术语)。第二个文件是您为获取结构的具体声明而包含的普通标头,它定义了这些特殊宏以扩展为普通的 C++ 结构,然后包含 ADF,然后(重要!)#undefines 它们。第三个文件创建 Python 绑定,首先包含头文件,然后定义相同的宏但不同(这就是为什么#undefs 很重要!),这次它们扩展为 pybind11 语法结构;然后第二次将 ADF 包含在同一编译单元中。现在让我们把整个事情放在一起。

\n

第一个文件是 ADF structs.absdef,. 我不会给它传统的.h扩展名,以防止它与“正常”头文件混淆。扩展名可以是您想要的任何内容,但在项目中选择一个唯一的扩展名有助于向代码阅读器发出信号,表明这不是“正常”包含文件。

\n
/* structs.absdef -- abstract definition of data structures */\n\n#ifndef BEGIN_STRUCT_DEF\n#error "This file should be included only from structs.h or pybind.cc"\n#endif\n\nBEGIN_STRUCT_DEF(Config)\n  STRUCT_MEMBER(Config, bool, option1)\n  STRUCT_MEMBER(Config, bool, option2)\n  STRUCT_MEMBER(Config, int, arg1)\nEND_STRUCT_DEF()\n\n/* ... and then structs, structs and more structs ... */\n
Run Code Online (Sandbox Code Playgroud)\n

//只是如果在包含文件之前未定义预处理器宏,则立即停止编译#ifndef;否则,您将收到一大堆编译错误,这些错误更有可能产生误导,而不是对诊断问题有帮助。#error#endif

\n

该文件将包含在第二个文件中,第二个文件是普通的 C++ 标头,它定义了 C++ 语法中的所有结构。这是您作为普通、简单且无聊的 C++ 标头包含到 C++ 源文件和/或其他包含文件中的文件,您希望这些结构体的声明在其中可见。

\n
/* structs.h -- C++ concrete definitions of data structures */\n\n#ifndef MYPROJECT_STRUCTS__H\n#define MYPROJECT_STRUCTS__H\n\n#define BEGIN_STRUCT_DEF(stype)            struct stype {\n#define STRUCT_MEMBER(stype, mtype, name)    mtype name;\n#define END_STRUCT_DEF()                   };\n\n#include "structs.absdef"\n\n#undef BEGIN_STRUCT_DEF\n#undef STRUCT_MEMBER\n#undef END_STRUCT_DEF\n\n#endif  // MYPROJECT_STRUCTS__H\n
Run Code Online (Sandbox Code Playgroud)\n

这里需要注意的一件事是该文件包含防护,但 ADT 没有。之所以如此,是因为它通过 pybind 调用两次包含在编译单元中。这个 C++ 文件很特殊:它将相同的 ADT 定义转换为 pybind 语法。我不知道 pybind 是如何工作的;我正在盲目地复制你的例子。

\n
/* pybind.cc -- Generate pybind11 Python bindings */\n\n#include "pybind11.h" // All these #include   ...\n#include "other.h"    // ... directives stand ...\n#include "stuff.h"    // ... for the real McCoy.\n\n#include "structs.h"  /* You need "normal" C++ definitions, too! */\n\n// We rely here on the ADF having had #undef\'d its definition of these.\n// The preprocessor does not allow silently redefining macros.\n#define BEGIN_STRUCT_DEF(stype)            py::class_<stype>(m, #stype)\n#define STRUCT_MEMBER(stype, mtype, name)   .def_readwrite(#name, &stype::name)\n#define END_STRUCT_DEF()                   ;\n\nvoid create_pybind_bindings() {\n  // The ADF is included the second time in the CU.\n  #include "structs.absdef"\n}\n\n// Not necessary, but customary to avoid polluting the preprocessor\n// namespace, unless the C++ source ends right here.\n#undef BEGIN_STRUCT_DEF\n#undef STRUCT_MEMBER\n#undef END_STRUCT_DEF\n
Run Code Online (Sandbox Code Playgroud)\n

需要注意的两点。

\n

首先,类函数宏和左括号之间没有空格:

\n
// Correct:\n#define FOO(x) ((x) + 42)\n// In this statement:\nint j = FOO(1);\n// `FOO(1)\' expands by replacing `x\' with `1\' into:\nint j = ((1) + 42);\n\n// Incorrect:\n//         v--- A feral space attacks!!! Everyone seek shelter!!!\n#define BAR (x) ((x) + 42)\n// Since BAR is not a function-like macro, it expands literally\n// as defined into `(x) ((x) + 42)\', such that this:\nint j = BAR(1);\n// expands into:\nint j = (x) ((x) + 42)(1);\n
Run Code Online (Sandbox Code Playgroud)\n

ieBAR被按字面意思完全替换为它出现的位置。当你的编译器试图消化结果时,它必须说的是一堆垃圾错误,当然不是“错误:你在 和 之间插入了一个空格BAR(,所以要小心。

\n

第二点是使用预处理器的字符串化运算符 #,它将后面的类似函数的宏参数扩展为双引号字符串:#sname变成"Config", 在引号中,这正是您需要传递给 pybind API 的内容。

\n

3. 奖励:一睹引擎盖下的风采

\n

显然,我们没有文件“pybind11.h”、“other.h”和“stuff.h”:它们只是占位符名称,所以我将简单地创建空文件。我从这个答案中复制了另外 3 个文件。当您编译时pybind.cc,C 预处理器首先由编译器驱动程序调用。我们将单独调用它并检查它的输出。该c++ -E <filename.cc>命令告诉编译器调用预处理器,但不是摄取结果文件,而是将其打印到标准输出并停止。

\n

我通过删除多个空行来压缩输出:预处理器剥离注释行和带有它所接受和处理的指令的行,但仍然打印生成的空行以维护诊断的正确行号,可能由下一个处理阶段输出。以 开头的额外行用于#下一次传递并且也具有相同的目的:它们只是建立正在处理的行号和文件名。为了更好的措施,请忽略它们。

\n
$ touch "pybind11.h" "other.h" "stuff.h"\n\n$ ls *.{cc,h,absdef}\nother.h  pybind.cc  pybind11.h  structs.absdef  structs.h  stuff.h\n\n$ c++ -E pybind.cc\n# 1 "pybind.cc"\n# 1 "<built-in>"\n# 1 "<command-line>"\n# 1 "/usr/include/stdc-predef.h" 1 3 4\n# 1 "<command-line>" 2\n# 1 "pybind.cc"\n\n# 1 "pybind11.h" 1\n# 4 "pybind.cc" 2\n# 1 "other.h" 1\n# 5 "pybind.cc" 2\n# 1 "stuff.h" 1\n# 6 "pybind.cc" 2\n\n# 1 "structs.h" 1\n# 10 "structs.h"\n# 1 "structs.absdef" 1\n\nstruct Config {\n  bool option1;\n  bool option2;\n  int arg1;\n};\n# 11 "structs.h" 2\n# 8 "pybind.cc" 2\n\nvoid create_pybind_bindings() {\n# 1 "structs.absdef" 1\n\npy::class_<Config>(m, "Config")\n  .def_readwrite("option1", &Config::option1)\n  .def_readwrite("option2", &Config::option2)\n  .def_readwrite("arg1", &Config::arg1)\n;\n# 15 "pybind.cc" 2\n}\n
Run Code Online (Sandbox Code Playgroud)\n

# number file flags或者,如果没有编译器打印正确的诊断上下文所需的形式提示(例如“structs.absdef:5 include from structs.h:10: error: ...”),则干净整洁的精确副本实际编译器处理的编译单元的所需代码是:

\n
struct Config {\n  bool option1;\n  bool option2;\n  int arg1;\n};\n\nvoid create_pybind_bindings() {\npy::class_<Config>(m, "Config")\n  .def_readwrite("option1", &Config::option1)\n  .def_readwrite("option2", &Config::option2)\n  .def_readwrite("arg1", &Config::arg1)\n;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

4. 版权页,或者一点聪明的说法和一点历史

\n
    \n
  • 并不是每一项新技术都因为它是新的而对所有事情都更好。
  • \n
  • 事实上,预处理器比 C 语言本身稍早一些。准确来说,49岁。C 采用了贝尔实验室内部用于其他语言的预处理器。
  • \n
\n