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

Any C++ macro that can export all member variables of a struct for pybind11

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

struct Config {
  bool option1;
  bool option2;
  int arg1;
};

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

py::class_<Config>(m, "Config")
    .def_readwrite("option1", &Config::option1)
    .def_readwrite("option2", &Config::option2)
    .def_readwrite("arg1", &Config::arg1);

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

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

PYBIND_EXPORT_STRUCT(Config1);
PYBIND_EXPORT_STRUCT(Config2);
...

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

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

struct Config {
    ADD_PROPERTY(bool, option1);
    ADD_PROPERTY(bool, option2);
    ADD_PROPERTY(int, arg1);
};

我的问题涉及两部分:

  1. 为了反射一个成员变量回到它的名字字符串。
  2. 迭代struct 个成员。

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

对于第二部分,C++不直接支持。但是,我正试图通过一些答案来弄清楚 here

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

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

对于数字 2,您可以尝试查看我的 pod_reflection 图书馆:

// main.cpp

struct Config {
  bool option1;
  bool option2;
  int arg1;
};

#include <pod_reflection/pod_reflection.h>
#include <iostream>

int main()
{
  std::cout << "Config size: " << eld::pod_size<Config>() << std::endl;
  std::cout << std::boolalpha;
  Config conf{true, false, 815};
  eld::for_each(conf, [](const auto& i){ std::cout << i << std::endl; });
  return 0;
}

CMakeLists.txt:

cmake_minimum_required(VERSION 3.7.2 FATAL_ERROR)

project(pod_example)

add_subdirectory(pod_reflection)

add_executable(main main.cpp)
target_link_libraries(main eld::pod_reflection)

可以遍历pod的基本类型元素。一组类型也可以通过 template<typename ... ArgsT> using extend_feed:

扩展为用户自定义类型
using my_feed = extend_feed<std::string, foo>;
eld::for_each<my_feed>(pod, callableVisitor);

您可以使用 eld::deduced& get<I, TupleFeed>(POD& pod) 填充 py::class_<Config>。但是,由于库不可能知道 pod 成员的名称,因此您必须想办法从 I 中推断出它们。如果没有适当的编译时反射,它几乎不可能实现自动化。 请注意get 使用 reinterpret_cast 通过偏移获取指向成员的指针。

1。怎么解决不了问题

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

I am aware of introspection to solve the first part, using typeid(arg1).name() to retrieve the name string.

这是不正确的。 C++ 具有 RTTI、run-time 类型信息,但与 C#、Java 或 Python 具有的“反射”相去甚远。特别地,成员函数std::type_info::name()“Returns一个实现定义了null-terminated包含类型名称的字符串。不提供任何保证;特别是,返回的字符串对于几种类型可能是相同的,并且在同一程序的调用之间会发生变化。” [亮点是我的-kkm]其实这个程序

#include <iostream>
#include <typeinfo>
struct Config { int option; };
int main() { std::cout << typeid(&Config::option).name() << "\n"; }

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

M6Configi

完全 standard-compliant。你的第 1 部分就此付诸东流。 type 不包含成员 name,它被称为 Runtime Type Information 而不是 运行时名称信息 是有原因的。您甚至可以做出有根据的猜测并解码打印的字符串:M = 指向成员的指针,6 = 接下来的 6 个字符命名结构类型,Config = 显而易见,i = int。指向 Config 类型成员的指针,int 类型本身。但是另一个编译器将以不同的方式编码(“mangle”,它的调用方式)类型。

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

CppCon 演示展示了机器的功能,而不是机器的用途。打个比方,这就是航空展上飞机的桶形滚转对例行客运航空公司运营的意义。如果我是你,我会避免生产代码中的桶滚...

实际上,这对编译器来说是一个很好的测试。我曾经遇到过使用更适度的元编程构造的编译器崩溃。此外,您可能不会喜欢所有这些 kaboodle 的编译时间。不要惊讶地坐下来等待 10 分钟,直到编译器以四种方式之一完成编译:崩溃;内部错误报告;成功生成错误代码;或者,祝你好运,成功生成正确的代码。此外,您需要对元编程、编译器如何选择不同的模板重载、未计算的上下文和 SFINAE 是什么等等有深入的了解,我的意思是 非常非常深入 的了解。简单来说就是不要。它可能有效,但不值得让会议演示工作所需的大量“框架”代码和编译器正确性的不确定性 w.r.t。如此复杂的元程序。

2。如何解决问题

有一种非常传统的方法可以完成您想要做的事情,它依赖于普通的旧 C 预处理器宏。核心思想是这样的:你将结构的定义写成function-like preprocessor macros in a separate file, which doesn't contain the definitions for these macros (let's call it an "abstract definitions file," or ADF, for the want of an accepted term). The second file, your normal header that you include to get concrete declaration of your structures, defines these special macros to expand into normal C++ constructs, then includes the ADF, then (important!) #undefines them. The third file, that creates Python bindings, first includes the header file, then defines the same macros but differently (this is why #undefs were important!), this time in such a way that they expand to pybind11 syntactic constructs; then includes the ADF the second time in the same compilation unit。现在让我们把整个事情放在一起。

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

/* structs.absdef -- abstract definition of data structures */

#ifndef BEGIN_STRUCT_DEF
#error "This file should be included only from structs.h or pybind.cc"
#endif

BEGIN_STRUCT_DEF(Config)
  STRUCT_MEMBER(Config, bool, option1)
  STRUCT_MEMBER(Config, bool, option2)
  STRUCT_MEMBER(Config, int, arg1)
END_STRUCT_DEF()

/* ... and then structs, structs and more structs ... */

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

此文件将包含在第二个文件中,这是您的普通 C++ header,它定义了 C++ 语法中的所有结构。这是您作为普通、普通和无聊的 C++ header 包含到您的 C++ 源代码中的文件 and/or 其他包含文件,您希望这些结构的声明在其中可见。

/* structs.h -- C++ concrete definitions of data structures */

#ifndef MYPROJECT_STRUCTS__H
#define MYPROJECT_STRUCTS__H

#define BEGIN_STRUCT_DEF(stype)            struct stype {
#define STRUCT_MEMBER(stype, mtype, name)    mtype name;
#define END_STRUCT_DEF()                   };

#include "structs.absdef"

#undef BEGIN_STRUCT_DEF
#undef STRUCT_MEMBER
#undef END_STRUCT_DEF

#endif  // MYPROJECT_STRUCTS__H

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

/* pybind.cc -- Generate pybind11 Python bindings */

#include "pybind11.h" // All these #include   ...
#include "other.h"    // ... directives stand ...
#include "stuff.h"    // ... for the real McCoy.

#include "structs.h"  /* You need "normal" C++ definitions, too! */

// We rely here on the ADF having had #undef'd its definition of these.
// The preprocessor does not allow silently redefining macros.
#define BEGIN_STRUCT_DEF(stype)            py::class_<stype>(m, #stype)
#define STRUCT_MEMBER(stype, mtype, name)   .def_readwrite(#name, &stype::name)
#define END_STRUCT_DEF()                   ;

void create_pybind_bindings() {
  // The ADF is included the second time in the CU.
  #include "structs.absdef"
}

// Not necessary, but customary to avoid polluting the preprocessor
// namespace, unless the C++ source ends right here.
#undef BEGIN_STRUCT_DEF
#undef STRUCT_MEMBER
#undef END_STRUCT_DEF

需要注意的两点

首先,在function-like宏和左括号之间没有space

// Correct:
#define FOO(x) ((x) + 42)
// In this statement:
int j = FOO(1);
// `FOO(1)' expands by replacing `x' with `1' into:
int j = ((1) + 42);

// Incorrect:
//         v--- A feral space attacks!!! Everyone seek shelter!!!
#define BAR (x) ((x) + 42)
// Since BAR is not a function-like macro, it expands literally
// as defined into `(x) ((x) + 42)', such that this:
int j = BAR(1);
// expands into:
int j = (x) ((x) + 42)(1);

BAR 被逐字替换并准确地出现在它出现的位置。你的编译器是什么我不得不说,当它试图消化时,结果是一大堆垃圾错误,当然不是“错误:你在 BAR( 之间插入了一个 space”,所以要小心。

第二点是 preprocessor's stringizing operator # 的使用,它将其后的 function-like 宏参数扩展为 double-quoted 字符串: #sname 变成 "Config", 在引号中,这正是您需要传递给 pybind API.

3。奖励:引擎盖下的一瞥

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

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

$ touch "pybind11.h" "other.h" "stuff.h"

$ ls *.{cc,h,absdef}
other.h  pybind.cc  pybind11.h  structs.absdef  structs.h  stuff.h

$ c++ -E pybind.cc
# 1 "pybind.cc"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "pybind.cc"

# 1 "pybind11.h" 1
# 4 "pybind.cc" 2
# 1 "other.h" 1
# 5 "pybind.cc" 2
# 1 "stuff.h" 1
# 6 "pybind.cc" 2

# 1 "structs.h" 1
# 10 "structs.h"
# 1 "structs.absdef" 1

struct Config {
  bool option1;
  bool option2;
  int arg1;
};
# 11 "structs.h" 2
# 8 "pybind.cc" 2

void create_pybind_bindings() {
# 1 "structs.absdef" 1

py::class_<Config>(m, "Config")
  .def_readwrite("option1", &Config::option1)
  .def_readwrite("option2", &Config::option2)
  .def_readwrite("arg1", &Config::arg1)
;
# 15 "pybind.cc" 2
}

或者,没有 # number file flags 形式的提示,编译器只需要这些提示来打印正确的诊断上下文(例如“structs.absdef:5 included from structs.h:10:错误:...”),实际编译器处理的编译单元所需代码的漂亮干净的精确副本是:

struct Config {
  bool option1;
  bool option2;
  int arg1;
};

void create_pybind_bindings() {
py::class_<Config>(m, "Config")
  .def_readwrite("option1", &Config::option1)
  .def_readwrite("option2", &Config::option2)
  .def_readwrite("arg1", &Config::arg1)
;
}

4。 Colophon,或一点 smartassery 和一点历史

  • 并非每项新技术都更好,因为它是新的。
  • 预处理器实际上比 C 语言本身稍老。准确地说是49岁。 C 采用了贝尔实验室内部用于其他语言的预处理器。