DinkydauSet / ExploreFractals

A tool for testing the effect of Mandelbrot set Julia morphings
GNU General Public License v3.0
5 stars 1 forks source link

remove readonly-trick? #7

Closed DinkydauSet closed 3 years ago

DinkydauSet commented 3 years ago

FractalParameters has "readonly" members with a macro trick. I don't know if that was a good idea.

DinkydauSet commented 3 years ago

I think it was a good idea because it makes usage of FractalParameters more reliable. Human time and intelligence is limited. Therefore it's good to make mistakes impossible by design. The usage of readonly members ensures that a FractalParameters instance is always in a valid state, no matter what.

...which reminds me there is one mistake left that can still be made: FractalParameters has an initialize function which must be called before use. Maybe I can prevent that by using a constructor, but I think I tried that before and it caused some problems with assigning FractalParameters instances to each other or something like that...

The readonly trick uses macros which are not as reliable as normal c++ because macros literally replace text, which can cause weird errors. It can also be done with templates and operators. This works:

class test {
    template <typename T>
    class readonly
    {
        friend class test;
        T value;

        //for assignment of a T to a readonly<T>
        inline T operator=(const T value) {
            return this->value = value
        }   

        //to use a readonly<T> where a T is expected
        inline operator T() {
            return value;
        }

    public:
        //for public access to value with the () operator
        inline T operator()() {
            return value;
        }
    };

public:
    readonly<int> i;
    void setter(int i_) { i = i_; }
    int use_i() {
        return i+1;
    }
};

int main() {
    test t;
    t.setter(100);
    return t.i() + t.use_i(); //result is 201
}

This construction allows readonly members. An example declaration of a readonly member is:

readonly<int> i;

From inside this class, i can be changed with assignment:

i = 100;

and used as if it's an int:

int j = i + 1;

From outside the class, the value can be read this way where instance is the name of the class instance:

int j = instance.i();

How it works: "friend class" allows private members of readonly to be used by the friend class. This makes it possible for the friend class to use and assign the value through the private operators. Outside of the friend class only the public operator () is available which retrieves the value. Of course the friend class is also permitted to use the operator () and use value directly.

DinkydauSet commented 3 years ago

No, it doesn't work. The fact remains that by using the readonly construction, members of FractalParameters are a different type. I now get errors because a - operator is not defined on readonly etc. To get this working, all operators would have to be redefined. It only works if an automatic cast takes place. In those cases the operator T() does what I want, but automatic casts don't always happen.

DinkydauSet commented 3 years ago

This contructor solves the initialization problem:

public: FractalParameters() {
    initialize();
}
daniel2013 commented 3 years ago

I think this satisfies your requirements, it is much simpler. Basically you just change FractalParameters S; to const FractalParameters S;. Then all members of S are also const by definition (only "shallow const" and not "deep const" but that is of no relevance here).

struct FractalParameters {
    int some_value = 3;
};

class FractalCanvas {
public:
    const FractalParameters S;

    FractalCanvas(const FractalParameters &parameters, unsigned int number_of_threads)
        : S(parameters) { // Note that `S` can now only be initialized in the member initializer list because it is `const`.
        // ...
    }

    void someFunction() {
        // e.g. `S.some_value` is read-only here
    }
};
DinkydauSet commented 3 years ago

That's also an interesting idea. I only learned what const really means yesterday. It can be used in all kinds of places and I'm not used to it yet.

What I wanted to achieve is making it impossible to ruin FractalParameters with invalid values. Your suggestion solves 90% of the problem because the FractalParameters instance in FractalCanvas is the most important one. What I really wanted was a perfect solution, so that even when you don't declare a FractalParameters as const, it still can never be in an invalid state. The macro that I already have creates private members and generates a get-function:

#define get_trick(name) get_ ## name
#define readonly(type, name) \
 private: type name; \
 public: inline type get_trick(name)() {\
        return name;\
 }

which is a perfect solution, other than that it's a macro.

The solution with the readonly class also solves the problem. I discarded it because it's not as nice as I hoped. Because it stores values inside a different type (readonly<T>) I can't do i++ if i is readonly<int> and I have to do i.value++. It's not a big deal but if I use it I'd be replacing a suboptimal solution (macros) with another suboptimal solution that's probably more confusing. I think get-functions are a widely understood concept.

DinkydauSet commented 3 years ago

I was talking to someone who also suggested to use const, to return a reference that can't be changed, so I think I'll change the macro to this:

  #define get_trick(name) get_ ## name
  #define readonly(type, name) \
   private: type name; \
   public: inline const type& get_trick(name)() {\
          return name;\
   }