20

I'm making a very simple class to represent positions in 3D space.

Currently, I'm just letting the user access and modify the individual X, Y and Z values directly. In other words, they're public member variables.

template <typename NumericType = double>
struct Position
{
    NumericType X, Y, Z;

    // Constructors, operators and stuff...
};

The reasoning behind this is that, because NumericType is a template parameter, I can't rely on there being a decent way to check values for sanity. (How do I know the user won't want a position to be represented with negative values?) Therefore, there's no point in adding getters or setters to complicate the interface, and direct access should be favored for its brevity.

Pos.X = Pos.Y + Pos.Z; // Versus...
Pos.SetX(Pos.GetY() + Pos.GetZ());

Is this an okay exception to good practice? Will a (hypothetical) future maintainer of my code hunt me down and punch me in the face?

Maxpm
  • 20,568
  • 29
  • 91
  • 159

6 Answers6

16

The idea behind using getters and setters is to be able to perform other behavior than just setting a value. This practice is recommended because there are a multitude of things you might want to retrofit into your class.

Common reasons to use a setter (there are probably more):

  • Validation: not all values allowed by the type of the variable are valid for the member: validation is required before assignment.
  • Invariants: dependent fields might need to be adjusted (e.g. re-sizing an array might require re-allocation, not just storing the new size).
  • Hooks: there is extra work to perform before/after assignment, such as triggering notifications (e.g. observers/listeners are registered on the value).
  • Representation: the field is not stored in the format "published" as getters and setters. The field might not even stored in the object itself; the value might be forwarded to some other internal member or stored in separate components.

If you think your code will never, ever use or require any of the above, then writing getters and setters by principle is definitely not good practice. It just results in code bloat.

Edit: contrarily to popular belief, using getters and setters is unlikely to help you in changing the internal representation of the class unless these changes are minor. The presence of setters for individual members, in particular, makes this change very difficult.

André Caron
  • 41,491
  • 10
  • 58
  • 117
  • Especially if the underlying representation is more or less three floats. As an example, look at what Microsoft did for XNA (C#, but irrelevant to my point): http://msdn.microsoft.com/en-us/library/microsoft.xna.framework.vector3_members.aspx All fields public, I've seen multiple libraries and framework do this, and I don't think it leaves many people sleepless. – Skurmedel Jun 30 '11 at 00:00
  • 1
    Another key reason could be debugging. – Neil G Jun 30 '11 at 03:20
  • 2
    @Neil: it should be as easy to monitor a function call than to monitor a value change. @Andre: Good point with the *Edit*, encapsulation really works only when the methods express high level operations. If the methods are as fine-tuned as the underlying representation, they are not encapsulating much... – Matthieu M. Jun 30 '11 at 06:32
3

Getters and setters are really only an important design choice if they get/set an abstract value that you may have implemented in any number of ways. But if your class is so straight-forward and the data members so fundamental that you need to expose them directly, then just make them public! You get a nice, cheap aggregate type without any frills and it's completely self-documenting.

If you really do want to make a data member private but still give full access to it, just make a single accessor function overloaded once as T & access() and once as const T & access() const.


Edit: In a recent project I simply used tuples for coordinates, with global accessor functions. Perhaps this could be useful:

template <typename T>
inline T cX(const std::tuple<T,T,T> & t) { return std::get<0>(t); }

typedef std::tuple<double, double, double> coords;
//template <typename T> using coords = std::tuple<T,T,T>; // if I had GCC 4.8

coords c{1.2, -3.4, 5.6};

// Now we can access cX(c), cY(c), cZ(c).
cschol
  • 12,025
  • 11
  • 62
  • 78
Kerrek SB
  • 428,875
  • 83
  • 813
  • 1,025
2

Took me a while, but I tracked this old Stroustrup interview down, where he discusses exposed-data structs versus encapsulated classes himself: http://www.artima.com/intv/goldilocks3.html

Getting more heavily into specifics, there's are dimensions to this that may be missing / understated in existing answers. The benefits of encapsulation increase with:

  • re-compilation/link dependency: low-level library code that is used by large numbers of applications, where those apps may be time-consuming and/or difficult to recompile and redeploy
    • it's usually easier if implementation was out-of-line (which may require pImpl idiom and performance compromises) so you only have to relink, and easier still if you can deploy new shared libraries and simply bounce the app
    • by way of contrast, there's massively less benefit from encapsulation if the object's only used in "non-extern" implementation of a specific translation unit
  • interface stability despite implementation volatility: code where the implementation is more experimental / volatile, but the API requirement is well understood
    • note that by being careful it may be possible to give direct access to member variables while using typedefs for their types, such that a proxy object can be substituted and support identical client-code usage while invoking different implementation
Tony Delroy
  • 94,554
  • 11
  • 158
  • 229
0

The reasoning behind this is that, because NumericType is a template parameter, I can't rely on there being a decent way to check values for sanity. (How do I know the user won't want a position to be represented with negative values?)

The language and compilers support this case well (via specialization).

Therefore, there's no point in adding getters or setters to complicate the interface, and direct access should be favored for its brevity.

Moot argument -- see above.

Is this an okay exception to good practice?

I don't think it is. Your question implies validation should exist, but it's not worth implementing/supporting because you've chosen to use a template in your implementation, and not specialize appropriate for the language feature you've chosen. By that approach, the interface only appears to be partially supported -- those missing implementations will just pollute clients' implementations.

justin
  • 101,751
  • 13
  • 172
  • 222
0

It is ok for such well known structure that :

  1. Can have any possible values, like an int;
  2. Should operate like a built-in type when manipulating it's value, for performance reasons.

However, if you need more than a type that "just is a 3D vector", then you should wrap it in another class, as private member, that would then expose x, y and z through member functions and additional features member functions.

Klaim
  • 60,771
  • 31
  • 121
  • 186
  • Not if it's inlined. (Except in some cases - some PPC compilers have issues with VMX registers in inlined functions, but that's a bit of a special case.) – EboMike Jun 29 '11 at 23:53
  • Yes, sorry I changed the answer. I've read that article I can't find and there were evidence that in some cases even inline code will be compiled in less good assembler than with free functions because of returns or something. Will have to search for it. MAybe it was specific to a console. – Klaim Jun 29 '11 at 23:57
0

If you do some very easy stuff your solution could be just fine.

If you later realize that calculations in a spherical coordinate system are much easier or faster (and you need performance), you can count on that punch.

Karoly Horvath
  • 88,860
  • 11
  • 107
  • 169
  • 2
    If you neeD to swich to spherical coordinates, you will end up with a lot of `setX()` calls that are suddenly incorrect. So a new type is probably more appropriate in this case anyhow. – Dennis Zickefoose Jun 30 '11 at 00:26
  • @Dennis: I'm not saying it happens much in real life, but the textbook-loved x,y,z -> distance,angle,angle implementation change has some validity if you consider that the x,y,z version might add members such as `set(distance,angle,angle)`, `change_distance_from_origin(new_distance)` etc., then empirically it's obverved that their use is more of a bottleneck. Similar issue with broken down day/month/year dates versus epoch+offset - which is more efficient can vary as business needs / algos evolve, but the types need to support both styles of access/modification (lookup tables make both fast). – Tony Delroy Jun 30 '11 at 01:08
  • Some validity, but in practice the choice of whether to compute in cartesian or polar coordinates is going to be made at the level of the application (or the part of the application that knows what's done with these points in space), rather than at the level of the class that represents the point. Two separate classes and the means to convert between them would be as good as one class expressing both representations, unless the class knows better than the application which representation will be more efficient for the application's purposes (which it could, e.g. if it profiles on the fly). – Steve Jessop Jun 30 '11 at 02:07
  • @Steve: "two separate classes...would be as good" - that's not addressing the big question of whether both classes should have the same interface, such that e.g. higher-level algorithmic code can be trivially benchmarked using both implementations. Re self-tuning, profiling may be cool, but there're potential benefits from much simpler lazy conversions where a Point tracks the momentary validity of its cartesian and polar values, only recalculating each as necessary. – Tony Delroy Jun 30 '11 at 02:21
  • @Steve I don't see how two separate classes are necessary, as one class can represent both equally well. Free-standing functions can do the math to convert what's assumed to be a Cartesian `Position` to a polar one, and vice-versa. – Maxpm Jun 30 '11 at 02:44
  • @Maxpm: That is the worst of all possible worlds. How is the user supposed to know if his is actually and not an in disguise, and thus whether or not he needs to call that free-standing member function? *That* is why two separate classes are neccessary... to explicitly codify the semantic meaning you added with that free-standing function. – Dennis Zickefoose Jun 30 '11 at 04:46
  • @Dennis Good point. If C++ implemented `typedef`s properly (i.e., like `alias`es in D) that wouldn't be much of an issue. – Maxpm Jun 30 '11 at 04:57