Passing stored arguments to a function

One of my friends threw me an interesting problem: you need to call the function through a pointer and pass previously stored 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. In no case does it pretend 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 simplest would be to use such a wonderful mechanism. For those who are not familiar with C ++ 14 and above, I will give the corresponding code:

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

In this code, a lambda function is created that captures a variable called Variable. The function lambda object itself is copied to 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 just like a call to a regular function:

Lambda();

It would seem that the task has already been solved, but in reality this is not so. A lambda function can be returned from a function, method or other lambda, but it is difficult to transfer it later 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 operations on lambdas:

  • lambda can be called;
  • a lambda can be cast to a function pointer if that lambda does not capture variables;
  • lambda can be copied.


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

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

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

As for the second solution, I like it less, because it contains a lot of self-written code that essentially solves what the compiler has already decided for us. Namely: capture of variables. I will not go into long discussions and discussions, but I will immediately give the code for the whole solution. Because it’s very large and doesn’t bother me, I’ll hide it under the cat:

not a beautiful code.
#include 
#include 
#include 
template  struct PromotedTraits { using Type = T; };
template <> struct PromotedTraits { using Type = int; };
template <> struct PromotedTraits { using Type = unsigned; };
template <> struct PromotedTraits { using Type = int; };
template <> struct PromotedTraits { using Type = unsigned; };
template <> struct PromotedTraits { using Type = double; };
template  class StorageHelper;
template 
class StorageHelper {
public:
  static void store(va_list &List, std::vector &Storage) {
    using Type = typename PromotedTraits::Type;
    union {                                       
      T Value;                                    
      std::uint8_t Bytes[sizeof(void *)];         
    };                                            
    Value = va_arg(List, Type);
    for (auto B : Bytes) {
      Storage.push_back(B);
    }
    StorageHelper::store(List, Storage);
  }
};
template <> class StorageHelper<> {
public:
  static void store(...) {}
};
template  class InvokeHelper;
template  class InvokeHelper {
public:
  template 
  static Result invoke(Result (*Fn)(Arguments...), Arguments... Args) {
    return Fn(Args...);
  }
};
template  class InvokeHelper {
public:
  template  static Result invoke(...) { return {}; }
};
struct Dummy;
template  class TypeAt {
public:
  using Type = Dummy *;
};
template 
class TypeAt {
public:
  using Type = typename TypeAt<(Index - 1u), Types...>::Type;
};
template  class TypeAt<0u, T, Types...> {
public:
  using Type = T;
};
template  class Signal;
template 
class Signal {
public:
  using CFunction = Result(Arguments...);
  Signal(CFunction *Delegate, Arguments... Values) : Delegate(Delegate) {
    initialize(Delegate, Values...);
  }
  Result invoke() {
    std::uintptr_t *Args = reinterpret_cast(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(Delegate);
    case 1u:
      return InvokeHelper<(1u == sizeof...(Arguments)),
                          Arguments...>::template invoke(Delegate,
                                                                 (T0 &)Args[0]);
    case 2u:
      return InvokeHelper<(2u == sizeof...(Arguments)),
                          Arguments...>::template invoke(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::store(List, Storage); 
    va_end(List);                                      
  }                                                    
  CFunction *Delegate;
  std::vector Storage; 
};

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

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

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

Also popular now: