[Из песочницы] Передача сохраненных аргументов в функцию11.06.2015 00:49
Один мой знакомый подкинул мне интересную задачку: нужно вызвать функцию через указатель и передать в нее предварительно сохраненные аргументы. Обязательным условием было не использовать 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
#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;
};
В этом примере: благодаря шаблонному конструктору, лямбда создаваемая внутри этого конструктора будет иметь информацию о типе Сallable, а значит, сможет привести данные в Storage к нужному типу. Фактически, в этом и заключается весь фокус. Вся сложная работа по захвату переменных и вызову функций и лямбд возложена на плечи компилятора. На мой взгляд, такое решение предельно простое и элегантное.Что же касается второго решения, то оно мне нравится меньше, т.к. в нем очень много самописного кода, который решает по сути то, что уже решено для нас компилятором. А именно: захват переменных. Не буду вдаваться в долгие рассуждения и обсуждения, а приведу сразу код всего решения. Т.к. он очень большой и мне не импанирует, то я его спрячу под кат:
не красивый код.
#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;
};
Тут вся интересность, на мой взгляд, заключается в двух вспомогательных классах: StorageHelper и InvokeHelper. Первый комбинирует эллипсис и рекурсивный проход по списку типов для того, чтобы заполнить хранилище аргументов. Второй предоставляет безопасный в плане типов способ извлечения аргументов из этого хранилища. Кроме того, есть еще одна небольшая хитрость: эллипсис промоутит одни типы к другим. Т.е. float переданный через… будет приведен к double, char к int, short к int и т.д. Хочу подвести этакий итог всему выше сказанному. По моему мнению, оба решения не идеальны: они много чего не умеют и пытаются изобрести колесо. Если бы меня спросили как правильно захватить аргументы и передать их в некую функцию, я бы не раздумывая сказал, что нужно использовать std: function + лямбду. Хотя в качестве упражнения для ума поставленная задачка очень даже неплоха.
Надеюсь, что все прочитанное вами окажется полезным. Спасибо, что так далеко дочитали!
© Habrahabr.ru