raytracing.github.io icon indicating copy to clipboard operation
raytracing.github.io copied to clipboard

Revisit public vs private member variable access qualifiers

Open GalenMoo opened this issue 4 years ago • 15 comments

Looks like this line should be private rather than public for the ray class

https://github.com/RayTracing/raytracing.github.io/blob/588b950cd27c41b5e18a2b5e346ff6fd6e73f67a/src/common/ray.h#L36

GalenMoo avatar Feb 21 '21 02:02 GalenMoo

Yep. I think you're right

trevordblack avatar Feb 21 '21 20:02 trevordblack

Actually, it should be public, and we should remove the accessors. There's no reason to guard these members, as it's not possible to create an inconsistent state, so the encapsulation machinery provides only additional complexity for no real value.

hollasch avatar Feb 22 '21 18:02 hollasch

Note that in general we could do an encapsulation pass, as this is something we have scattered among our various classes. The way the code is structured, many of these are simple classes/structs with only helper methods, and no real state management to do.

hollasch avatar Feb 22 '21 19:02 hollasch

If we look at the v1.42.0, the original ray class was:

class ray
{
    public:
        ray() {}
        ray(const vec3& a, const vec3& b, float ti = 0.0) { A = a; B = b; _time = ti;}  
        vec3 origin() const       { return A; }
        vec3 direction() const    { return B; }
        float time() const    { return _time; }
        vec3 point_at_parameter(float t) const { return A + t*B; }

        vec3 A;
        vec3 B;
        float _time;
};

So I can see how today's kludge evolved.

trevordblack avatar Feb 22 '21 19:02 trevordblack

I added the additional public specifier to mark the beginning of member variables, preserving original access class. If we decided in the future to convert to greater encapsulation, we could update the access. In this case, it doesn't need to be private, though the method accessors suggest it.

We could convert such situations to use struct instead of class. Even though these keywords are just about synonyms in C++, struct more closely hints at how the class operates logically.

I do admit that the multiple public-public labels are unconventional.

hollasch avatar Feb 22 '21 20:02 hollasch

More and more (and this is a personal opinion), I chafe at the automatic getter-setter method for private member variables, when they are uniformly implemented in trivial fashion. It feels knee-jerk and a bit cargo-cultish to me, adds no additional real value or protection, and often adds a considerable about of useless glitter.

For example, I'm now tempted to just do this:

struct ray {
    point3 origin;
    vec3 direction;
    double time;

    ray(const point3& o, const vec3& d) : origin(o), direction(d), time(0) {}
    ray(const point3& o, const vec3& d, double t) : origin(o), direction(d), time(t) {}

    point3 at(double t) const {
        return orig + t*dir;
    }
};

Notes

  1. Really, our ray is just a struct with three values, helpful constructors, and a single evaluate method.

  2. For simple structs, I prefer the data members up front.

  3. The original ray() {} constructor pattern isn't great. It bypasses the standard language default constructor (which default-initializes all members). This is more efficient when allocating blocks of ray that you don't want to spend time initializing, but we don't do that. We have several classes that use this pattern, and it might make sense to revisit this.

  4. I could almost see naming the members 'p' and 'd', but naming time 't' would be a point of confusion. Since we can't do that, I prefer to spell the names out, as they don't have unambiguous abbreviations.

hollasch avatar Feb 22 '21 20:02 hollasch

Just found out that we need the ray() {} constructor, because we have uninitialized declarations (later assigned), and the compiler doesn't automatically provide the default constructor. You'd need to explicitly include ray() = default; to get that.

hollasch avatar Feb 23 '21 04:02 hollasch

Also, out of curiosity I tried just making all member variables const, but that leaves you with the inability to assign new values to ray variables. Guess that's why you don't often see that pattern.

hollasch avatar Feb 23 '21 04:02 hollasch

We removed all structs from the codebase.

We did it for good reason

trevordblack avatar Feb 23 '21 04:02 trevordblack

? The only thing struct does is default access to public. Flipping to class doesn't change any of the above.

hollasch avatar Feb 23 '21 04:02 hollasch

I don't remember the specific conversation (and I don't really want to go looking for it), but we decided that we should unify as either all struct or all class.

The c++-ism is pretty exclusive to that language.

The specification differences between structs and classes are incredibly minor (and not a good interview question!), but they imply legions. I don't want to needlessly expose readers to the implication.

For some reason I am feeling very strong about this one.

trevordblack avatar Feb 23 '21 04:02 trevordblack

I am perfectly content with:

class ray {
public:
    point3 origin;
    vec3 direction;
    double time;

    ray(const point3& o, const vec3& d) : origin(o), direction(d), time(0) {}
    ray(const point3& o, const vec3& d, double t) : origin(o), direction(d), time(t) {}

    point3 at(double t) const {
        return origin + t*direction;
    }
};

trevordblack avatar Feb 23 '21 04:02 trevordblack

Well, I look at that and see a struct, but I am feeling very not strong about this one. Win win! The only change to the above is that we need to add back the ray() {} constructor.

hollasch avatar Feb 23 '21 04:02 hollasch

I think it makes sense to revisit this across all of our classes/structs. My proposal:

  • If no member variables are directly accessed/set, then we should just flip them to private uniformly.
  • For simple primitives that are more like structs with methods, just move the data to the top of the class (immediately following public:), and eliminate any trivial getters. Such classes should be robust against any single member variable modification (no zombie/invalid states).
  • For more complicated classes, particularly those where member variable state is not trivial, work to make the member variables private. If absolutely necessary (fingers crossed that this doesn't happen), split the member variables as appropriate to public and private sections.

hollasch avatar Feb 25 '21 00:02 hollasch

So the camera class is a useful case where member variables are split into public and private sections. I like the division here, as it's clear which members are more like a public API than the others, which manage state (set in camera::initialize(...)).

One question here is whether the public member variables should be moved to the top of the class. I'm beginning to think they should be.

hollasch avatar Feb 25 '21 00:02 hollasch

Came back review our v4 issues, planning to argue for what we've already apparently agreed upon. Settable data at the top, followed by the public API. Private data and methods follow. Sweet.

hollasch avatar May 11 '23 21:05 hollasch