需求背景
有时候静态编译太不灵活,我们需要更灵活的运行时操作。
又或真假设你在开发一个脚本,想注册本地的C++函数到脚本语言的标准库中。例如gdscript的Callable。
下面是一个我的一个简单的实现。我们假设脚本语言中的变量类型是std::any。根据情况不同实现细节也可能不同。
实现
首先我们需要对可调用对象进行一个抽象的表示。
struct Callable {
int _argc = 0;
virtual std::any call(const std::vector<std::any>& args) = 0;
virtual ~Callable() = default;
};
很简单,定义一个抽象方法call,参数列表使用vector<any>来表示,。然后用argc表示这个可调用的对象可接收多少个参数。
类方法
这里仅仅实现一个比较复杂的可调用对象,类方法。这是我们准备的测试类。
struct Foo {
int a = 10;
int foo(int n, char ch, const std::string* msg)
{
a += n;
cout << ch << ": " << *msg << a << endl;
return a;
}
int bar(int n)
{
cout << "--" << n << "--" << endl;
return n;
}
};
应当注意的是,类方法并不能简单的转换为函数指针。
比如上述的bar的类型不是int(*)(int)
,而是int(Foo::*)(int)
。应当注意这点。随后我们继承这个Callable,一个名为ClassMethod的类来包装类方法。这里我们实现了使用了和std::functional差不多的形式。为了表达直观,我们用类似ClassMethod<ClassName, int(int)>
这样的形式,而不是ClassMethod<int,int>
这样的形式。
template<typename... Args>
struct ClassMethod; //永远也不会匹配到这个模板,所以不用定义。
template<typename ClassName, typename Ret, typename ... Args>
struct ClassMethod<ClassName, Ret(Args...)> : public Callable {/*TODO*/}
//使用方式
<ClassMethod<Foo, int(int, char, const std::string*)>> callable;
Ret表示返回值,Args表示一个参数包,也就是参数的类型列表。后续我们使用包展开来对其进行操作。
ClassMethod内部:
static constexpr int Argc = sizeof...(Args);
using Callee = Ret(ClassName::*)(Args...);
Argc可在编译阶段求解,Callee是一个别名,定义方式就是上述所说的类方法的类型。不过是换成了模板参数。
类方法在调用的时候实际上隐藏了一个参数,就是this指针。所以我们需要一个指向对象的指针来表示this,和一个Callee来表示调用的具体是哪个方法。因为不同的方法可能有相同的参数名称。
ClassMethod内部:
ClassName* _class = nullptr;
Callee _callee = nullptr;
ClassName* set_class(ClassName* ptr)
{
ClassName* old = _class;
_class = ptr;
return old;
}
Callee* set_callee(Callee callee)
{
Callee* old = _callee;
_callee = callee;
return old;
}
我们需要对Args,也就是参数包,进行转发调用。
Ret call_impl(Args&& ... args)
{
return (_class->*_callee)(std::forward<Args>(args)...);
}
你可能会惊讶_class->*_callee
这样的调用形式,但实际上这就是一个固定的写法,没什么可惊讶的,
剩下的最后一部就是将any进行类型转换并检查类型。这一步我们重写call来完成:
any call(const std::vector<std::any>& args) override
{
//检查参数个数是否匹配
if (args.size()!=Argc) {
std::string msg = std::format("args size not match expect {} but is {}", Argc, args.size());
throw std::logic_error(msg.c_str());
}
//应当如何包展开这个Args
auto ret = call_impl(??? Args...);
return ret;
}
别忘了any_cast,这可以进行类型检查。但我们同时还需要一个计数器,来看表示我们检查对象所处参数中的索引位置。这样才能访问到对应位置的参数。使用一个类包装这个操作。
struct CallHelper {
int index;
explicit CallHelper(int argc)
:index(argc-1) { }
template<typename T>
T operator()(const std::vector<any>& list)
{
try { //捕获异常,添加Index的提示信息
auto ret = std::any_cast<T>(list[index]);
index--;
return ret;
}
catch (const std::bad_any_cast& err) { // 再抛出异常,或者干点他什么的都可以
auto msg = std::format("{}. Index: {}.", err.what(), index);
throw std::logic_error(msg.c_str());
}
}
};
注意每次调用index不是从0开始递增,而是从argc-1开始向0递减。这是因为包展开后是如下的形式:
// Args && ... args
std::forward<Args...>(args);
//假设Args为int,double,char
//会被展开为这样的形式
std::forward<int>(v1),std::forward<int>(v2),std::forward<int>(v3)
展开成一系列的由逗号分割的表达式。重要的是——他是从右向左求值的。
any call(const std::vector<std::any>& args) override
{
if (args.size()!=Argc) {
std::string msg = std::format("args size not match expect {} but is {}", Argc, args.size());
throw std::logic_error(msg.c_str());
}
//*******
CallHelper helper(Argc);
auto ret = call_impl(helper.operator()<Args>(args)...);
//********
return ret;
}
Args会按照helper.operator()
这个模式进行展开,从右向左的求值,使用index进行any的索引。区别于Args && .. arg
中的args,这处的args只是一个vector<any>,和call_impl的args不是一个东西。也就是说,它会被展开成为这样:
//假设Args是int,char,double
helper.operator()<Args>(args)
//会被展开为
helper.operator()<int>(args),helper.operator()<char>(args),helper.operator()<double>(args)
测试:
int main()
{
Foo foo;
std::unique_ptr<Callable> test_foo = std::make_unique<ClassMethod<Foo, int(int, char, const std::string*)>>(&foo,
&Foo::foo);
std::unique_ptr<Callable> test_bar = std::make_unique<ClassMethod<Foo,int(int)>>(&foo,&Foo::bar);
test_bar->call({10});
string msg = "hello world";
test_foo->call({make_any<int>(10), make_any<char>('A'), make_any<const std::string*>(&msg)});
try {
test_foo->call({});
} catch (const std::exception & e) {
cerr << e.what() << endl;
}
try {
test_foo->call({1,2,3});
} catch (const std::exception & e) {
cerr << e.what() << endl;
}
}
输出:
--10--
A: hello world20
args size not match expect 3 but is 0
bad any_cast. Index: 2.
进程已结束,退出代码为 0
至于其他的可调用类型例如函数指针,函数对象,则都可以使用std::functional来当作中间层来实现,就不展开说了。
完全的代码
#include <iostream>
#include <vector>
#include <memory>
#include <any>
#include <format>
using namespace std;
struct Foo {
int a = 10;
int foo(int n, char ch, const std::string* msg)
{
a += n;
cout << ch << ": " << *msg << a << endl;
return a;
}
int bar(int n)
{
cout << "--" << n << "--" << endl;
return n;
}
};
struct Callable {
int _argc = 0;
virtual std::any call(const std::vector<std::any>& args) = 0;
virtual ~Callable() = default;
};
struct CallHelper {
int index;
explicit CallHelper(int argc)
:index(argc-1) { }
template<typename T>
T operator()(const std::vector<any>& list)
{
try {
auto ret = std::any_cast<T>(list[index]);
index--;
return ret;
}
catch (const std::bad_any_cast& err) {
auto msg = std::format("{}. Index: {}.", err.what(), index);
throw std::logic_error(msg.c_str());
}
}
};
template<typename... Args>
struct ClassMethod;
template<typename ClassName, typename Ret, typename ... Args>
struct ClassMethod<ClassName, Ret(Args...)> : public Callable {
static constexpr int Argc = sizeof...(Args);
using Callee = Ret(ClassName::*)(Args...);
explicit ClassMethod(ClassName* _class_name, Callee callee)
{
_class = _class_name;
_callee = callee;
_argc = sizeof...(Args);
}
ClassName* _class = nullptr;
Callee _callee = nullptr;
ClassName* set_class(ClassName* ptr)
{
ClassName* old = _class;
_class = ptr;
return old;
}
Callee* set_callee(Callee callee)
{
Callee* old = _callee;
_callee = callee;
return old;
}
Ret call_impl(Args&& ... args)
{
return (_class->*_callee)(std::forward<Args>(args)...);
}
any call(const std::vector<std::any>& args) override
{
if (args.size()!=Argc) {
std::string msg = std::format("args size not match expect {} but is {}", Argc, args.size());
throw std::logic_error(msg.c_str());
}
CallHelper helper(Argc);
auto ret = call_impl(helper.operator()<Args>(args)...);
return ret;
}
};
int main()
{
Foo foo;
std::unique_ptr<Callable> test_foo = std::make_unique<ClassMethod<Foo, int(int, char, const std::string*)>>(&foo,
&Foo::foo);
std::unique_ptr<Callable> test_bar = std::make_unique<ClassMethod<Foo,int(int)>>(&foo,&Foo::bar);
test_bar->call({10});
string msg = "hello world";
test_foo->call({make_any<int>(10), make_any<char>('A'), make_any<const std::string*>(&msg)});
try {
test_foo->call({});
} catch (const std::exception & e) {
cerr << e.what() << endl;
}
try {
test_foo->call({1,2,3});
} catch (const std::exception & e) {
cerr << e.what() << endl;
}
}