[ ... ]
I don't consider a method declaration/definition like:
int GetAge( ) { return m_Age; }
to really be that much of a journey into "ugly-land". Nor do I consider:
If you never actually used it, this wouldn't be particularly ugly -- but
when you actually use it, ugliness sets in very quickly.
int theirAge = theObj.GetAge( )
to be really stupendously superior to:
int theirAge = theObj.Age;
I don't either -- I consider it substantially _inferior_. As long as the
only use was as you've shown above, it wouldn't be particularly awful,
but that's rarely the case. What gets truly ugly is things like:
a.setSomething(b.getSomething()+b.getSomethingElse()*c.GetSomething());
which strikes me (at least) as a whole lot less readable than:
a.something = b.something+b.somethingElse*c.something;
And yet, to yield a minor gain in readability you suggest the OP does the
following, for each potential member variable which might need to be turned
into a function one day (to avoid *changing* the code-base):
And you somehow consider that the above is not in the realm of "ugly code" ?
Is this ugly code more acceptable because there's presumably less of it? How
ugly does this get when there are 20 or 30 members that are "converted" in
this way?
First of all, if you honestly have 20 or 30 members, chances are pretty
good that what you have is already ugly and poorly designed -- I can
count on my fingers the number of times I've written a class with that
many member variables.
Second, if you're doing this very much, you (of course) gather the
majority of the code into a template, so most of it ends up something
like:
read_only<int> a;
read_only<float> b;
That doesn't take care of the "friend" declarations but they're pretty
easy to handle in similar fashion (e.g. it's pretty trivial to create a
macro that defines the proper instantiation of read_only and a friend
declaration for it).
That, of course, is assuming that you really need a friend declaration
at all -- my experience is that it's really fairly rare. When you make
data private, it's normally to enforce some set of constraints on that
data. IME, it's _usually_ better to enforce those constraints directly.
In fact, about 90% of the time, what's really wanted is simply to ensure
that that data is always within a defined range. For that, you can use a
template like this:
#include <exception>
#include <iostream>
#include <functional>
template <class T, class less=std::less<T> >
class bounded {
const T lower_, upper_;
T val_;
bool check(T const &value) {
return less()(value, lower_) || less()(upper_, value);
}
void assign(T const &value) {
if (check(value))
throw std::domain_error("Out of Range");
val_ = value;
}
public:
bounded(T const &lower, T const &upper)
: lower_(lower), upper_(upper) {}
bounded(bounded const &init)
: lower_(init.lower), upper_(init.upper)
{
assign(init);
}
bounded &operator=(T const &v) { assign(v); return *this; }
operator T() const { return val_; }
friend std::istream &operator>>(std::istream &is, bounded &b) {
T temp;
is >> temp;
if (b.check(temp))
is.setstate(std::ios::failbit);
else
b.val_ = temp;
return is;
}
};
This lets you express your constraint directly, something like:
bounded<int> x(1,1024);
to say that x should be an int in the range [1..1024). While there
certainly _are_ situations in which that's not what's desired (and it's
not what the OP asked about) I find it fits the bill a whole lot of the
time, and does so much more cleanly than anything with explicit get/set
member functions.
I've also done a version where the range is given as template parameters
instead of ctor parameters. With a few restrictions (e.g. no floating
point ranges) this can cure most performance when/if they arise (though
IME, it's pretty rare). Specifically, this eliminates bounds checking in
the (typical) case of assigning one such bounded object to another of
the same type.
I would rather go back and modify every affected line of my code (ie. to
call a member function rather than access a member variable) rather than
implement *that*.
Unfortunately I think this is an example where "style" is given too much
weight over substance, and the substance (the complexity, maintainability,
and performance of the code) suffers as a result.
Having actual facts based upon years of working with and profiling such
code handicaps me in my desire to agree with you.