4

Yesterday, I was trying to program a basic renderer where the renderer controlled when data was loaded into a shader without the renderable object knowing anything about the shader being used. Being a stubborn person (and not running on enough sleep), I spent several hours trying to have function pointers sent to the renderer, saved, then run at the appropriate time. It wasn't until later that I realized what I was trying to build was a message system. It got me wondering, though, is it possible to save function pointers with arguments directly to be run at a later time in c++.

My original idea looked something like this:

// 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
}

I realize that boost probably supports some kind of similar functionality, but I wanted to avoid using boost.

Is something like this possible, what would the general design look like, and what would it even be used for seeing as a message bus is a much more proven design pattern for something like this?

JeJo
  • 30,635
  • 6
  • 49
  • 88
Sara W
  • 127
  • 1
  • 8
  • 3
    [`std::bind` is along the lines of what you're looking for](https://en.cppreference.com/w/cpp/utility/functional/bind) – user4581301 Aug 28 '18 at 17:01
  • 3
    Or just use a [lambda](https://en.cppreference.com/w/cpp/language/lambda), so long as you capture by value. – Useless Aug 28 '18 at 17:02

2 Answers2

6

std::bind is one approach, but if you have access to C++ 11 and later, you may want to consider using lambdas instead. Scott Meyer recommends their use over std::bind (in most cases) in Effective Modern C++.

A lambda has three parts:

  • the [] part, which identifies the values or references to capture,
  • the () part, which identifies the arguments that will be provided later, when the lambda is invoked.
  • the {} part, which identifies what to do with the captured values and parameters

Simple example:

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

If you want to capture a reference to x, use [&x]. In the example above, the result would then be 17 (i.e. 15 + 2). If you do use a reference, be careful not to let x fall out of scope before the storedFunction, as it would then become a dangling reference to garbage data.

Most compilers support C++ 11 now, but you may need to add the support explicitly in the project settings:

  • Visual Studio: Project Properties/C++/Language/C++ Language Standard
  • gcc: --std=c++11 (or 14, or 17...)
  • CMake also allows you to set the standard: set (CMAKE_CXX_STANDARD 11)
BareMetalCoder
  • 569
  • 5
  • 17
  • If I'm understanding this correctly, you create a lambda that calls the function that you want then save the lambda. Is that correct? – Sara W Aug 28 '18 at 17:21
  • To clarify: during the lambda's creation, the function is not called right away. Creating a lambda puts together the information required to make the call (parameters + function call(s)). Then the lambda is "called"/invoked using (). – BareMetalCoder Aug 28 '18 at 17:27
  • If I recall correctly (big hand waving here), under the hood, creating a lambda creates a class that stores all the values and references passed into [], and creates an operator() that takes the arguments described in the () at the beginning of the lambda. The {} body of the lambda becomes operator()'s logic. Basically. – BareMetalCoder Aug 28 '18 at 17:28
  • 2
    To clarify: `std::bind` also requires C++11. – eerorika Aug 28 '18 at 18:05
3

Is it possible to save function pointers with arguments directly to be run at a later time in c++.

Yes, it is.

First of all, if you are in already C++11 or latter you do not need boost to deal with your current problem.

The simplest and intuitive approach is to, make all your functions as a lambda function (i.e, return lambdas) and store to your

std::queue<storableFunction> instruction;

You will find a detailed explanation about lambdas here: What is a lambda expression in C++11?


Providing storableFunction idea is good as you can tell the function pointer type explicitly for each member functions which you are storing to the modelObj.

However, in case of thinking of storing to some STL containers, you need to use std::function, with some type erasure overhead, which can deal with the different lambda functions, capable of capturing variables in scope).

Here is an example code with 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;
}

Output:

1
2
3
4 String

In your case, Renderer can be written as follows. One thing to note that, you need to make a bit workaround for passing different arguments to the member functions(or lambda captures for sure.)

Side-note: Here you will find some tips to avoid the performance issues due to std::function, which might be helpful.

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;
}
JeJo
  • 30,635
  • 6
  • 49
  • 88
  • I see that makes a lot of sense! I'm still wondering is there any reason why you would use this design pattern over a more traditional message system? – Sara W Aug 28 '18 at 23:09
  • @JustinW Simple reason: the flexibility of Lambda functions which has its definition available, where we need them. On the other side, you can also look for signal slote mechanisum, such as [vdk-signal](https://github.com/vdksoft/signals), [boost-signals2](https://theboostcpplibraries.com/boost.signals2), which actually works as you intended. ( if you do not wannt to use lambdas) – JeJo Aug 29 '18 at 05:10