Можно ли сохранить указатель на функцию с аргументами для последующего использования?
Вчера я пытался запрограммировать базовый рендерер, в котором рендерер контролировал, когда данные загружались в шейдер, но объекту рендеринга ничего не известно об используемом шейдере. Будучи упрямым человеком (и не бегающим в достаточном количестве сна), я потратил несколько часов, пытаясь получить указатели функций, отосланные рендереру, сохранить, а затем запустить в подходящее время. Только позже я понял, что я пытался создать систему сообщений. Меня удивило то, можно ли сохранить указатели на функции с аргументами для последующего запуска в C++.
Моя оригинальная идея выглядела примерно так:
//set up libraries and variables
Renderer renderer();
renderable obj();
mat4 viewMatrix();
// renderer returns and object id
int objID = renderer.loadObj(obj)
int main()
{
//do stuff
while(running)
{
//do stuff
renderer.pushInstruction(//some instruction);
renderer.render();
}
}
// functionPtr.h
#include <functional>
class storableFunction
{
public:
virtual ~storableFunction = 0;
virtual void call() = 0;
};
template<class type>
class functionPtr : public storableFunction
{
std::function<type> func;
public:
functionPtr(std::function<type> func)
: func(func) {}
void call() { func(); }
};
//renderer.h
struct modelObj
{
// model data and attached shader obj
std::queue<storableFunction> instruction;
}
class renderer
{
std::map<int, modelObj> models;
public:
// renderer functions
void pushInputDataInstruction(int id, //function, arg1, arg2);
// this was overloaded because I did not know what type the second argument would be
// pushInputDataInstruction implementation in .cpp
{
models[id].instruction.push(functionPtr(std::bind(//method with args)))
}
void render();
};
//implantation in .cpp
{
for(// all models)
//bind all data
applyInstructions(id);
// this would call all the instructrions using functionptr.call() in the queue and clear the queue
draw();
// unbind all data
}
Я понимаю, что boost, вероятно, поддерживает какую-то похожую функциональность, но я хотел избежать использования boost.
Возможно ли что-то подобное, как будет выглядеть общий дизайн, и что бы он даже использовал, чтобы видеть, как шина сообщений - гораздо более проверенный шаблон проектирования для чего-то подобного?
2 ответа
Можно ли сохранить указатели на функции с аргументами напрямую для последующего запуска в C++.
Да, это так.
Прежде всего, если вы уже в C++11 или более поздней версии, вам не нужно повышение, чтобы справиться с вашей текущей проблемой.
Самый простой и интуитивно понятный подход - сделать все ваши функции лямбда-функцией (т.е. вернуть лямбда-выражения) и сохранить их
std::queue<storableFunction> instruction;
Вы найдете подробное объяснение о лямбдах здесь: что такое лямбда-выражение в C++11?
обеспечение storableFunction
Идея хороша, так как вы можете явно указать тип указателя функции для каждой функции-члена, которую вы храните в modelObj
,
Однако, если вы хотите сохранить некоторые контейнеры STL, вам нужно использовать std::function
, с некоторыми типами издержек на стирание, которые могут иметь дело с различными лямбда-функциями, способными захватывать переменные в области видимости).
Вот пример кода с std::vector
#include <iostream>
#include <vector>
#include <functional>
int main()
{
int arg1 = 4;
std::string arg2 = "String";
std::vector<std::function<void()>> vecFunPtr
{
[](int arg1 = 1){ std::cout << arg1 << std::endl; },
[](float arg1 = 2.0f){ std::cout << arg1 << std::endl; },
[](double arg1 = 3.0){ std::cout << arg1 << std::endl; },
[&arg1, &arg2](){ std::cout << arg1 << " " << arg2 << std::endl; }
};
for(const auto& funs: vecFunPtr) funs(); // call the stored lambdas
return 0;
}
Выход:
1
2
3
4 String
В твоем случае, Renderer
можно записать следующим образом. Стоит отметить, что вам нужно сделать обходной путь для передачи различных аргументов функциям-членам (или лямбда-захватам точно).
Примечание: здесь вы найдете несколько советов, чтобы избежать проблем с производительностью из-за std::function
, что может быть полезно.
class Renderer
{
typedef std::queue<std::function<void()>> modelObj; // you might need modelObj for only this class
typedef std::function<void()> fFunPtr; // typedef for void(*)() using std::function
std::map<int, modelObj> models;
public:
// renderer functions can be written like returning a lambda
fFunPtr rFun1(int arg1) { return [](int arg1 = 1){ std::cout << arg1 << std::endl; }; }
fFunPtr rFun2(double arg1) { return [](float arg1 = 2.0f){ std::cout << arg1 << std::endl; }; }
// function to store them for latter use
void pushInputDataInstruction(const int id, const fFunPtr& funPtr)
{
models[id].push(funPtr);
}
};
int main()
{
Renderer objRender;
//do stuff
while(/*condition*/)
{
//do stuff
objRender.pushInstruction(/* id number, function pointer*/)
renderer.render();
}
return 0;
}
std::bind
Это один из подходов, но если у вас есть доступ к C++ 11 и более поздним версиям, вы можете вместо этого рассмотреть возможность использования лямбд. Скотт Мейер рекомендует их использовать поверх std::bind (в большинстве случаев) в Effective Modern C++.
Лямбда состоит из трех частей:
[]
часть, которая определяет значения или ссылки для захвата,()
часть, которая определяет аргументы, которые будут предоставлены позже, когда лямбда будет вызвана.{}
часть, которая определяет, что делать с захваченными значениями и параметрами
Простой пример:
#include <iostream>
void printValue(int x) {
std::cout << x << std::endl;
}
int main(int argc, char * argv[]) {
int x = 23;
// [x] means 'capture x's value, keep it for later'
// (int y) means 'I'll provide y when I invoke the lambda'
auto storedFunction = [x](int y){return printValue(x + y);};
x = 15;
// Now we invoke the lamda, with y = 2
// Result: 25 (23 + 2), even if x was changed after the lambda was created
storedFunction(2);
return 0;
}
Если вы хотите захватить ссылку на x
использовать [&x]
, В приведенном выше примере результат будет 17 (т.е. 15 + 2). Если вы используете ссылку, будьте осторожны, чтобы не допустить x
выпадать из поля зрения до storedFunction
, поскольку это стало бы свисающей ссылкой на мусорные данные.
Большинство компиляторов сейчас поддерживают C++ 11, но вам может понадобиться добавить поддержку явно в настройках проекта:
- Visual Studio: Свойства проекта /C++/ Язык / Стандарт языка C++
- НКА:
--std=c++11
(или 14, или 17...) - CMake также позволяет установить стандарт:
set (CMAKE_CXX_STANDARD 11)