Передача сохраненных аргументов в функцию

от автора

Один мой знакомый подкинул мне интересную задачку: нужно вызвать функцию через указатель и передать в нее предварительно сохраненные аргументы. Обязательным условием было не использовать std::function. Я хочу поделиться с вами моим решением этой задачки. Не судите строго приведенную реализацию. Она не в коем случае не претендует на полноту и всеобъемлимость. Я хотел сделать все как можно проще, минимальным, но достаточным. Кроме того, решений будет два. Одно из них, по моему мнению, лучше чем другое.

Первое решение основано на том, что С++ уже предоставляет нам механизм захвата переменных. Речь идет о лямбдах. Естественно, что самым очевидным и простым было бы использовать такой чудесный механизм. Для тех, кто не знаком с С++14 и выше, я приведу соответствующий код:

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

В этом коде создается лямбда функция, которая захватывает переменную с именем Variable. Сам объект лямбда функции копируется в переменную с именем Lambda. Именно через эту переменную в дальнейшем можно будет вызывать саму лямбда функцию. И такой вызов будет выглядеть совсем как вызов обычной функции:

Lambda(); 

Казалось бы, что поставленная задача уже решена, но в реальности это не так. Лямбда функцию можно вернуть из функции, метода или другой лямбды, но передать ее потом куда-то не используя шаблонов затруднительно.

auto makeLambda(int Variable) {     return [Variable]() {         someFunction(Variable);     }; }  auto Lambda = makeLambda(3);  // Какой должна быть сигнатура функции, принимающей такой аргумент? someOtherFunction(Lambda); 

Лямда функции являются объектами какого-то анонимного типа, у них есть известная лишь только компилятору внутренняя структура. И чистый С++ (я имею ввиду язык без библиотек) предоставляет программисту не так уж и много операций над лямбдами:

  • лямбду можно вызвать;
  • лямбду можно привести к указателю на функцию, если эта лямбда не захватыает переменные;
  • лямбду можно скопировать.

В принципе, этих базовых операций вполне достаточно, ведь используя их и другие механизмы языка можно сделать очень и очень многое. Вот что у меня получилось в итоге.

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

В этом примере: благодаря шаблонному конструктору, лямбда создаваемая внутри этого конструктора будет иметь информацию о типе Сallable, а значит, сможет привести данные в Storage к нужному типу. Фактически, в этом и заключается весь фокус. Вся сложная работа по захвату переменных и вызову функций и лямбд возложена на плечи компилятора. На мой взгляд, такое решение предельно простое и элегантное.

Что же касается второго решения, то оно мне нравится меньше, т.к. в нем очень много самописного кода, который решает по сути то, что уже решено для нас компилятором. А именно: захват переменных. Не буду вдаваться в долгие рассуждения и обсуждения, а приведу сразу код всего решения. Т.к. он очень большой и мне не импанирует, то я его спрячу под кат:

не красивый код.

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

Тут вся интересность, на мой взгляд, заключается в двух вспомогательных классах: StorageHelper и InvokeHelper. Первый комбинирует эллипсис и рекурсивный проход по списку типов для того, чтобы заполнить хранилище аргументов. Второй предоставляет безопасный в плане типов способ извлечения аргументов из этого хранилища. Кроме того, есть еще одна небольшая хитрость: эллипсис промоутит одни типы к другим. Т.е. float переданный через… будет приведен к double, char к int, short к int и т.д.

Хочу подвести этакий итог всему выше сказанному. По моему мнению, оба решения не идеальны: они много чего не умеют и пытаются изобрести колесо. Если бы меня спросили как правильно захватить аргументы и передать их в некую функцию, я бы не раздумывая сказал, что нужно использовать std::function + лямбду. Хотя в качестве упражнения для ума поставленная задачка очень даже неплоха.

Надеюсь, что все прочитанное вами окажется полезным. Спасибо, что так далеко дочитали!

ссылка на оригинал статьи http://habrahabr.ru/post/260027/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *