views:

156

answers:

4

I was converting a struct to a class so I could enforce a setter interface for my variables.
I did not want to change all of the instances where the variable was read, though. So I converted this:

struct foo_t {
    int x;
    float y;
};

to this:

class foo_t {
    int _x;
    float _y;
public:
    foot_t() : x(_x), y(_y) {  set(0, 0.0);  }

    const int &x;
    const float &y;

    set(int x, float y)  {  _x = x;  _y = y;  }
};

I'm interested in this because it seems to model C#'s idea of public read-only properties.
Compiles fine, and I haven't seen any problems yet.

Besides the boilerplate of associating the const references in the constructor, what are the downsides to this method?
Any strange aliasing issues?
Why haven't I seen this idiom before?

A: 

Your approach is not flexible. When you have a getter / setter for every variable it means you don't have to rewrite your set method if you add something to your class.

It's not good because you can't have const and non-const getters (which are used rarely, but sometimes could be useful).

You can't copy references, therefore, your class becomes non-copyable.

Also, having referenced initialized in your class means extra memory and if we're talking about, for example, vertex class (though I think it shouldn't be a class actually), this could become a disaster.


[Everything that follows is completely subjective]

Purpose of getters and setters, to my mind, is not about simple modification, but rather about encapsulating a sequence of actions that results in value modification or returns the value (which we treat as visible result).

Personally struct in case of your example would be more effective, because it wraps POD data and logically "structurizes" it.

Kotti
+5  A: 

There is an aliasing issue in that because you expose a reference to the foo_t's internal data, it's possible for code external to a foo_t object to hold on to references into its data beyond the object's lifetime. Consider:

foo_t* f = new foo_t();
const int& x2 = f->x;
delete f;
std::cout << x2; // Undefined behavior; x2 refers into a foo_t object that was deleted

Or, even simpler:

const int& x2 = foo_t().x;
std::cout << x2; // Undefined behvior; x2 refers into a foo_t object that no longer exists

These aren't particularly realistic examples, but this is a potential issue whenever an object exposes or returns a reference to its data (public or private). Of course, it's just as possible to hold on to a reference to the foo_t object itself beyond its lifetime, but that might be harder to miss or to do by accident.

Not that this is an argument against what you're doing. In fact I've used this pattern before (for a different reason) and I don't think there's anything inherently wrong with it, aside from the lack of encapsulation, which you seem to recognize. The above issue is just something to be aware of.

Tyler McHenry
I was suspecting something like this - thanks.
mskfisher
+11  A: 

One problem is that your class is no longer copyable or assignable, and so can't be stored in C++ containers like vectors. Another is that experienced C++ programmers maintaining your code will look at it and exclaim "WTF!!" very loudly, which is never a good thing.

anon
Touche. But I could fix that by defining them (which I left out for brevity).
mskfisher
(I like how your edit made my comment ambiguous - I could now either be referring to "defining copy constructors" or "defining experienced C++ programmers".)
mskfisher
@mskfisher Such is the wonder of SO - the fact that you commented should not stop me from improving the answer. Anyone that is confused can always look at the edit history. And I must say I found your comment ambiguous to start of with - what are "them" in the context of my original answer?
anon
Defining the copy constructor and assignment operator.
mskfisher
100% agree. The calls for inline accessor functions, not references.
sellibitze
@Neil: Can just anybody look at the edit history? Not everybody has 2K+ rep here.
David Thornley
@David: Yes, anyone can view edit history.
BlueRaja - Danny Pflughoeft
+2  A: 

You could also do something like this, which works for built in types: (Sorry if this code snippet contains errors, but you get the idea)

template <typename T, typename F>
class read_only{
   typedef read_only<T, F> my_type;
   friend F;

public:
   operator T() const {return mVal;}

private:
   my_type operator=(const T& val) {mVal = val; return *this;}
   T mVal;
};


class MyClass {
public:
   read_only <int, MyClass> mInt;
   void MyFunc() {
      mInt = 7; //Works
   }
};

AnyFunction(){
   MyClass myClass;
   int x = myClass.mVal; // Works (okay it hasnt been initalized yet so you might get a warning =)
   myClass.mVal = 7; // Error
}
Viktor Sehr
That's pretty clever I have to say.
Tyler McHenry
I also like that you've eschewed the const reference for return-by-value, which eliminates Tyler's complaint about my method.
mskfisher
AFAIR template parameter cannot be friend, so it will not work and even if it worked it should be "friend class F". There are tricks to work around that by they are nasty and don't work everywhere.
Tomek
@Tomek: Ive made and compiled a similar class in VS2008 (dont have a compiler here right now) without using any tricks.
Viktor Sehr
That doesn't mean that it is correct ;). Try with online Comeau compiler (don't select c++0x extensions) and it will fail. g++ 4.4 fails on this one even with c++0x extension enabled. I've seen quite interesting article (or maybe it was somewhere in a book) on that topic and it compared quite a few compilers with regard to this topic - it was a mess...
Tomek