Первое решение основано на том, что С++ уже предоставляет нам механизм захвата переменных. Речь идет о лямбдах. Естественно, что самым очевидным и простым было бы использовать такой чудесный механизм. Для тех, кто не знаком с С++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 + лямбду. Хотя в качестве упражнения для ума поставленная задачка очень даже неплоха.
Надеюсь, что все прочитанное вами окажется полезным. Спасибо, что так далеко дочитали!
Комментарии (20)
kmu1990
10.06.2015 20:05new (Storage.data()) Callable(std::move(Fn));
а деструктор у Callable вызывать не нужно? Кроме того, чем вам type erasure не угодил, зачем в vector-е конструировать Callable (может у меня вкус плохой, но мне конкретно этот момент не кажется элегантным решением)?
Eivind
10.06.2015 22:32+2Я это вижу как-то так:
Код#include <tuple> #include <memory> #include <cassert> namespace detail { template <typename T> struct call_type : call_type<decltype(&T::operator())> {}; template <typename ClassType, typename ResultType, typename ... Args> struct call_type<ResultType(ClassType::*)(Args...) > : call_type<ResultType(Args...)> {}; template <typename ClassType, typename ResultType, typename ... Args> struct call_type<ResultType(ClassType::*)(Args...) const > : call_type<ResultType(Args...)> {}; template <typename ClassType, typename ResultType, typename ... Args> struct call_type<ResultType(ClassType::*)(Args...) volatile > : call_type<ResultType(Args...)> {}; template <typename ClassType, typename ResultType, typename ... Args> struct call_type<ResultType(ClassType::*)(Args...) const volatile > : call_type<ResultType(Args...)> {}; template <typename ClassType, typename ResultType, typename ... Args> struct call_type<ResultType(ClassType::*)(Args...) &> : call_type<ResultType(Args...)> {}; template <typename ClassType, typename ResultType, typename ... Args> struct call_type<ResultType(ClassType::*)(Args...) const & > : call_type<ResultType(Args...)> {}; template <typename ClassType, typename ResultType, typename ... Args> struct call_type<ResultType(ClassType::*)(Args...) volatile & > : call_type<ResultType(Args...)> {}; template <typename ClassType, typename ResultType, typename ... Args> struct call_type<ResultType(ClassType::*)(Args...) const volatile & > : call_type<ResultType(Args...)> {}; template <typename ClassType, typename ResultType, typename ... Args> struct call_type<ResultType(ClassType::*)(Args...) && > : call_type<ResultType(Args...)> {}; template <typename ClassType, typename ResultType, typename ... Args> struct call_type<ResultType(ClassType::*)(Args...) const && > : call_type<ResultType(Args...)> {}; template <typename ClassType, typename ResultType, typename ... Args> struct call_type<ResultType(ClassType::*)(Args...) volatile && > : call_type<ResultType(Args...)> {}; template <typename ClassType, typename ResultType, typename ... Args> struct call_type<ResultType(ClassType::*)(Args...) const volatile && > : call_type<ResultType(Args...)> {}; template <typename ResultType, typename ... Args> struct call_type<ResultType(*)(Args...)> : call_type<ResultType(Args...)> {}; template <typename ResultType, typename ... Args> struct call_type<ResultType(&)(Args...)> : call_type<ResultType(Args...)> {}; template <typename ResultType, typename ... Args> struct call_type<ResultType(Args...)> { using type = ResultType(Args...); }; template <class R, class ... Args> struct any_function { virtual ~any_function() {}; virtual R invoke(Args ... args)=0; virtual std::unique_ptr<any_function<R,Args...>> clone() const = 0; }; template <class F, class R, class ... Args> struct concrete_function : any_function<R, Args...> { template <class Q> concrete_function(Q&& f) : f_(std::forward<Q>(f)) {}; R invoke(Args ... args) override { return f_(std::forward<Args>(args)...); } std::unique_ptr<any_function<R,Args...>> clone() const override { return std::make_unique<concrete_function<F,R,Args...>>(f_); } private: F f_; }; } template <class Function> struct function; template <class R, class ... Args> struct function<R(Args...)> { using self_type = function<R(Args...)>; using result_type = R; using argument_types = std::tuple<Args...>; function() {}; function(self_type&& other) : ptr(std::move(other.ptr)) {} function(const self_type& other) : ptr(other.clone()) {} self_type& operator=(const self_type& other) { ptr = other.clone(); return *this; } self_type& operator=(self_type&& other) { ptr = std::move(other.ptr); return *this; } template <class F> function(F&& f) : ptr(std::make_unique<detail::concrete_function<F,R,Args...>>(std::forward<F>(f))) {} R operator()(Args ... args) { assert(ptr); return ptr->invoke(std::forward<Args>(args)...); } private: std::unique_ptr<detail::any_function<R,Args...>> clone() const { if(ptr) { return ptr->clone(); } else { return nullptr; } } std::unique_ptr<detail::any_function<R,Args...>> ptr; }; template<class F> decltype(auto) make_function(F&& f) { return function<typename detail::call_type<F>::type>(std::forward<F>(f)); }
Orient
11.06.2015 07:48Вы используете forwarding-references, но для случая `const T &` код не сработает:
template <typename T> struct call_type : call_type<decltype(&T::operator())> {};
Такой user-код даёт hard-error:
struct A { void operator () () const { ; } }; A const a{}; auto f = make_function(a);
Необходимо использовать `std::remove_reference_t`.Eivind
11.06.2015 11:53Как раз об этом подумал, но вы меня обогнали. Еще нужно поправить и в конструкторе. Но обычный std::remove_reference_t не подойдет, тогда не получится использовать простые функции. Напишем свой:
Код#include <tuple> #include <memory> #include <cassert> namespace detail { template <typename T> struct call_type : call_type<decltype(&T::operator())> {}; template <typename ClassType, typename ResultType, typename ... Args> struct call_type<ResultType(ClassType::*)(Args...) > : call_type<ResultType(Args...)> {}; template <typename ClassType, typename ResultType, typename ... Args> struct call_type<ResultType(ClassType::*)(Args...) const > : call_type<ResultType(Args...)> {}; template <typename ClassType, typename ResultType, typename ... Args> struct call_type<ResultType(ClassType::*)(Args...) volatile > : call_type<ResultType(Args...)> {}; template <typename ClassType, typename ResultType, typename ... Args> struct call_type<ResultType(ClassType::*)(Args...) const volatile > : call_type<ResultType(Args...)> {}; template <typename ClassType, typename ResultType, typename ... Args> struct call_type<ResultType(ClassType::*)(Args...) &> : call_type<ResultType(Args...)> {}; template <typename ClassType, typename ResultType, typename ... Args> struct call_type<ResultType(ClassType::*)(Args...) const & > : call_type<ResultType(Args...)> {}; template <typename ClassType, typename ResultType, typename ... Args> struct call_type<ResultType(ClassType::*)(Args...) volatile & > : call_type<ResultType(Args...)> {}; template <typename ClassType, typename ResultType, typename ... Args> struct call_type<ResultType(ClassType::*)(Args...) const volatile & > : call_type<ResultType(Args...)> {}; template <typename ClassType, typename ResultType, typename ... Args> struct call_type<ResultType(ClassType::*)(Args...) && > : call_type<ResultType(Args...)> {}; template <typename ClassType, typename ResultType, typename ... Args> struct call_type<ResultType(ClassType::*)(Args...) const && > : call_type<ResultType(Args...)> {}; template <typename ClassType, typename ResultType, typename ... Args> struct call_type<ResultType(ClassType::*)(Args...) volatile && > : call_type<ResultType(Args...)> {}; template <typename ClassType, typename ResultType, typename ... Args> struct call_type<ResultType(ClassType::*)(Args...) const volatile && > : call_type<ResultType(Args...)> {}; template <typename ResultType, typename ... Args> struct call_type<ResultType(*)(Args...)> : call_type<ResultType(Args...)> {}; template <typename ResultType, typename ... Args> struct call_type<ResultType(&)(Args...)> : call_type<ResultType(Args...)> {}; template <typename ResultType, typename ... Args> struct call_type<ResultType(Args...)> { using type = ResultType(Args...); }; template <typename T> struct remove_reference { using type = T; }; template <typename ResultType, typename ... Args> struct remove_reference<ResultType(&)(Args...)> { using type = ResultType(&)(Args...); }; template <typename T> struct remove_reference <T&> { using type = T; }; template <class R, class ... Args> struct any_function { virtual ~any_function() {}; virtual R invoke(Args ... args)=0; virtual std::unique_ptr<any_function<R,Args...>> clone() const = 0; }; template <class F, class R, class ... Args> struct concrete_function : any_function<R, Args...> { template <class Q> concrete_function(Q&& f) : f_(std::forward<Q>(f)) {}; R invoke(Args ... args) override { return f_(std::forward<Args>(args)...); } std::unique_ptr<any_function<R,Args...>> clone() const override { return std::make_unique<concrete_function<F,R,Args...>>(f_); } private: F f_; }; } template <class Function> struct function; template <class R, class ... Args> struct function<R(Args...)> { using self_type = function<R(Args...)>; using result_type = R; using argument_types = std::tuple<Args...>; function() {}; function(self_type&& other) : ptr(std::move(other.ptr)) {} function(const self_type& other) : ptr(other.clone()) {} self_type& operator=(const self_type& other) { ptr = other.clone(); return *this; } self_type& operator=(self_type&& other) { ptr = std::move(other.ptr); return *this; } template <class F> function(F&& f) : ptr(std::make_unique<detail::concrete_function<typename detail::remove_reference<F>::type,R,Args...>>(std::forward<F>(f))) {} R operator()(Args ... args) { assert(ptr); return ptr->invoke(std::forward<Args>(args)...); } private: std::unique_ptr<detail::any_function<R,Args...>> clone() const { if(ptr) { return ptr->clone(); } else { return nullptr; } } std::unique_ptr<detail::any_function<R,Args...>> ptr; }; template<class F> decltype(auto) make_function(F&& f) { return function<typename detail::call_type<typename detail::remove_reference<F>::type>::type>(std::forward<F>(f)); }
Orient
11.06.2015 12:25template <typename T> struct call_type : call_type<decltype(&std::remove_reference_t< T >::operator())> {};
Я бы сделал так.Eivind
11.06.2015 12:40Да, но нам еще нужно вывести тип функтора в конструкторе, и при этом сохранить ссылки на обычные функции.
template <class F> function(F&& f) : ptr(std::make_unique<detail::concrete_function<typename detail::remove_reference<F>::type,R,Args...>>(std::forward<F>(f))) {}
vScherba
11.06.2015 00:37// Какой должна быть сигнатура функции, принимающей такой аргумент? someOtherFunction(Lambda);
void someOtherFunction(decltype(Lambda) lam);
Amomum
11.06.2015 01:29+4Один мой знакомый подкинул мне интересную задачку: нужно вызвать функцию через указатель и передать в нее предварительно сохраненные аргументы. Обязательным условием было не использовать std::function.
Вероятно, я чего-то не понял, но почему нельзя было именно это и сделать? То есть, создать просто указатель на функцию и вызвать функцию через него?
gshep
11.06.2015 09:07-1ещё один вариант — Parameter Object design pattern.
У данного подхода большой плюс — не зависит от языка программирования.
Orient
11.06.2015 11:34вызвать функцию через указатель и передать в нее предварительно сохраненные аргументы
Я себе это вижу так:
#include <utility> template< typename ...types > auto make_caller(types &&... _values) { return [_values...] (auto && callee) mutable -> decltype(auto) { return std::forward< decltype(callee) >(callee)(std::forward< types >(_values)...); }; } // main.cpp #include <iostream> #include <cstdlib> struct A { template< typename ...types > void operator () (types...) const & { std::cout << __PRETTY_FUNCTION__ << std::endl; } template< typename ...types > void operator () (types...) && { std::cout << __PRETTY_FUNCTION__ << std::endl; } }; void f(int, float, double, long double) { std::cout << __PRETTY_FUNCTION__ << std::endl; } int main() { auto caller = make_caller(1, 1.0f, 1.0, 1.0L); caller(A{}); A const a{}; caller(a); caller(f); caller(&f); // в точности то, что нужно return EXIT_SUCCESS; }
topa
11.06.2015 12:07-4Это уже не тот C++, который я знал и любил :(
dyadyaSerezha
11.06.2015 12:47+2Просто чел обожает лечить зубы перректально. Новый стандарт призван сделать код более чистым, кратким и понятным, а не для того, чтобы воротить горы трубочек, колесиков и рычажков для тривиальной вещи, прямо поддерживаемой языком.
dyadyaSerezha
11.06.2015 13:17+1Чтобы не быть голословным:
int p1 = 1, p2 = 2;
typedef double (*FuncT)(int i1, int i2);
FuncT f1 = xyz; // тут имя любой фунции с той же сигнатурой.
double d1 = f1(p1, p2);
или, если сам вызоыв надо передать как параметр:
double myFunc() { return f1(p1, p2); }
или как-угодно еще, в зависимости от требований, но в 2-3 строчки!
kloppspb
11.06.2015 13:41>Обязательным условием было не использовать std::function
Этот ёжик — что, вообще про C и стек ничего не знает?
nickolaym
11.06.2015 14:52+3Лямбду можно вызвать, скопировать, привести к функции (если она без связанных переменных)… и, внезапно, привести к std::function.
Если std::function не проходит через слишком узкие ограничения «мой приятель мне запретил», — ну какие проблемы, копируем минимум-миниморум из стандартной библиотеки, переименовываем и получаем то же самое, только без костылей и велосипедов.
Но, видимо, каждый программист должен написать велосипедную версию std::function, а в перспективе — велосипедные форт и лисп. (Точнее, написать велосипедный лисп — это не долженствование, а неизбежность).
CodingMayhem Автор
В коде второго примера заметил опечатку:
using T1 = typename TypeAt<0u, Arguments...>::Type;
Вместо той сторки должна была быть эта:
using T1 = typename TypeAt<1u, Arguments...>::Type;