16

是否可以std::function在 C++ 中序列化和反序列化 a 、函数对象或闭包?如何?C++11 是否促进了这一点?是否有任何库支持可用于此类任务(例如,在 Boost 中)?

例如,假设一个 C++ 程序std::function需要与驻留在另一台机器上的另一个 C++ 程序进行通信(比如通过 TCP/IP 套接字)。在这种情况下你有什么建议?


编辑:

澄清一下,要移动的函数应该是纯的且无副作用的。所以我没有安全或状态不匹配的问题。

该问题的解决方案是构建一个小型嵌入式领域特定语言并序列化其抽象语法树。我希望我能找到一些语言/库支持来移动与机器无关的函数表示。

4

3 回答 3

12

是的,用于函数指针和闭包。不为std::function

函数指针是最简单的——它只是一个和其他指针一样的指针,因此您可以将其读取为字节:

template <typename _Res, typename... _Args>
std::string serialize(_Res (*fn_ptr)(_Args...)) {
  return std::string(reinterpret_cast<const char*>(&fn_ptr), sizeof(fn_ptr));
}

template <typename _Res, typename... _Args>
_Res (*deserialize(std::string str))(_Args...) {
  return *reinterpret_cast<_Res (**)(_Args...)>(const_cast<char*>(str.c_str()));
}                   

但令我惊讶的是,即使不重新编译,函数的地址也会在程序的每次调用时发生变化。如果您想传输地址,则不是很有用。这是由于ASLRyour_program造成的,您可以在 Linux 上以.开头将其关闭setarch $(uname -m) -LR your_program

现在您可以将函数指针发送到运行相同程序的不同机器,然后调用它!(这不涉及传输可执行代码。但除非您在运行时生成可执行代码,否则我认为您不会在寻找那个。)

lambda 函数完全不同。

std::function<int(int)> addN(int N) {
  auto f = [=](int x){ return x + N; };
  return f;
}

的值f将是捕获的int N. 它在内存中的表示与int! 编译器为 lambda 生成一个未命名的类,它f是一个实例。这个类已经operator()用我们的代码重载了。

未命名的类给序列化带来了问题。它还提出了从函数返回 lambda 函数的问题。后一个问题由std::function.

std::function据我了解是通过创建一个模板化的包装类来实现的,该类通过模板类型参数有效地保存对 lambda 函数背后的未命名类的引用。(这是_Function_handler函数式中。)std::function接受一个指向_M_invoke这个包装类的静态方法()的函数指针,并存储它加上闭包值。

不幸的是,所有内容都隐藏在private成员中,并且没有存储闭包值的大小。(它不需要,因为 lambda 函数知道它的大小。)

所以std::function不适合序列化,但可以很好地作为蓝图。我遵循它的做法,对其进行了很多简化(我只想序列化 lambda,而不是无数其他可调用的东西),将闭包值的大小保存在 a 中size_t,并添加了用于(反)序列化的方法。有用!

于 2014-03-31T21:08:49.220 回答
8

不。

C++ 没有对序列化的内置支持,并且从未设想过将代码从一个进程传输到另一个进程的想法,以免将一台机器传输到另一台机器。可以这样做的语言通常同时具有 IR(与机器无关的代码的中间表示)和反射。

因此,您只需要自己编写一个协议来传输您想要的操作,而 DSL 方法当然是可行的……这取决于您希望执行的任务的种类和对性能的需求。

另一种解决方案是使用现有语言。例如 Redis NoSQL 数据库嵌入了一个 LUA 引擎并且可以执行 LUA 脚本,你也可以这样做并在网络上传输 LUA 脚本。

于 2012-09-09T14:04:36.683 回答
0

不,但有一些受限的解决方案。

您最希望的是在发送代码和接收代码(在不同的计算机或序列化前后)通用的某种全局映射(例如,使用键字符串)中注册函数。然后,您可以序列化与函数关联的字符串并在另一端获取它。

作为一个具体的例子,HPX 库在HPX_ACTION中实现了类似的东西。

这需要大量协议,并且在代码更改方面很脆弱。

但毕竟这与试图用私有数据序列化一个类的东西没有什么不同。在某种意义上,函数的代码是它的私有部分(参数和返回接口是公共部分)。

让您望而却步的是,根据您组织代码的方式,这些“对象”可以是全局的或通用的,如果一切顺利,它们可以在序列化和反序列化期间通过某种预定义的运行时间接使用。

这是一个粗略的例子:

序列化代码:

// common:
class C{
  double d;
  public:
  C(double d) : d(d){}
  operator(double x) const{return d*x;}
};
C c1{1.};
C c2{2.};
std::map<std::string, C*> const m{{"c1", &c1}, {"c2", &c2}};
// :common

main(int argc, char** argv){
   C* f = (argc == 2)?&c1:&c2;
   (*f)(5.); // print 5 or 10 depending on the runtime args
   serialize(f); // somehow write "c1" or "c2" to a file
}

解串器代码:

// common:
class C{
  double d;
  public:
  operator(double x){return d*x;}
};
C c1;
C c2;
std::map<std::string, C*> const m{{"c1", &c1}, {"c2", &c2}};
// :common

main(){
   C* f;
   deserialize(f); // somehow read "c1" or "c2" and assign the pointer from the translation "map"
   (*f)(3.); // print 3 or 6 depending on the code of the **other** run
}

(代码未经测试)。

请注意,这会强制使用许多通用且一致的代码,但取决于环境,您可能能够保证这一点。代码中最轻微的变化都会产生难以检测的逻辑错误。

另外,我在这里玩过全局对象(可以在自由函数上使用),但同样可以用作用域对象来完成,更棘手的是如何在本地建立映射(#include本地范围内的公共代码?)

于 2018-10-10T08:54:48.380 回答