📜 ⬆️ ⬇️

Passing Stored Arguments to a Function

An acquaintance of mine threw an interesting puzzle to me: you need to call a function through a pointer and pass previously saved arguments to it. A prerequisite was not to use std :: function. I want to share with you my solution to this problem. Do not judge strictly given implementation. She does not in any way claim to be complete and comprehensive. I wanted to make everything as simple as possible, minimal, but sufficient. In addition, there will be two solutions. One of them, in my opinion, is better than the other.

The first solution is based on the fact that C ++ already provides us with a mechanism for capturing variables. It's about lambdas. Naturally, the most obvious and simple would be to use such a wonderful mechanism. For those who are not familiar with C ++ 14 and higher, I will provide the appropriate code:

auto Variable = 1; auto Lambda = [Variable]() { someFunction(Variable); }; 

This code creates a lambda function that captures a variable named Variable. The object itself of the lambda function is copied into a variable named Lambda. It is through this variable that in the future it will be possible to call the lambda function itself. And such a call will look like a normal function call:

 Lambda(); 

It would seem that the task has already been solved, but in reality it is not. A lambda function can be returned from a function, method or other lambda, but then it is difficult to transfer it somewhere without using templates.
')
 auto makeLambda(int Variable) { return [Variable]() { someFunction(Variable); }; } auto Lambda = makeLambda(3); //     ,   ? someOtherFunction(Lambda); 

Lambda functions are objects of some anonymous type, they have an internal structure known only to the compiler. And pure C ++ (I mean a language without libraries) provides the programmer with not so many lambda operations:



In principle, these basic operations are quite enough, because using them and other mechanisms of the language can be done very, very much. That's what I got in the end.

 #include <utility> #include <cstdint> #include <vector> template <typename Function> class SignalTraits; template <typename R, typename... A> class SignalTraits<R(A...)> { public: using Result = R; }; template <typename Function> class Signal { public: using Result = typename SignalTraits<Function>::Result; template <typename Callable> Signal(Callable Fn) : Storage(sizeof(Fn)) { new (Storage.data()) Callable(std::move(Fn)); Trampoline = [](Signal *S) -> Result { auto CB = static_cast<Callable *>(static_cast<void *>(S->Storage.data())); return (*CB)(); }; } Result invoke() { return Trampoline(this); } private: Result (*Trampoline)(Signal *Self); std::vector<std::uint8_t> Storage; }; 

In this example: thanks to the template constructor, the lambda created inside this constructor will have information about the type of Callable, which means it can bring the data in the Storage to the desired type. In fact, that’s the whole trick. All the hard work of capturing variables and calling functions and lambdas is on the compiler’s shoulders. In my opinion, such a solution is extremely simple and elegant.

As for the second solution, I like it less, because there is a lot of self-written code in it that essentially solves what has already been decided for us by the compiler. Namely: capture variables. I will not go into long discussions and discussions, but I will give the code of the entire solution right away. Since it is very big and it does not impaniate me, then I will hide it under the cat:

not a beautiful code.
 #include <cstdarg> #include <cstdint> #include <vector> template <typename T> struct PromotedTraits { using Type = T; }; template <> struct PromotedTraits<char> { using Type = int; }; template <> struct PromotedTraits<unsigned char> { using Type = unsigned; }; template <> struct PromotedTraits<short> { using Type = int; }; template <> struct PromotedTraits<unsigned short> { using Type = unsigned; }; template <> struct PromotedTraits<float> { using Type = double; }; template <typename... Arguments> class StorageHelper; template <typename T, typename... Arguments> class StorageHelper<T, Arguments...> { public: static void store(va_list &List, std::vector<std::uint8_t> &Storage) { using Type = typename PromotedTraits<T>::Type; union { T Value; std::uint8_t Bytes[sizeof(void *)]; }; Value = va_arg(List, Type); for (auto B : Bytes) { Storage.push_back(B); } StorageHelper<Arguments...>::store(List, Storage); } }; template <> class StorageHelper<> { public: static void store(...) {} }; template <bool, typename...> class InvokeHelper; template <typename... Arguments> class InvokeHelper<true, Arguments...> { public: template <typename Result> static Result invoke(Result (*Fn)(Arguments...), Arguments... Args) { return Fn(Args...); } }; template <typename... Arguments> class InvokeHelper<false, Arguments...> { public: template <typename Result> static Result invoke(...) { return {}; } }; struct Dummy; template <std::size_t Index, typename... Types> class TypeAt { public: using Type = Dummy *; }; template <std::size_t Index, typename T, typename... Types> class TypeAt<Index, T, Types...> { public: using Type = typename TypeAt<(Index - 1u), Types...>::Type; }; template <typename T, typename... Types> class TypeAt<0u, T, Types...> { public: using Type = T; }; template <typename Function> class Signal; template <typename Result, typename... Arguments> class Signal<Result(Arguments...)> { public: using CFunction = Result(Arguments...); Signal(CFunction *Delegate, Arguments... Values) : Delegate(Delegate) { initialize(Delegate, Values...); } Result invoke() { std::uintptr_t *Args = reinterpret_cast<std::uintptr_t *>(Storage.data()); Result R = {}; using T0 = typename TypeAt<0u, Arguments...>::Type; using T1 = typename TypeAt<0u, Arguments...>::Type; // ... and so on. switch (sizeof...(Arguments)) { case 0u: return InvokeHelper<(0u == sizeof...(Arguments)), Arguments...>::template invoke<Result>(Delegate); case 1u: return InvokeHelper<(1u == sizeof...(Arguments)), Arguments...>::template invoke<Result>(Delegate, (T0 &)Args[0]); case 2u: return InvokeHelper<(2u == sizeof...(Arguments)), Arguments...>::template invoke<Result>(Delegate, (T0 &)Args[0], (T1 &)Args[1]); // ... and so on. } return R; } private: void initialize(CFunction *Delegate, ...) { va_list List; va_start(List, Delegate); StorageHelper<Arguments...>::store(List, Storage); va_end(List); } CFunction *Delegate; std::vector<std::uint8_t> Storage; }; 

Here all the interestingness, in my opinion, lies in two auxiliary classes: StorageHelper and InvokeHelper. The first combines ellipse and recursive passage through the type list in order to fill the argument store. The second provides a type-safe way to extract arguments from this repository. In addition, there is another small trick: the ellipse will promote one type to another. Those. the float passed through ... will be cast to double, char to int, short to int, etc.

I want to summarize all of the above. In my opinion, both solutions are not perfect: they do not know how much and are trying to invent a wheel. If I were asked how to properly capture the arguments and pass them into a certain function, I would not hesitate to say that you need to use std :: function + lambda. Although as an exercise for the mind, the task set is very good.

I hope that everything you read will be useful. Thank you for reading this far!

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


All Articles