Я хотел бы создать шаблонный класс, который мог бы хранить указатель на функцию и аргументы для этой функции, чтобы потом можно было вызывать функцию с этими аргументами.
Я хотел бы написать это универсально и не зависеть от типов аргументов или числа.
Вот отрывок идеи с использованием шаблонов переменных c ++ 11:
template<class T, typename... Params>
class LazyEvaluation {
private:
// Function to be invoked later
T (*f)(Params...);
// Params for function f
Params... storedParams; // This line is not compilable!
bool evaluated;
T result;
public:
// Constructor remembers function pointer and parameters
LazyEvaluation(T (*f)(Params...),Params... params)
: f(f),
storedParams(params) //this line also cannot be compiled
{}
// Method which can be called later to evaluate stored function with stored arguments
operator T&() {
// if not evaluated then evaluate
if (! evaluated) {
result = f(storedParams...);
evaluated = true;
}
return result;
}
}
Я хотел бы, чтобы хотя бы публичный интерфейс этого класса был безопасным, если это возможно. Хотя получить эту работу хотя бы как-то важнее.
Мне удалось как-то сохранить переменное количество аргументов. Но я не смог передать их функции f. Я напишу это в ответах, но я бы хотел, чтобы вы подумали о своих собственных решениях, прежде чем увидите мою уродливую неработающую попытку.
Я пытаюсь скомпилировать приведенный выше код с помощью компилятора Microsoft Visual C ++, ноябрь 2012 г., CTP (v120_CTP_Nov2012), но было бы лучше, если бы существовало независимое от компилятора решение.
Спасибо
Вот как я пытался это решить:
Пакет параметров может быть рекурсивно расширен и каждый параметр сохранен. Функция store должна это делать. Он использует одну (дважды перегруженную) вспомогательную функцию.
template<typename T>
void storeHelperFunction(void*& memory, T last) {
*((T*)memory) = last;
memory = (void*)((char*)memory + sizeof(T));
}
template<typename T, typename... Params>
void storeHelperFunction(void*& memory, T first, Params... rest) {
storeHelperFunction(memory, first);
storeHelperFunction(memory, rest...);
}
template<typename... Params>
void store(void* memory, Params... args) {
// Copy of pointer to memory was done when passing it to this function
storeHelperFunction(memory, args...);
}
Хранилище функций берет указатель на память, где предполагается сохранить переменное число аргументов.
Указатель может указывать на некоторую динамически распределенную память или лучше структуру, размер которой равен sizeof...(Params)
,
Такая структура, которая имеет абсолютно любой требуемый размер, может быть построена с использованием шаблонного метапрограммирования:
template <int N>
struct allocatorStruct {
char byte1;
allocatorStruct<N-1> next;
};
template <>
struct allocatorStruct<1> {};
Я не уверен, что говорит стандарт или как другие компиляторы, кроме Microsoft, компилируют его. Но используя мой компилятор, sizeof (allocatorStruct) равен N для любого N, которое больше или равно 1.
следовательно allocatorStruct<sizeof...(Params)>
имеет тот же размер, что и Params.
Другой способ создать что-то такого же размера, как Params, — это использовать тип char [sizeof...(Params)]
, Это имеет тот недостаток, что компилятор передает только указатель на этот массив, когда вы пытаетесь передать такой массив в качестве аргумента.
Вот почему лучше использовать allocatorStruct<sizeof...(Params)>
,
А теперь основная идея:
При сохранении функции мы можем привести ее к: T (*)(allocatorStruct<sizeof...(Params)>)
,
При сохранении аргументов для функции мы можем сохранить их в структуре типа allocatorStruct<sizeof...(Params)>
,
Размер аргументов одинаков. Несмотря на то, что указатель на функцию относится к типу функции, указанная функция будет правильно получать свои данные.
По крайней мере, я надеялся. В зависимости от соглашения о вызовах я ожидал, что переданные аргументы могут быть переупорядочены или неверны из-за разницы между сохранением аргументов слева направо и передачей справа налево. Но это был не тот случай. При использовании соглашения о вызовах __cdecl был передан только первый аргумент, а другой потерян. С другими соглашениями о вызовах программа перестала работать.
Я не тратил много времени на его отладку и поиск данных в памяти (в стеке). Это хотя бы правильный путь?
Просто используйте лямбда-выражение
// Some function.
int add(int a, int b) {
return a + b;
}
auto lazyFunc = [] { return add(1, 2); };
std::cout << lazyFunc() << std::endl; // Evaluate function and output result.
Если вы действительно хотите создать класс, который оценивает функцию только один раз (лениво), используя переменные шаблоны, вы можете сделать что-то вроде следующего кода.
Я также сделал класс таким, чтобы вам не приходилось создавать новый экземпляр при каждом изменении параметров. Я использую std::tuple
сохранить заданные аргументы и сравнить с ранее заданными аргументами. Если аргументы различаются, функция будет переоценена.
Функции передаются и хранятся с использованием std::function
обертка, поэтому мне не нужно работать с необработанными указателями на функции (гадость).
#include <iostream>
#include <functional>
#include <utility>
#include <tuple>
template <typename T>
class LazyEvaluation {};
template <typename ReturnType, typename... Params>
class LazyEvaluation<ReturnType(Params...)> {
private:
std::function<ReturnType(Params...)> func_;
ReturnType result;
std::tuple<Params...> oldParams; // Contains the previous arguments.
public:
explicit LazyEvaluation(std::function<ReturnType(Params...)> func)
: func_(std::move(func)) {}
template <typename... Args>
ReturnType operator() (Args&&... args) {
auto newParams = std::make_tuple(std::forward<Args>(args)...);
// Check if new arguments.
if (newParams != oldParams) {
result = func_(std::forward<Args>(args)...);
oldParams = newParams;
std::cout << "Function evaluated" << std::endl;
}
std::cout << "Returned result" << std::endl;
return result;
}
};
int main() {
auto f = [] (int a, int b) {
return a + b;
};
// Specify function type as template parameter.
// E.g. ReturnType(Param1Type, Param2Type, ..., ParamNType)
LazyEvaluation<int(int, int)> ld(f);
std::cout << ld(1, 2) << std::endl;
std::cout << ld(1, 2) << std::endl;
std::cout << ld(3, 4) << std::endl;
}
Выход:
Function evaluated
Returned result
3
Returned result
3
Function evaluated
Returned result
7
Приведено стандартное оборудование для формирования пакетов с переменным индексом:
template <std::size_t... I> struct index_sequence {};
template <std::size_t N, std::size_t... I>
struct make_index_sequence : public make_index_sequence<N-1, N-1, I...> {};
template <std::size_t... I>
struct make_index_sequence<0, I...> : public index_sequence<I...> {};
и вызывать функции с неупакованными аргументами кортежа:
template <typename Function, typename... Types, std::size_t... I>
auto apply_(Function&& f, const std::tuple<Types...>& t, index_sequence<I...>)
-> decltype(std::forward<Function>(f)(std::get<I>(t)...)) {
return std::forward<Function>(f)(std::get<I>(t)...);
}
template <typename Function, typename... Types>
auto apply(Function&& f, const std::tuple<Types...>& t)
-> decltype(apply_(f, t, make_index_sequence<sizeof...(Types)>())) {
return apply_(f, t, make_index_sequence<sizeof...(Types)>());
}
Это довольно просто:
template<typename Function, typename... Params>
class LazyEvaluation {
private:
typedef decltype(std::declval<Function>()(std::declval<Params>()...)) result_type;
// Function to be invoked later
Function f;
// Params for function f
std::tuple<Params...> storedParams;
mutable bool evaluated;
union {
std::aligned_storage<sizeof(result_type)> space;
mutable result_type result;
};
// Method which can be called later to evaluate stored function with stored arguments
void evaluate() const {
// if not evaluated then evaluate
if (! evaluated) {
new (&result) result_type{apply(f, storedParams)};
evaluated = true;
}
}
public:
// Constructor remembers function pointer and parameters
LazyEvaluation(Function f, Params... params)
: f(std::move(f)),
storedParams(std::move(params)...),
evaluated(false)
{}
~LazyEvaluation() {
if (evaluated)
result.~result_type();
}
operator result_type&() {
evaluate();
return result;
}
operator const result_type& () const {
evaluate();
return result;
}
};
template <typename Function, typename... Params>
LazyEvaluation<Function, Params...>
make_lazy(Function&& f, Params&&... params) {
return {std::forward<Function>(f), std::forward<Params>(params)...};
}
Я использовал союз и размещение new
сохранить результат оценки так, чтобы он не должен был быть конструируемым по умолчанию типом, а некоторые mutable
трюки, чтобы const LazyEvaluator
может быть преобразован так же как неконстантный экземпляр.