0

I'm working on visualizing the Mandelbrot set as well as a few other fractals and there's a lot of duplicated code but no code reuse.

One of the functions I am using is below:

/**
 * determines whether a pixel lies in the set
 * @params x, y - x and y coordinates on R/I axes
 * @param c - a complex number
 */
void calculateSet(int x, int y, Complex c) {
    Complex z = c.clone();
    int n = 0;
    for (; n < maxDepth; n++) {
        if (z.dis() > 4) { break; }
        z = z^2 + c;
    }
    // some code using n to color the set
}

This follows the Mandelbrot set:

z_(n+1) = z_n^2 + c

But look at the relevant code for the Burning Ship set:

void calculateSet(int x, int y, Complex c) {
    Complex z = c.clone();
    int n = 0;
    for (; n < maxDepth; n++) {
        if (z.dis() > 4) { break; }
        z = abs(z)^2 + c; // ***
    }
    // follows z_(n+1) = abs(z_1)^2 + c
}

All the code save for the starred line is identical. Right now I have separate classes for Mandelbrot, BurningShip, and a few others with the only difference being that one line.

Is there a way to define this expression and pass to a generalized Set class?

Some pseudocode:

class Set {
    // ...
    Set(Type expression) {
        // ...
        // x, y, c initialized
        // ...
        calculateSet(x, y, c, expression);
    }
    void calculateSet(int x, int y, Complex c, Type e) {
        Complex z = c.clone();
        int n = 0;
        for (; n < maxDepth; n++) {
            if (z.dis() > 4) { break; }
            z = e;
        }
    }
};

And I can just use Set to describe any kind of set I wish?

Set mandelbrot = Set(Type("z^2 + c"));
Set burningship = Set(Type("abs(z)^2 + c"));
// etc

I could use if/else statements to have just one class, but it's not generalized.

user6556709
  • 1,272
  • 8
  • 13
gator
  • 3,465
  • 8
  • 36
  • 76
  • What version of C++ are you using? `C++17` has some nice features you could leverage. EDIT: Also, how performance sensitive are you? – druckermanly May 03 '19 at 07:11
  • @druckermanly I am limited to C++03. The performance is poor as is but I can sacrifice a bit more. – gator May 03 '19 at 07:13
  • You can pass functions, and function-like objects, as arguments in C++. – molbdnilo May 03 '19 at 07:13
  • passing code as string? you will create your own parser for this. – Stack Danny May 03 '19 at 07:33
  • @StackDanny the code at the bottom is just to illustrate what I'm trying to do. I'd rather note code my own parser for a string if possible. – gator May 03 '19 at 07:34
  • Passing a function pointer would've been my first idea as well. Thinking twice, I realized that fractal computation might be performance intensive. This reminds me to the basic optimization rule to remove branches from inner loops. One option to achieve this could be templates with hope for the optimization capabilities of the compiler (as templates allow compile time polymorphism). Even C++03 should provide support of templates to some extent. (The other option would be [self-modifiying code](https://en.wikipedia.org/wiki/Self-modifying_code) but I wouldn't this consider seriously in C++.) – Scheff's Cat May 03 '19 at 07:57

3 Answers3

5

Since you're limited to C++03, you can use a function pointer relatively painlessly.

Complex mandlebrotCompute(Complex z, Complex c) {
  return z*z + c;
}

void calculateSet(int x, int y, Complex c, Complex (*func)(Complex, Complex)) {
    Complex z = c.clone();
    int n = 0;
    for (; n < maxDepth; n++) {
        if (z.dis() > 4) { break; }
        z = func(z, c);
    }
}

It is used like the following:

Complex foo;
calculateSet(1, 2, foo, mandlebrotCompute);

It might help make the code cleaner to use a typedef for the function pointer.

druckermanly
  • 2,694
  • 15
  • 27
2

You can make a template, with the function as template argument.
I believe this is the method that provides the most inlining opportunities.

typedef Complex (*Function)(const Complex&, const Complex&);

template<Function fn>
class Set
{
    // ...
    void calculateSet(int x, int y, Complex c) {
        Complex z = c;
        int n = 0;
        for (; n < maxDepth; n++) {
            if (z.dis() > 4) { break; }
                z = fn(z, c)
            }
        // some code...
    }
}

Complex mandelbrot_fn(const Complex& z, const Complex& c)
{
    return z^2 + c;
}

Complex burning_fn(const Complex& z, const Complex& c)
{
    return abs(z)^2 + c;
}


Set<mandelbrot_fn> mandelbrot;
Set<burning_fn> burning_ship;
molbdnilo
  • 64,751
  • 3
  • 43
  • 82
1

That is what lambdas are for I guess.

template<typename Lam>
class Set
{
private:
  Lam lam;

public:
  Set (Lam&& lam) : lam(lam) {}
  void calculateSet(int x, int y, Complex c)
  {
    Complex z = c.clone();
    int n = 0;
    for (; n < maxDepth; n++) {
      if (z.dis() > 4) { break; }
      z = lam(z, c);
    }
  }
};

You can use this class like this:

auto mandelbrot = Set([](Complex z, Complex c) -> Complex {
  return (z*z) + c;
});


auto burningShip = Set([](Complex z, Complex c) -> Complex {
  return abs((z*z)) + c;
});

mandelbrot.calculateSet(...);
burningShip .calculateSet(...);
user3520616
  • 60
  • 3
  • 17
  • Require C++11 for lambda, and require C++17 for Template argument deduction. – Jarod42 May 03 '19 at 07:33
  • I know; the edit was made while I typed the answer. Could still be useful for other people finding this via google I guess so I'll keep it here. – user3520616 May 03 '19 at 07:38
  • Comment was not to remove that answer. but to clarify in which context it can be used. – Jarod42 May 03 '19 at 08:16