我在C ++中实现委托的方法:在运行时调用参数未知的函数

背景知识


我喜欢C ++语言。 我什至会说这是我最喜欢的语言。 另外,我使用.NET技术进行开发,我认为其中的许多想法简直令人赞叹。 一旦我想到了这个想法-如何在C ++中实现一些反射和动态函数调用的方法? 我真的希望C ++具有CLI的优势,例如可以使用数量未知的参数及其类型来调用委托。 例如,在事先不知道需要调用哪种数据类型的情况下,这可能会很有用。

当然,完全模仿委托人太复杂了,因此本文将仅演示库的一般体系结构以及在处理语言不直接支持的内容时出现的一些重要问题的解决方案。

编译期间使用不确定数量的参数和未知类型调用函数


当然,这是C ++的主要问题,并不是很容易解决。 当然,在C ++中,有一个继承自C- varargs的工具,很可能这是我想到的第一件事...但是,它们首先不适合,因为它们具有类型不安全的特性(就像C中的许多东西一样),其次,使用此类参数时,您需要事先知道什么类型的参数。 但是,几乎可以肯定,这不是varargs的全部问题。 通常,此工具不是这里的助手。

现在,我将列出帮助我解决此问题的工具。

性病::任何


从C ++ 17开始,该语言具有出色的容器容器,可以容纳任何内容-与CLI中System.Object的遥远相似之处是std :: any 。 这个容器实际上可以存储任何内容,甚至可以存储:有效! -该标准建议您直接将小对象存储在其中,大对象已经可以存储在动态内存中(尽管此行为不是强制性的,但是Microsoft在其C ++实现中做到了这一点,这是个好消息)。 只能将其称为相似性,因为System.Object涉及继承关系(“ is a”),而std :: any涉及成员关系(“ has a”)。 除了数据之外,容器还包含一个指向std :: type_info -RTTI对象的指针,该对象有关其对象“位于”容器中的类型。

整个头文件<any>被分配给容器。

要从容器中“拉”对象,您需要使用std :: any_cast()模板函数,该函数返回对该对象的引用。
用法示例:

#include <any> void any_test() { std::any obj = 5; int from_any = std::any_cast<int>(obj); } 

如果请求的类型与对象在容器内的类型不匹配,则抛出异常std :: bad_any_cast

除了std :: anystd :: bad_any_cast类std :: any_cast函数之外 ,头文件中还有一个模板函数std :: make_any,类似于std :: make_sharedstd :: make_pair和此类其他函数。

RTTI


当然,在C ++中在运行时没有类型信息的情况下实现动态函数调用实际上是不现实的。 毕竟,有必要检查一下是否传递了正确的类型。

在C ++中对RTTI的原始支持已经存在了很长时间。 这只是原始的意义-除非经过修饰和未经修饰的名称,否则我们几乎无法了解类型。 另外,我们可以相互比较类型。

通常,术语“ RTTI”与多态类型一起使用。 但是,这里我们将更广泛地使用该术语。 例如,我们将考虑以下事实:每个类型在运行时都具有有关该类型的信息(尽管与多态类型不同,尽管只能在编译时静态地获取它)。 因此,有可能(并且有必要)在运行时比较甚至非多态类型的类型(对不起重言式)。
可以使用std :: type_info类访问RTTI。 此类位于<typeinfo>头文件中。 至少使用typeid()运算符,可以(至少现在)获得对此类对象的引用。

模式


实现想法所需的语言的另一个极其重要的功能是模板。 该工具功能强大且极其困难,实际上,它允许您在编译时生成代码。

模板是一个非常广泛的主题,不可能在本文的框架内进行揭示,并且没有必要。 我们假设读者了解它的含义。 在此过程中会发现一些晦涩的地方。

参数包装后跟一个调用


因此,我们有一个使用几个参数作为输入的特定函数。

我将向您展示一个代码草图,它将解释我的意图。

 #include <Variadic_args_binder.hpp> #include <string> #include <iostream> #include <vector> #include <any> int f(int a, std::string s) { std::cout << "int: " << a << "\nstring: " << s << std::endl; return 1; } void demo() { std::vector<std::any> params; params.push_back(5); params.push_back(std::string{ "Hello, Delegates!" }); delegates::Variadic_args_binder<int(*)(int, std::string), int, std::string> binder{ f, params }; binder(); } 

您可能会问,这怎么可能? 类名Variadic_args_binder告诉您该对象绑定了函数和调用时需要传递给它的参数。 因此,仅需将此绑定器作为没有参数的函数来调用!
因此它看起来在外面。

如果立即不假思索地假设如何实现,那么可能想到为不同数量的参数编写多个Variadic_args_binder专门化功能。 但是,如果您需要支持无限数量的参数,则这是不可能的。 这就是问题所在:不幸的是,参数需要静态地替换为函数调用,也就是说,最终对于编译器,应将调用代码简化为:

 fun_ptr(param1, param2, …, paramN); 

这就是C ++的工作方式。 所有这些使情况变得非常复杂。

只有模板魔术可以处理!

主要思想是创建在每个嵌套级别存储参数或函数之一的递归类型。

因此,声明_Tagged_args_binder类:

 namespace delegates::impl { template <typename Func_type, typename... T> class _Tagged_args_binder; } 

为了方便地“传输”类型的程序包,我们将创建一个辅助类型Type_pack_tag (为什么需要这种类型,很快将变得清楚):

 template <typename... T> struct Type_pack_tag { }; 

现在,我们创建_Tagged_args_binder类的特化。

最初的专业


如您所知,为了使递归不是无限的,有必要定义边界情况。
以下是专长。 为简单起见,我仅引用非引用类型和右值引用类型的专业化。
直接参数值的专业化:

 template <typename Func_type, typename T1, typename... Types_to_construct> class _Tagged_args_binder<Func_type, Type_pack_tag<T1, Types_to_construct...>, Type_pack_tag<>> { public: static_assert(!std::is_same_v<void, T1>, "Void argument is not allowed"); using Ret_type = std::invoke_result_t<Func_type, T1, Types_to_construct...>; _Tagged_args_binder(Func_type func, std::vector<std::any>& args) : ap_arg{ std::move(unihold::reference_any_cast<T1>(args.at(0))) }, ap_caller_part{ func, args } { } auto operator()() { if constexpr(std::is_same_v<void, Ret_type>) { ap_caller_part(std::move(ap_arg)); return; } else { return std::forward<Ret_type>(ap_caller_part(std::move(ap_arg))); } } auto operator()() const { if constexpr (std::is_same_v<void, Ret_type>) { ap_caller_part(std::move(ap_arg)); return; } else { return std::forward<Ret_type>(ap_caller_part(std::move(ap_arg))); } } private: _Tagged_args_binder<Func_type, Type_pack_tag<Types_to_construct...>, Type_pack_tag<T1>> ap_caller_part; T1 ap_arg; }; 

ap_arg调用的第一个参数和递归ap_caller_part对象的其余参数存储在此处 。 注意, T1类型从此对象中的第一个类型数据包“移动”到递归对象的“尾部”中的第二个数据包。

右值链接的专业化:

 template <typename Func_type, typename T1, typename... Types_to_construct> class _Tagged_args_binder<Func_type, Type_pack_tag<T1&&, Types_to_construct...>, Type_pack_tag<>> { using move_ref_T1 = std::add_rvalue_reference_t<std::remove_reference_t<T1>>; public: using Ret_type = std::invoke_result_t<Func_type, move_ref_T1, Types_to_construct>; _Tagged_args_binder(Func_type func, std::vector<std::any>& args) : ap_arg{ std::move(unihold::reference_any_cast<T1>(args.at(0))) }, ap_caller_part{ func, args } { } auto operator()() { if constexpr (std::is_same_v<void, Ret_type>) { ap_caller_part(std::move(unihold::reference_any_cast<T1>(ap_arg))); } else { return std::forward<Ret_type>(ap_caller_part(std::move(unihold::reference_any_cast<T1>(ap_arg)))); } } auto operator()() const { if constexpr (std::is_same_v<void, Ret_type>) { ap_caller_part(std::move(unihold::reference_any_cast<T1>(ap_arg))); } else { return std::forward<Ret_type>(ap_caller_part(std::move(unihold::reference_any_cast<T1>(ap_arg)))); } } private: _Tagged_args_binder<Func_type, Type_pack_tag<Types_to_construct...>, Type_pack_tag<move_ref_T1>> ap_caller_part; std::any ap_arg; }; 


模板的“右手”链接并不是真正的右手含义。 这些就是所谓的“通用链接”,根据T1的类型,它们变为T1&T1 && 。 因此,您必须使用变通方法:首先,由于为链接的两种类型(由于已经陈述的原因而不能正确说出)和非引用参数都定义了专门化,因此在实例化模板时,即使是右手链接也将选择必要的专门化; 其次,为了将T1类型从一个包传递到另一个包,使用了更正版本的move_ref_T1 ,它变成了一个真正的右值链接。

使用正常链接进行专业化的方式与之前相同,并进行了必要的更正。

终极专业化


 template <typename Func_type, typename... Param_type> class _Tagged_args_binder<Func_type, Type_pack_tag<>, Type_pack_tag<Param_type...>> { public: using Ret_type = std::invoke_result_t<Func_type, Param_type...>; inline _Tagged_args_binder(Func_type func, std::vector<std::any>& args) : ap_func{ func } { } inline auto operator()(Param_type... param) { if constexpr(std::is_same_v<void, decltype(ap_func(std::forward<Param_type>(param)...))>) { ap_func(std::forward<Param_type>(param)...); return; } else { return std::forward<Ret_type>(ap_func(std::forward<Param_type>(param)...)); } } inline auto operator()(Param_type... param) const { if constexpr(std::is_same_v<void, Ret_type>) { ap_func(param...); return; } else { return std::forward<Ret_type>(ap_func(param...)); } } private: Func_type ap_func; }; 

这种专门化负责存储功能对象,实际上是对其的包装。 这是最终的递归类型。

注意这里如何使用Type_pack_tag 。 现在,所有参数类型都在左侧的包中进行编译。 这意味着它们都已被处理和打包。

现在,我想清楚了为什么必须使用Type_pack_tag了 。 事实是,这种语言不允许并排使用两种类型的程序包,例如:

 template <typename Func_type, typename T1, typename... Types_to_construct, typename... Param_type> class _Tagged_args_binder<Func_type, T1, Types_to_construct..., Param_type...> { }; 

因此,您必须将它们分成两种类型的两个单独的程序包。 另外,您需要以某种方式将已处理的类型与尚未处理的类型分开。

中级专业


从中间专业领域,我最后将再次给出专业领域,对于值类型,其余类似地:

 template <typename Func_type, typename T1, typename... Types_to_construct, typename... Param_type> class _Tagged_args_binder<Func_type, Type_pack_tag<T1, Types_to_construct...>, Type_pack_tag<Param_type...>> { public: using Ret_type = std::invoke_result_t<Func_type, Param_type..., T1, Types_to_construct...>; static_assert(!std::is_same_v<void, T1>, "Void argument is not allowed"); inline _Tagged_args_binder(Func_type func, std::vector<std::any>& args) : ap_arg{ std::move(unihold::reference_any_cast<T1>(args.at(sizeof...(Param_type)))) }, ap_caller_part{ func, args } { } inline auto operator()(Param_type... param) { if constexpr (std::is_same_v<void, Ret_type>) { ap_caller_part(std::forward<Param_type>(param)..., std::move(ap_arg)); return; } else { return std::forward<Ret_type>(ap_caller_part(std::forward<Param_type>(param)..., std::move(ap_arg))); } } inline auto operator()(Param_type... param) const { if constexpr (std::is_same_v<void, Ret_type>) { ap_caller_part(std::forward<Param_type>(param)..., std::move(ap_arg)); } else { return std::forward<Ret_type>(ap_caller_part(std::forward<Param_type>(param)..., std::move(ap_arg))); } } private: _Tagged_args_binder<Func_type, Type_pack_tag<Types_to_construct...>, Type_pack_tag<Param_type..., T1>> ap_caller_part; T1 ap_arg; }; 

该专业化旨在打包除第一个参数以外的任何参数。

活页夹类


_Tagged_args_binder类不能直接使用,我想在其名称的开头加一个下划线来强调它。 因此,我将给出一个小类的代码,它是这种丑陋且不方便使用的类型的“接口”(但是,使用了非常不寻常的C ++技巧,在我看来,这给了它一些魅力):

 namespace cutecpplib::delegates { template <typename Functor_type, typename... Param_type> class Variadic_args_binder { using binder_type = impl::_Tagged_args_binder<Functor_type, Type_pack_tag<Param_type...>, Type_pack_tag<>>; public: using Ret_type = std::invoke_result_t<binder_type>; inline Variadic_args_binder(Functor_type function, Param_type... param) : ap_tagged_binder{ function, param... } { } inline Variadic_args_binder(Functor_type function, std::vector<std::any>& args) : ap_tagged_binder{ function, args } { } inline auto operator()() { return ap_tagged_binder(); } inline auto operator()() const { return ap_tagged_binder(); } private: binder_type ap_tagged_binder; }; } 

Unihold约定-在std内部传递链接::任何


细心的读者一定已经注意到该代码使用unihold :: reference_any_cast()函数。 此函数及其类似的unihold :: pointer_any_cast()用于实现库协议:必须通过引用传递的参数由指向std :: any的指针传递。

reference_any_cast函数始终返回对对象的引用,无论对象本身是存储在容器中还是仅指向容器的指针。 如果std :: any包含对象,则在容器内返回对此对象的引用; 如果它包含一个指针,则将引用返回到该指针指向的对象。

对于每个函数,都有用于常量std :: any和重载版本的选项,以确定容器std :: any是拥有对象还是仅包含指针。

函数需要明确地专门针对存储对象的类型,就像C ++类型转换和类似的模板函数一样。

这些功能的代码:

 template <typename T> std::remove_reference_t<T>& unihold::reference_any_cast(std::any& wrapper) { bool result; return reference_any_cast<T>(wrapper, result); } template <typename T> const std::remove_reference_t<T>& unihold::reference_any_cast(const std::any& wrapper) { bool result; return reference_any_cast<T>(wrapper, result); } template <typename T> std::remove_reference_t<T>& unihold::reference_any_cast(std::any& wrapper, bool& is_owner) { auto ptr = pointer_any_cast<T>(&wrapper, is_owner); if (!ptr) throw std::bad_any_cast{ }; return *ptr; } template <typename T> const std::remove_reference_t<T>& unihold::reference_any_cast(const std::any& wrapper, bool& is_owner) { auto ptr = pointer_any_cast<T>(&wrapper, is_owner); if (!ptr) throw std::bad_any_cast{ }; return *ptr; } template <typename T> std::remove_reference_t<T>* unihold::pointer_any_cast(std::any* wrapper, bool& is_owner) { using namespace std; using NR_T = remove_reference_t<T>; // No_reference_T //     wrapper NR_T** double_ptr_to_original = any_cast<NR_T*>(wrapper); //      wrapper NR_T* ptr_to_copy; if (double_ptr_to_original) { // Wrapper      is_owner = false; return *double_ptr_to_original; } else if (ptr_to_copy = any_cast<NR_T>(wrapper)) { // Wrapper    is_owner = true; return ptr_to_copy; } else { throw bad_any_cast{}; } } template <typename T> const std::remove_reference_t<T>* unihold::pointer_any_cast(const std::any* wrapper, bool& is_owner) { using namespace std; using NR_T = remove_reference_t<T>; // No_reference_T //     wrapper NR_T*const * double_ptr_to_original = any_cast<NR_T*>(wrapper); //      wrapper const NR_T* ptr_to_copy; //remove_reference_t<T>* ptr2 = any_cast<remove_reference_t<T>>(&wrapper); if (double_ptr_to_original) { // Wrapper      is_owner = false; return *double_ptr_to_original; } else if (ptr_to_copy = any_cast<NR_T>(wrapper)) { // Wrapper    is_owner = true; return ptr_to_copy; } else { throw bad_any_cast{}; } } template <typename T> std::remove_reference_t<T>* unihold::pointer_any_cast(std::any* wrapper) { bool result; return pointer_any_cast<T>(wrapper, result); } template <typename T> const std::remove_reference_t<T>* unihold::pointer_any_cast(const std::any* wrapper) { bool result; return pointer_any_cast<T>(wrapper, result); } 

结论


我试图简要描述解决C ++中动态函数调用问题的一种可能方法。 随后,这将构成C ++委托库的基础(实际上,我已经编写了该库的基本功能,即多态委托,但是仍然需要按需重写该库以演示代码并添加一些未实现的功能)。 我计划在不久的将来完成该库的工作,并告诉我如何在C ++中完全实现其余的委托功能。

下一部分将演示使用RTTI的PS。

Source: https://habr.com/ru/post/zh-CN457160/


All Articles