Quantcast
Channel: GameDev.net
Viewing all articles
Browse latest Browse all 17825

Calling Functions With Pre-Set Arguments in Modern C++

$
0
0

Introduction


A good fellow of mine gave me this interesting problem: pass a pre-stored set of arguments into a function without using std::function. I'd like to share with you my solution to this problem. Please, don't judge it strictly. I've never meant it to be perfect or finished for production use. Instead, I wanted to do everything as simple as possible, minimalistic but sufficient. Besides, there will be two solutions in this article. And one of them I like more than the other.

Implementation


Good Solution


The first way of solving the task exploits the fact that C++ already has a mechanism that allows us to capture variables. I talk about lambda functions. Of course, it would be great to use lambdas for this task. I'd show you a simple code snippet that has a lambda in it, just in case some of you are not familiar with C++14:

auto Variable = 1;

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

A lambda function is being created in this call. This lambda captures the value of the variable named Variable. The object of the lambda function is being copied into a variable named Lambda. One can later call the lambda through that variable. A call to lambda will look like this:

Lambda();

It seems at first that the problem is solved, but really it's not. A lambda function can be returned from a function, a method or another lambda function, but it is hard to pass a lambda as an argument unless the receiver of that argument is a template.

auto makeLambda(int Variable) {
    return [Variable]() {
        someFunction(Variable);
    };
}

auto Lambda = makeLambda(3);

// What should be the signature of someOtherFunction()?
someOtherFunction(Lambda);

Lambda functions are objects of anonymous types. They have an internal structure which only the compiler knows of. Pure C++ (I mean C++ as a language without its libraries) does not give a programmer much operations at hand:

  • a lambda can be called;
  • a lambda can be converted to a function pointer, when the lambda is not capturing anything;
  • a lambda can be copied.

Frankly speaking, these operations are more than enough, because there are other mechanisms in the language which when combined give us a lot of flexibility. Let me share with you the solution to the problem which I ended up with.

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

I'll explain briefly what is happening in that code snippet: the created non-capturing lambda function knows the type of Callable because it (the lambda) is being constructed in the template constructor. That's why the lambda is able to cast the data in Storage to the proper type. Really, that's it. All the hard lifting is done by the compiler. I consider this implementation to be simple and elegant.

Not So Good Solution


I like the other solution less, because it is filled with handmade stuff. And all that stuff is needed to capture variables, something C++ language already does for us out of the box. I don't want to spend a lot of words on this, so let me show you the implementation, which is large and clumsy.

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

As for me, the only interesting things are the two helper classes: StorageHelper and InvokeHelper. The first class combines ellipsis with type list recursive algorithm to put arguments into Storage. The second class provides a type safe way of fetching arguments from that storage. And there's a tiny important detail: ellipsis promotes some types to others. I.e. float is promoted to double, char to int, short to int, etc.

Summary


I'd like to make a kind of a summary: I don't think the two solutions are perfect. They lack a lot and they try to reinvent the wheel. I'd say that the best way to pass pre-stored arguments into a function would be to use std::function + lambda. Though, as a mind exercise the problem is a lot of fun indeed.

I hope you liked what you read and learned something useful for you. Thanks a lot for reading!

Article Update Log


9 June 2015: Initial release

Viewing all articles
Browse latest Browse all 17825

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>