是否可以在C ++中序列化和反序列化std::function
,函数对象或闭包?怎么样? C ++ 11是否有助于此?是否有任何图书馆支持可用于此类任务(例如,在Boost中)?
例如,假设一个C ++程序有一个std::function
需要与另一台机器上的另一个C ++程序进行通信(比如通过TCP / IP套接字)。在这种情况下你有什么建议?
<小时/> 修改
为了澄清,要移动的功能应该是纯粹的,没有副作用的。所以我没有安全或状态不匹配问题。
该问题的解决方案是构建一个小的嵌入式域特定语言并序列化其抽象语法树。 我希望我能找到一些语言/库支持来代替移动与机器无关的函数表示。
答案 0 :(得分:10)
是函数指针和闭包。不适用于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()));
}
但我很惊讶地发现,即使没有重新编译,函数的地址也会在每次调用程序时发生变化。如果要传输地址,则不是很有用。这是由ASLR引起的,您可以通过your_program
启动setarch $(uname -m) -LR your_program
来关闭Linux。
现在您可以将函数指针发送到运行相同程序的其他计算机,并调用它! (这不涉及传输可执行代码。但除非您在运行时生成可执行代码,否则我认为您不是在寻找它。)
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函数后面的未命名类的引用。 (这是functional中的_Function_handler
。)std::function
接受一个指向此包装类的静态方法(_M_invoke
)的函数指针,并存储该值以及闭包值。
不幸的是,所有内容都隐藏在private
成员中,并且不会存储闭包值的大小。 (它不需要,因为lambda函数知道它的大小。)
所以std::function
不适合序列化,但作为蓝图很有效。我跟着它做了什么,简化了很多(我只想序列化lambdas,而不是无数其他可调用的东西),在size_t
中保存了闭包值的大小,并添加了(de)序列化的方法。它有效!
答案 1 :(得分:6)
没有
C ++没有对序列化的内置支持,从来没有考虑过将代码从一个进程传输到另一个进程的想法,以免一台机器到另一台机器。可能这样做的语言通常同时具有IR(与机器无关的代码的中间表示)和反射。
因此,您只需编写一个用于传输所需操作的协议,而DSL方法肯定是可行的......取决于您希望执行的各种任务以及对性能的需求。
另一种解决方案是使用现有语言。例如,Redis NoSQL数据库嵌入了LUA引擎并可以执行LUA脚本,您也可以这样做并在网络上传输LUA脚本。
答案 2 :(得分: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
本地范围?)