📜 ⬆️ ⬇️

My approach to implementing delegates in C ++: calling a function with unknown parameters at runtime

Prehistory


I like the C ++ language. I would even say that this is my favorite language. In addition, for my development I use .NET technologies, and many of the ideas in it, in my opinion, are simply amazing. Once I had an idea - how to implement some means of reflection and dynamic function call in C ++? I really wanted C ++ to have the advantage of the CLI, like calling a delegate with an unknown number of parameters and their types. This can be useful, for example, when it is not known in advance what types of data are needed by the function to be called.

Of course, a complete imitation of delegates is too complicated, so this article will demonstrate only the general architecture of the library and the solution of some important problems that arise when dealing with something that is not directly supported by the language.

Calling functions with an indefinite number of parameters and unknown types during compilation


Of course, this is the main problem with C ++, which is not so easy to solve. Of course, in C ++ there is a tool inherited from C - varargs , and, most likely, this is the first thing that comes to mind ... However, they are not suitable, firstly, because of their type-safe nature (like many things from C), secondly, when using such arguments, it is necessary to know in advance exactly what types the arguments have. However, almost certainly, this is not all the problems with varargs . In general, this tool is not our assistant.

And now I will list the funds that helped me solve this problem.
')

std :: any


Beginning with C ++ 17, a wonderful storage container for anything appeared in the language - some distant similarity of System.Object in the CLI is std :: any . This container can really store anything, and how to: efficiently! - The standard recommends that small objects be stored directly in it, large ones can already be stored in dynamic memory (although this behavior is not mandatory, Microsoft has done just that in its C ++ implementation, which is good news). And it can only be called a similarity because System.Object participates in the inheritance relationship (“is a”), and std :: any participates in the ownership relationship (“has a”). In addition to data, the container contains a pointer to an object std :: type_info - RTTI about a type whose object is “lying” in the container.

For the container, the whole <any> header file is highlighted.

To “pull out” an object from a container, you need to use the template function std :: any_cast () , which returns a reference to the object.
Example of use:

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

If the requested type does not match the one that the object has inside the container, then the exception std :: bad_any_cast is thrown .

In addition to the std :: any , std :: bad_any_cast classes and the std :: any_cast functions , the header file contains the template function std :: make_any , similar to std :: make_shared , std :: make_pair, and other functions of this kind.

RTTI


Of course, it would be practically impossible in C ++ to implement dynamic function calls without type information at runtime. After all, it is necessary to somehow check whether the correct types are transferred or not.

Primitive support for RTTI in C ++ has been around for quite some time. That's just the fact of the matter is that the primitive - we can learn little about the type, except decorated and undecorated names. In addition, we can compare types with each other.

Typically, the term "RTTI" is used in connection with polymorphic types. However, here we will use this term in a wider sense. For example, we will take into account the fact that type information at runtime is on each type (although it can only be obtained statically, at compile time, unlike polymorphic types). Therefore, it is possible (and necessary) to compare the types of even non-polymorphic types (sorry for tautology) at run time.
Access to RTTI can be obtained using the class std :: type_info . This class is in the <typeinfo> header file. A link to an object of this class can be obtained (at least for now) only with the help of the typeid () operator.

Templates


Another extremely important feature of the language that we need to implement our ideas is templates. This tool is quite powerful and extremely difficult, in fact allows you to generate code at compile time.

Templates are a very broad topic, and it will not be possible to reveal it within the framework of the article, and it is not necessary. We assume that the reader understands what they mean. Some obscure points will be revealed in the process.

Packing arguments with a subsequent call


So, we have a certain function that takes several parameters as input.

I will demonstrate a sketch of the code that explains my intentions.

 #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(); } 

You may ask - how is this possible? The class name Variadic_args_binder suggests that the object binds the function and the arguments that need to be passed to it when called. Thus, it remains only to call this binder as a function without parameters!
So it looks outside.

If immediately, without thinking, to make an assumption how this can be implemented, then it may come to mind to write several Variadic_args_binder specializations for a different number of parameters. However, this is not possible if it is necessary to support an unlimited number of parameters. And the problem is: the arguments, unfortunately, need to be substituted statically into the function call, that is, in the end for the compiler, the calling code should be reduced to this:

 fun_ptr(param1, param2, …, paramN); 

This is how C ++ works. And this complicates things a lot.

Only patterned magic can do this!

The main idea is to create recursive types that store one of the arguments or a function at each nesting level.

So, let's declare the _Tagged_args_binder class:

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

To conveniently “transfer” type packages, create an auxiliary type Type_pack_tag (why it was needed, it will become clear soon):

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

Now we create specializations of the _Tagged_args_binder class.

Initial specialization


As it is known, that the recursion was not infinite, it is necessary to define boundary cases.
The following specializations are initial. To simplify, I will give specializations only for non-reference types and right-side reference types (rvalue reference).
Specialization for directly parameter-values:

 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; }; 

The first argument of the ap_arg call and the rest of the recursive ap_caller_part object are stored here . Note that the type T1 “moved” from the first type package in this object to the second in the “tail” of the recursive object.

Specialization for rvalue links:

 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; }; 


Template “right-hand” links are not really right-hand values. These are the so-called “universal links”, which, depending on the type of T1 , become either T1 & then T1 && . Therefore, it is necessary to use workarounds: first, since the specializations for both types of links are defined (not quite correctly stated, for the reason already stated) and for non-reference parameters, when the template is instantiated, the necessary specialization will be chosen, even if it is a right-side link; secondly, to transfer the type T1 from the packet to the packet, use the corrected version of move_ref_T1 , converted into a real rvalue-link.

Specialization with the usual link is done similarly, with the necessary corrections.

Ultimate specialization


 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; }; 

This specialization is responsible for storing the functional object and, in fact, is a wrapper over it. It is the final recursive type.

Notice how Type_pack_tag is used here. All types of parameters are now collected in the left package. This means that they are all processed and packaged.

Now, I think it becomes clear why it was necessary to use Type_pack_tag . The fact is, the language would not allow using two types of packages side by side, for example, like this:

 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...> { }; 

therefore it is necessary to separate them into two separate packages within two types. In addition, you must somehow separate the processed types from those that have not yet been processed.

Intermediate specializations


Of the intermediate specializations, finally, I will give a specialization, again, for value types, the rest by analogy:

 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; }; 

This specialization is intended for the packaging of any argument except the first.

Class binder


The _Tagged_args_binder class is not intended to be used directly, which I wanted to emphasize with a single underscore at the beginning of its name. Therefore, I will give the code of a small class, which is a kind of “interface” to this ugly and inconvenient to use type (which, however, uses rather unusual C ++ techniques, which gives it some charm, in my opinion):

 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 agreement - passing links inside std :: any


The attentive reader probably noticed that the code uses the unihold :: reference_any_cast () function. This function, as well as its counterpart unihold :: pointer_any_cast () , is designed to implement the library's agreement: the arguments that need to be passed by reference are passed by pointer to std :: any .

The reference_any_cast function always returns a reference to an object, whether the object itself is stored in the container or only a pointer to it. If std :: any contains an object, then a reference to this object is returned inside the container; if it contains a pointer, then a reference is returned to the object the pointer points to.

For each of the functions, there are variants of the constant std :: any and overloaded versions to determine whether the std :: any container owns the object or contains only a pointer.

Functions need to be specialized explicitly with the type of the stored object, as well as C ++ type conversions and similar template functions.

The code for these functions is:

 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); } 

Conclusion


I tried to briefly describe one of the possible approaches to solving the problem of dynamic function call in C ++. Subsequently, this will form the basis of the delegates library in C ++ (in fact, I have already written the main library functionality, namely, polymorphic delegates, but the library still needs to be rewritten as necessary so that you can demonstrate the code and add some unimplemented functionality). In the near future I plan to finish the work on the library and tell you exactly how I implemented the rest of the delegates functionality in C ++.

PS The use of RTTI will be demonstrated in the next section.

Source: https://habr.com/ru/post/457160/


All Articles