Sponsored Link •
Bjarne Stroustrup: My rule of thumb is that you should have a real class with an interface and a hidden representation if and only if you can consider an invariant for the class.
Bill Venners: What do you mean by invariant?
Bjarne Stroustrup: What is it that makes the object a valid object?
An invariant allows you to say when the object's
representation is good and when it isn't. Take a
vector as a very simple example.
vector knows that it has n elements. It has a pointer to n elements. The invariant is
exactly that: the pointer points to something, and that something can hold n elements. If
it holds n+1 or n-1 elements, that's a bug. If that pointer is zero, it's a bug, because it
doesn't point to anything. That means it's a violation of an invariant. So you have to be
able to state which objects make sense. Which are good and which are bad. And you can write
the interfaces so that they maintain that invariant. That's one way of keeping track that
your member functions are reasonable. It's also a way of keeping track of which operations
need to be member functions. Operations that don't need to mess with the representation
are better done outside the class. So that you get a clean, small interface that you can
understand and maintain.
Bill Venners: So the invariant justifies the existence of a class, because the class takes the responsibility for maintaining the invariant.
Bjarne Stroustrup: That's right.
Bill Venners: The invariant is a relationship between different pieces of data in the class.
Bjarne Stroustrup: Yes. If every data can have any value, then it doesn't make
much sense to have a class. Take a single data structure that has a name and an address.
Any string is a good name, and any string is a good address. If that's what it is, it's a
structure. Just call it a
struct. Don't have anything private. Don't do
anything silly like having a hidden name and address field with
set_name functions. Or even worse, make a virtual base class with virtual
set_name functions and so on, and override it with
the one and only representation. That's just elaboration. It's not necessary.
Bill Venners: It's not necessary because there's one and only representation. The justification is usually that if you make it a function, then you can change the representation.
Bjarne Stroustrup: Exactly, but some representations you don't change. You don't change the representation of an integer very often, or a point, of a complex number. You have to make design decisions somewhere.
And the next stage, where you go from the plain data structure to a real class with real
class objects, could be that name and address again. You probably wouldn't call it
name_and_address. You'll maybe call it
mailing_address. At that stage you believe name and address are not just
strings. Maybe you break the name down into first, middle, and last name strings. Or you
decide the semantics should be that the one string you store really has first, middle, and
last name as parts of it. You can also decide that the address really has to be a valid
address. Either you validate the string, or you break the string up into first
address field, second address field, city, state, country, zip code, that kind of stuff.
When you start breaking it down like that, you get into the possibilities of different representations. You can start deciding, does it really add to have private data, to have a hierarchy? Do you want a plain class with one representation to deal with, or do you want to provide an abstract interface so you can represent things in different ways? But you have to make those design decisions. You don't just randomly spew classes and functions around. And you have to have some semantics that you are defending before you start having private data.
The way the whole thing is conceived is that the constructor establishes the environment for the member functions to operate in, in other words, the constructor establishes the invariant. And since to establish the invariant you often have to acquire resources, you have the destructor to pull down the operating environment and release any resources required. Those resources can be memory, files, locks, sockets, you name it—anything that you have to get and put back afterwards.