2

I'm creating a node system (similar to eg. UE4 or Blender's Cycles) in which i can create nodes of different types and use them later. At the moment I have 2 classes of nodes with output functions like these:

class InputInt
{
public:
    int output()
    {
        int x;
        std::cin>>x;
        return x;
    }
};

class RandomInt
{
public:
    int rand10()
    {
        int x;
        x = rand()%10;
        return x;
    }
    int rand100()
    {
        int x;
        x = rand()%100;
        return x;
    }
};

I don't pass anything to these nodes. Now I want to create a node which takes and output function from and object of one of above classes. Here is how I implemented it to use InputInt node only:

class MultiplyBy2
{
    typedef int (InputInt::*func)();

    func input_func;
    InputInt *obj;

public:
    MultiplyBy2(InputInt *object, func i): obj(object), input_func(i) {}
    int output()
    {
        return (obj->*input_func)()*2;
    }
};

Having this I can create and use object of MultiplyBy2 in main() and it works perfectly.

int main()
{
    InputInt input;
    MultiplyBy2 multi(&input, input.output);
    std::cout<<multi.output()<<std::endl;
}

It doesn't obviously work for object of RandomInt as I have to pass *InputInt object to MultiplyBy2 object. Is there a way to make MultiplyBy2 take any kind of an object with its output function eg. like this?

int main()
{
    RandomInt random;
    MultiplyBy2 multi2(&random, random.rand10);
    std::cout<<multi2.output()<<std::endl;
}
Elgirhath
  • 329
  • 4
  • 13
  • Have a look at [The Tiny Calculator Project](https://stackoverflow.com/a/46965151/7478597) there I modeled something similar as Abstract Syntax Tree (with execution feature). I believe, this solved it by a little bit different design of classes. – Scheff's Cat Aug 08 '18 at 10:43

3 Answers3

4

An alternative approach, using a common base class with virtual methods:

#include <iostream>

struct IntOp {
  virtual int get() = 0;
};

struct ConstInt: IntOp {
  int n;
  explicit ConstInt(int n): n(n) { }
  virtual int get() override { return n; }
};

struct MultiplyIntInt: IntOp {
  IntOp *pArg1, *pArg2;
  MultiplyIntInt(IntOp *pArg1, IntOp *pArg2): pArg1(pArg1), pArg2(pArg2) { }
  virtual int get() override { return pArg1->get() * pArg2->get(); }
};

int main()
{
  ConstInt i3(3), i4(4);
  MultiplyIntInt i3muli4(&i3, &i4);
  std::cout << i3.get() << " * " << i4.get() << " = " << i3muli4.get() << '\n';
  return 0;
}

Output:

3 * 4 = 12

Live Demo on coliru


As I mentioned std::function in post-answer conversation with OP, I fiddled a bit with this idea and got this:

#include <iostream>
#include <functional>

struct MultiplyIntInt {
  std::function<int()> op1, op2;
  MultiplyIntInt(std::function<int()> op1, std::function<int()> op2): op1(op1), op2(op2) { }
  int get() { return op1() * op2(); }
};

int main()
{
  auto const3 = []() -> int { return 3; };
  auto const4 = []() -> int { return 4; };
  auto rand100 = []() -> int { return rand() % 100; };
  MultiplyIntInt i3muli4(const3, const4);
  MultiplyIntInt i3muli4mulRnd(
    [&]() -> int { return i3muli4.get(); }, rand100);
  for (int i = 1; i <= 10; ++i) {
    std::cout << i << ".: 3 * 4 * rand() = "
      << i3muli4mulRnd.get() << '\n';
  }
  return 0;
}

Output:

1.: 3 * 4 * rand() = 996
2.: 3 * 4 * rand() = 1032
3.: 3 * 4 * rand() = 924
4.: 3 * 4 * rand() = 180
5.: 3 * 4 * rand() = 1116
6.: 3 * 4 * rand() = 420
7.: 3 * 4 * rand() = 1032
8.: 3 * 4 * rand() = 1104
9.: 3 * 4 * rand() = 588
10.: 3 * 4 * rand() = 252

Live Demo on coliru

With std::function<>, class methods, free-standing functions, and even lambdas can be used in combination. So, there is no base class anymore needed for nodes. Actually, even nodes are not anymore needed (explicitly) (if a free-standing function or lambda is not counted as "node").


I must admit that graphical dataflow programming was subject of my final work in University (though this is a long time ago). I remembered that I distinguished

  • demand-driven execution vs.
  • data-driven execution.

Both examples above are demand-driven execution. (The result is requested and "pulls" the arguments.)

So, my last sample is dedicated to show a simplified data-driven execution (in principle):

#include <iostream>
#include <vector>
#include <functional>

struct ConstInt {
  int n;
  std::vector<std::function<void(int)>> out;
  ConstInt(int n): n(n) { eval(); }
  void link(std::function<void(int)> in)
  {
    out.push_back(in); eval();
  }
  void eval()
  {
    for (std::function<void(int)> &f : out) f(n);
  }  
};

struct MultiplyIntInt {
  int n1, n2; bool received1, received2;
  std::vector<std::function<void(int)>> out;
  void set1(int n) { n1 = n; received1 = true; eval(); }
  void set2(int n) { n2 = n; received2 = true; eval(); }
  void link(std::function<void(int)> in)
  {
    out.push_back(in); eval();
  }
  void eval()
  {
    if (received1 && received2) {
      int prod = n1 * n2;
      for (std::function<void(int)> &f : out) f(prod);
    }
  } 
};

struct Print {
  const char *text;
  explicit Print(const char *text): text(text) { }
  void set(int n)
  {
    std::cout << text << n << '\n';
  }
};

int main()
{
  // setup data flow
  Print print("Result: ");
  MultiplyIntInt mul;
  ConstInt const3(3), const4(4);
  // link nodes
  const3.link([&mul](int n) { mul.set1(n); });
  const4.link([&mul](int n) { mul.set2(n); });
  mul.link([&print](int n) { print.set(n); });
  // done
  return 0;
}

With the dataflow graph image (provided by koman900 – the OP) in mind, the out vectors represent outputs of nodes, where the methods set()/set1()/set2() represent inputs.

Snapshot of a dataflow graph in Blender

Output:

Result: 12

Live Demo on coliru

After connection of graph, the source nodes (const3 and const4) may push new results to their output which may or may not cause following operations to recompute.

For a graphical presentation, the operator classes should provide additionally some kind of infrastructure (e.g. to retrieve a name/type and the available inputs and outputs, and, may be, signals for notification about state changes).


Surely, it is possible to combine both approaches (data-driven and demand-driven execution). (A node in the middle may change its state and requests new input to push new output afterwards.)

Scheff's Cat
  • 16,517
  • 5
  • 25
  • 45
  • Correct me if I am wrong but with this approach I think I wouldn't be able to create many outputs for one node, eg. `RandomInt` has 2 different outputs and I would like to specify which output I want to use. If there is a way to do that with your approach I would be thankful for explanation. – Elgirhath Aug 08 '18 at 10:57
  • 1
    @koman900 As in `ConstInt`, you can make such operators parametric. You could also introduce `VarInt` or `Assignment` (but this would break the pure functional approach it has in this state). – Scheff's Cat Aug 08 '18 at 11:00
  • 1
    @koman900 But I agree, in this design: one class - one type of operator. (I would call it functor even without the `operator()()`.) I didn't consider this as limitation... – Scheff's Cat Aug 08 '18 at 11:02
  • I think this image of node structure in Blender's Cycles is a proper explanation of that: [link](https://static1.squarespace.com/static/58586fa5ebbd1a60e7d76d3e/t/594b3bec8419c2312790e7e0/1498102947950/?format=750w). In this example we can use 2 different outputs for most of the nodes. – Elgirhath Aug 08 '18 at 11:02
  • 1
    @koman900 Ah - a dataflow language. (I didn't see the image before.) For this case, I would introduce functors which can output tuple like types. (This way, we did it in our professional software.) ...and a type system. But this needs a bit more lines than in the above code. :-) – Scheff's Cat Aug 08 '18 at 11:05
  • @koman900 I could make the `get()` parametric but finally it end's up in something like in acade's answer. (May be, it's worth to mention `std::function` which would make things very flexible.) So, I guess you got two approaches and may combine them to what you intend to do. – Scheff's Cat Aug 08 '18 at 11:13
  • @koman900 If you intend to make a GUI for the data flow, then I would suggest a base class with some kind of reflection (to retrieve input(s) and output(s) from operator nodes). – Scheff's Cat Aug 08 '18 at 11:42
  • 1
    @koman900 I added a 3rd example and some notes about data-driven vs. demand-driven execution. – Scheff's Cat Aug 08 '18 at 12:10
2

You can use templates.

template <typename UnderlyingClass>
class MultiplyBy2
{
    typedef int (UnderlyingClass::*func)();

    func input_func;
    UnderlyingClass *obj;

public:
    MultiplyBy2(UnderlyingClass *object, func i) : obj(object), input_func(i) {}
    int output()
    {
        return (obj->*input_func)() * 2;
    }
};

int main()
{
    // test
    InputInt ii;
    MultiplyBy2<InputInt> mii{ &ii, &InputInt::output };
    RandomInt ri;
    MultiplyBy2<RandomInt> mri{ &ri, &RandomInt::rand10 };
}
acade
  • 245
  • 2
  • 7
0

This is a bit convoluted. However I think you should be making an interface or class that returns a value and the objects should inherit from this. Then the operator class can take any class that inherits from the base/interface. Eg Make an BaseInt class that stores an int and has the output method/ RandomInt and InputInt should inherit from BaseInt

  • That's what I implemented in my answer. :-) (Except that I used a method `get()` instead of overloading the `operator()()`.) – Scheff's Cat Aug 08 '18 at 10:52