在使用C++开发大型桌面应用的场景下,需要频繁的修改函数调用参数并执行,通过编码方式修改会给开发者带来困扰,因为需要重新编译.
那么是可以在函数执行前弹出界面来调整参数,然后再执行的.这意味着针对每个参数不同的函数都需要提供相应界面,会增加开发者的工作量.
是否能够提供一种函数对象,以用来设计通用的界面,完成相应场景的需求? 这里展示一种实现方法.
场景分析
如果想要设计通用的界面,则就需要通用的函数对象和函数参数:
class IArgument{
public:
virtual ~IArgument()=default;
virtual std::string name() const noexcept = 0;
virtual Type type() const noexcept = 0;
};
class Argument:public IArgument {
public:
T value() const noexcept;
void setValue(T v) noexcept;
};
class ICallback{
public:
virtual ~ICallback()=default;
virtual void execute() = 0;
virtual std::string name() = 0;
virtual std::vector<IArgument*> argument() noexcept = 0;
};
复制代码
通过全局的ICallback仓库,可以获取所有可调用函数,根据名称得到函数对象ICallback,又能够获取参数对象IArgument,界面修改后执行ICallback的execute即可.
界面的主要工作量在于针对不同的参数类型,提供相应的界面显示和设置.
实现思路
提供参数基类IArgument
提供通用的参数实现Argument
提供参数包实现Arguments
提供函数基类ICallback
使用Arguments实现通用的函数类Callback
这里假设有个函数,接收 1 个整数iV、1 个浮点数dV、1 个字符串sV作为输入参数,执行时打印到命令行,则代码类似如下:
Callback<int, double, std::string> cb([](int iV,double dV,std::string sV) { //输出到命令行 std::cout << "iV:" << iV << "\ndV:" << dV << "\nsV:" << sV << "\n";},{"iV","dV","sV"});//参数名称
//提供默认的参数cb.args.set<0, int>(1024);cb.args.set<1, double>(3.1415926);cb.args.set<2, std::string>("liff.engineer@gmail.com");
//执行函数cb();
复制代码
那么这个Callback使用方式如下:
void callback_example(ICallback& cb){ //查询参数 auto iV = cb.argumentAs<int>("iV"); auto dV = cb.argumentAs<double>("dV"); auto sV = cb.argumentAs<std::string>("sV"); if (iV) { std::cout << "iV:" << iV.value() << "\n"; } if (dV) { std::cout << "dV:" << dV.value() << "\n"; } if (sV) { std::cout << "sV:" << sV.value() << "\n"; }
//执行 cb.execute();
//修改参数 cb.setArgument("iV", 123456); cb.setArgument("dV", 1.717); cb.setArgument("sV", std::string{"Garfield"}); //执行 cb.execute();}
复制代码
下面来看一看是如何一步步实现的.
参数接口 IArgument 及其实现 Argument
为了简单期间,这里只展示部分实现方法,首先看一下参数接口IArgument:
class IArgument{public: virtual ~IArgument() = default; //判断是哪种参数类型 template<typename T> bool is() const noexcept; //转换为哪种参数类型的参数 template<typename T> T& as() & noexcept;};
复制代码
然后是通用的参数实现Argument<T>,通过模板实现,支持任意可复制的参数类型:
template<typename T>class Argument final:public IArgument{public: T v; Argument(T v_arg) :v(std::move(v_arg)) {};};
复制代码
这样之前IArgument的两个模板函数就可以通过如下方式实现:
//简单期间,这里使用RTTI做类型判断template<typename T>bool IArgument::is() const noexcept{ return dynamic_cast<const Argument<T>*>(this) != nullptr;}
//直接使用静态cast,有效性判断使用istemplate<typename T>T& IArgument::as() & noexcept { return static_cast<Argument<T>*>(this)->v;}
复制代码
这样对于示例中的函数,其函数参数可以结合Argument与std::tuple来定义:
std::tuple<Argument<int>,Argument<double>,Argument<std::string>> args;
复制代码
通用参数包实现 Arguments
对于函数的输入参数,通常格式和类型都不固定,可以提供函数参数包Arguments<Ts...>,来应对各种需求,譬如之前示例的函数,其参数包定义如下:
//定义参数包Arguments<int, double, std::string> args;
//调整参数值args.set<0, int>(1024);args.set<1, double>(3.1415926);args.set<2, std::string>("liff.engineer@gmail.com");
//重置参数args.reset<0>();
//获取参数auto v = args.at(1)->as<double>();
args.set<0, int>(8192);
复制代码
即,参数包需要按照参数顺序和类型定义,修改时由于类型确定,稍微麻烦,不过可以提供IArgument方式修改.其定义如下:
template<typename... Ts>class Arguments { //参数存储,这里采用std::optional使得参数可以不存在,使用时需小心 std::tuple<std::optional<Argument<Ts>>...> m_values; //参数指针存储,用来提供IArgument std::array<IArgument*, sizeof...(Ts)> m_pointers{};public: Arguments() { //默认参数为空,指针也为空 m_pointers.fill(nullptr); }; //其它成员函数实现 template<std::size_t I> IArgument* at() noexcept { //根据参数位置获取参数接口 return std::get<I>(m_pointers); }
IArgument* at(std::size_t i) { //根据参数位置获取参数接口 return m_pointers.at(i); } //其它成员函数实现 };
复制代码
提供set和reset接口用来填充/修改参数值:
template<typename... Ts>class Arguments {public: //设置参数值 template<std::size_t I,typename T> void set(T v) { if (IArgument* arg = std::get<I>(m_pointers)) { if (!arg->is<T>()) { throw std::invalid_argument("invalid setting"); } arg->as<T>() = v; } else { //更新值并刷新指针 std::get<I>(m_values) = v; std::get<I>(m_pointers) = std::get<I>(m_values).operator->(); } } //重置参数值 template<std::size_t I> void reset() { std::get<I>(m_pointers) = nullptr; std::get<I>(m_values).reset(); }};
复制代码
由于m_pointers存储的就是m_values中的指针,一旦值发生变化,就要刷新m_pointers对应内容,提供refresh:
template<typename... Ts>class Arguments {public: //其它实现 //刷新某个函数 template<std::size_t I> void refresh() noexcept { if (std::get<I>(m_values)) { //获取值地址并更新指针 std::get<I>(m_pointers) = std::get<I>(m_values).operator->(); } }
template<std::size_t ...Is> void refreshImpl(std::index_sequence<Is...> is) noexcept { (refresh<Is>(), ...); }
void refresh() { refreshImpl(std::make_index_sequence<sizeof...(Ts)>{}); }};
复制代码
当函数调用时,需要获取存储在参数包的原始信息,即std::tuple<int,double,std::string>,然后转换成(int iV,double dV,std::string sV),以此来驱动函数,所以提供as接口将其转换成原始参数:
template<typename... Ts>class Arguments {public: //其它实现 template<std::size_t ...Is> std::tuple<Ts...> asImpl(std::index_sequence<Is...> is) const { return std::make_tuple(std::get<Is>(m_values).value().v...); }
decltype(auto) as() const { return asImpl(std::make_index_sequence<sizeof...(Ts)>{}); }};
复制代码
这样通用的参数包就实现完成了,下面来看函数的实现.
函数接口 ICallback
接口需要包含执行和参数访问、设置等内容,其定义如下:
class ICallback{public: virtual ~ICallback() = default; //执行函数 virtual void execute() = 0; //根据名称获取参数 virtual IArgument* argument(std::string_view name) noexcept = 0;
//设置参数 template<typename T> void setArgument(std::string_view name, T v) { auto arg = argument(name); if (arg && arg->is<T>()) { arg->as<T>() = v; } }
//获取参数 template<typename T> std::optional<T> argumentAs(std::string_view name) noexcept { auto arg = argument(name); if (arg && arg->is<T>()) { return arg->as<T>(); } return {}; }};
复制代码
通用函数实现 Callback
Callback只需要利用Arguments,保存函数地址,并添加参数名称,提供出ICallback所需接口实现即可:
template<typename ... Ts>class Callback:public ICallback{ //与参数一一对应的名称 std::array<std::string_view, sizeof...(Ts)> m_names; //函数地址,可以替换成function或者继续派生,将类实例及其成员函数绑定成为Callback void(*m_addr)(Ts...) = nullptr;public: //参数列表 Arguments<Ts...> args;public: Callback() = default; Callback(void(*f)(Ts...), std::array<std::string_view, sizeof...(Ts)> names) :m_addr(f), m_names(names) {}; explicit operator bool() const noexcept { return m_addr != nullptr; } //作为仿函数调用 void operator()() { std::apply(m_addr, args.as()); } //ICallback的执行接口 void execute() override { std::apply(m_addr, args.as()); } //ICallback的参数接口 IArgument* argument(std::string_view name) noexcept override { for (std::size_t i = 0; i < m_names.size(); i++) { if (m_names[i] == name) { return args.at(i); } } return nullptr; }};
复制代码
Callback对应于示例,使用方式如下:
//参数包Arguments<int, double, std::string> args;args.set<0, int>(1024);args.set<1, double>(3.1415926);args.set<2, std::string>("liff.engineer@gmail.com");
//函数对象Callback<int, double, std::string> cb([](int iV,double dV,std::string sV) { std::cout << "iV:" << iV << "\ndV:" << dV << "\nsV:" << sV << "\n"; }, {"iV","dV","sV"});
//利用之前的参数包,这里复制完要刷新cb.args = args;cb.args.refresh();
//执行函数,修改参数,再执行cb();cb.args.set<1, double>(1.414);cb();
复制代码
总结
以上只是原理性的简单实现,真实场景会略显复杂,需要酌情修改.
通过这种实现方法,可以将函数转换成可检视、编辑参数,并且能够调用的对象,再通过相对通用的界面/命令行交互,就可以在不修改代码的情况下重新执行某些代码片段了.
评论