写点什么

一种 C++ 中支持界面调用函数的实现方法

发布于: 2021 年 02 月 21 日
一种C++中支持界面调用函数的实现方法

在使用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,界面修改后执行ICallbackexecute即可.


界面的主要工作量在于针对不同的参数类型,提供相应的界面显示和设置.

实现思路


  1. 提供参数基类IArgument

  2. 提供通用的参数实现Argument

  3. 提供参数包实现Arguments

  4. 提供函数基类ICallback

  5. 使用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;}
复制代码

这样对于示例中的函数,其函数参数可以结合Argumentstd::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); } //其它成员函数实现 };
复制代码

提供setreset接口用来填充/修改参数值:

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();
复制代码

总结


以上只是原理性的简单实现,真实场景会略显复杂,需要酌情修改.


通过这种实现方法,可以将函数转换成可检视、编辑参数,并且能够调用的对象,再通过相对通用的界面/命令行交互,就可以在不修改代码的情况下重新执行某些代码片段了.


发布于: 2021 年 02 月 21 日阅读数: 11
用户头像

还未添加个人签名 2017.11.30 加入

从程序员到工程师

评论

发布
暂无评论
一种C++中支持界面调用函数的实现方法