C++ Object model

S

sumsin

From 'Inside the C++ Object Model' by 'Stanley B. Lippman'

'The primary strength of the C++ Object Model is its space and runtime
efficiency. Its primary drawback is the need to recompile unmodified
code that makes use of an object of a class for which there has been
an addition, removal, or modification of the nonstatic class data
members.'

Here I can understand the strength of C++ object model but can anybody
elaborate the primary drawback of it.
 
P

Pascal J. Bourguignon

sumsin said:
From 'Inside the C++ Object Model' by 'Stanley B. Lippman'

'The primary strength of the C++ Object Model is its space and runtime
efficiency. Its primary drawback is the need to recompile unmodified
code that makes use of an object of a class for which there has been
an addition, removal, or modification of the nonstatic class data
members.'

Here I can understand the strength of C++ object model but can anybody
elaborate the primary drawback of it.

Imagine you have:

class Widget :public Object {
private:
Rect bounds;
public:
virutal Rect& getBounds(void);
};

and a thousand of subclasses of Widget.


And suddenly, the user requets that the widgets may be oriented by
some angle on the screen. So you add a field:

class Widget :public Object {
private:
Rect bounds;
float angle;
public:
virutal Rect getBounds(void);
virutal float getAngle(void);
};

and lo, you have to recompile all the thousand of subclasses, because
now the field layout has changed, for all these classes.


Contrast that with CLOS:
(defclass widget () ((bounds :initarg :bounds :accessor bounds)))
#1=# said:
(defclass button (widget) ((title :initarg :title :accessor title)))
#1=# said:
(defvar *button-1* (make-instance 'button :bounds #(10 20 110 40) :title "Test")) *BUTTON-1*
(inspect *button-1*)
#<COMMON-LISP-USER::BUTTON #x000334202008>: standard object
type: COMMON-LISP-USER::BUTTON
0 [BOUNDS]: #<ARRAY T (4) #x000334201F10>
1 [TITLE]: "Test"
INSPECT-- type :h for help; :q to return to the REPL ---> :q

We created the superclass and a subclass, and made an instance of the subclass.


Suddenly, we modify the superclass:
(defclass widget () ((bounds :initarg :bounds :accessor bounds) (angle :initarg :angle :accessor angle :initform 0.0)))
WARNING: DEFCLASS: Class BUTTON (or one of its ancestors) is being redefined, instances are obsolete
#1=#<STANDARD-CLASS WIDGET :VERSION 1>

Of course new instances of the subclasses will take into account the
changes (nothing has to be done about the subclasses themselves):
(defvar *button-2* (make-instance 'button :bounds #(10 20 110 40) :angle (/ pi 3) :title "At an angle")) *BUTTON-2*
(inspect *button-2*)
#<COMMON-LISP-USER::BUTTON #x00033426C9A0>: standard object
type: COMMON-LISP-USER::BUTTON
0 [BOUNDS]: #<ARRAY T (4) #x00033426C878>
1 [ANGLE]: 1.0471975511965977461L0
2 [TITLE]: "At an angle"
INSPECT-- type :h for help; :q to return to the REPL ---> :q


But what's more, the old instances of the subclasses have been updated too:
(inspect *button-1*)
#<COMMON-LISP-USER::BUTTON #x000334202008>: standard object
type: COMMON-LISP-USER::BUTTON
0 [BOUNDS]: #<ARRAY T (4) #x000334201F10>
1 [ANGLE]: 0.0
2 [TITLE]: "Test"
INSPECT-- type :h for help; :q to return to the REPL ---> :q


In CLOS, it's even possible to define (and compile) subclasses before having defined the superclass:
(defclass window (view) ())
#1=# said:
(defclass view (widget) ())
#1=# said:
(make-instance 'window :bounds #(0 0 512 342))
# said:
(inspect *)
#<COMMON-LISP-USER::WINDOW #x0003342E24C0>: standard object
type: COMMON-LISP-USER::WINDOW
0 [BOUNDS]: #<ARRAY T (4) #x0003342E2400>
1 [ANGLE]: 0.0
INSPECT-- type :h for help; :q to return to the REPL ---> :q
 
M

Michael DOUBEZ

sumsin a écrit :
From 'Inside the C++ Object Model' by 'Stanley B. Lippman'

'The primary strength of the C++ Object Model is its space and runtime
efficiency. Its primary drawback is the need to recompile unmodified
code that makes use of an object of a class for which there has been
an addition, removal, or modification of the nonstatic class data
members.'

Here I can understand the strength of C++ object model but can anybody
elaborate the primary drawback of it.

The drawback is in terms of impact upon change: if you change the
definition of a class (its members) ,you have to recompile every piece
of code that uses this class even if it has not been modified.

A solution to this drawback is the pimpl idiom.
 
M

Matthias Buelow

Pascal said:
In CLOS, [...]

That's comparing apples and oranges. C++ is basically a macro assembler,
just like C. It only has more features than C but the basic mode of
operation is the same. Once the code is compiled, the language is gone.
However, it works pretty well in that role (imho). You can't compare
that to a dynamic language (lisp, smalltalk?, obj-C) and complain how
inflexible that model is. One of the stated goals of C++ is to remain
compatible with C's language model as a portable assembler -- not to
introduce any (substantial) language middle layer between the program
and the hardware. For that, it works quite ok but of course this brings
some limitations (aswell as advantages).
 
S

sumsin

sumsin a écrit :




The drawback is in terms of impact upon change: if you change the
definition of a class (its members) ,you have to recompile every piece
of code that uses this class even if it has not been modified.

A solution to this drawback is the pimpl idiom.

You mean if I add any:
- static data member and/or
- non-static member function and/or
- static member function and/or
- virtual member function, even then also the unmodified code will
recompile!!

But that is not the meant of statement I think.
 
S

sumsin

Imagine you have:

class Widget :public Object {
private:
Rect bounds;
public:
virutal Rect& getBounds(void);

};

and a thousand of subclasses of Widget.

And suddenly, the user requets that the widgets may be oriented by
some angle on the screen. So you add a field:

class Widget :public Object {
private:
Rect bounds;
float angle;
public:
virutal Rect getBounds(void);
virutal float getAngle(void);

};

but suppose if user request only for some member functions not for
data members, in that case what will happen? Do we still need to
recompile thousand of the subclasses?
 
M

Michael DOUBEZ

sumsin a écrit :
You mean if I add any:
- static data member and/or
- non-static member function and/or
- static member function and/or
- virtual member function, even then also the unmodified code will
recompile!!

Yes. If you add or remove something you must recompile every compilation
unit that uses this class (in practice that include the header file).

For static data, you can get away with it somewhat (when the value of
the static is defined outside the definition of the class).

This is usually handled by the dependency system (Makefile, ...).
But that is not the meant of statement I think.

IMO, that is what he says:
<< Its primary drawback is the need to recompile unmodified code that
makes use of an object of a class for which there has been an addition,
removal, or modification of the nonstatic class data members.>>

Typically, you may want to change the internal representation of a class
(let say a float into a double). Even if a code doesn't explicitly uses
the member (by example if it is part of the private part of the class),
it must be recompiled.
 
P

Pascal J. Bourguignon

sumsin said:
but suppose if user request only for some member functions not for
data members, in that case what will happen? Do we still need to
recompile thousand of the subclasses?

If it's not a virtual member, then it should be more or less ok not to
recompile, but if it's a virtual member you will have to recompile
because it changes the layout of the vtable, and the numbering of all
the other virtual members following in that class, and in the
subclasses.
 
S

sumsin

If it's not a virtual member, then it should be more or less ok not to
recompile, but if it's a virtual member you will have to recompile
because it changes the layout of the vtable, and the numbering of all
the other virtual members following in that class, and in the
subclasses.

Ok, apart from the sub-classing concept, let we talk in some other
way.
Supose we have two classes say 'foo' and 'bar'.
And class bar uses some instance of class foo then what will happen?

I mean in what condition the class bar will recompile?
 
P

Pascal J. Bourguignon

sumsin said:
Ok, apart from the sub-classing concept, let we talk in some other
way.
Supose we have two classes say 'foo' and 'bar'.
And class bar uses some instance of class foo then what will happen?

I mean in what condition the class bar will recompile?

It's the same, since the selection of the method to be called is done
at the call site, with a virtual method index, and that's this virtual
method index that needs to be globally computed.

Basically, o->m() is implemented as o->vtable[ vindex of method m in class o ]();
and the vindexes are computed so that whatever the subclass the same
index is used for the same virtual method.

The vindex is a literal that is hard coded in all call sites.
 
S

sumsin

Ok, apart from the sub-classing concept, let we talk in some other
way.
Supose we have two classes say 'foo' and 'bar'.
And class bar uses some instance of class foo then what will happen?
I mean in what condition the class bar will recompile?

It's the same, since the selection of the method to be called is done
at the call site, with a virtual method index, and that's this virtual
method index that needs to be globally computed.

Basically, o->m() is implemented as o->vtable[ vindex of method m in class o ]();
and the vindexes are computed so that whatever the subclass the same
index is used for the same virtual method.

The vindex is a literal that is hard coded in all call sites.

Ok, lets we don't talk about virtual member functions. So if I add any
normal function then will it force the recompilation?
 
J

Jerry Coffin

[ ... ]
Ok, lets we don't talk about virtual member functions. So if I add any
normal function then will it force the recompilation?

Most of these questions are almost entirely theoretical, and even in
theory don't have much in the way of solid answers.

In practice, you use make, which only looks at the last modified time on
the _file_ -- if you make _any_ change in a header (even something like
adding a comment that can't possibly have any real effect) it'll re-
compile everything that you've said depends on that file, unless you use
touch (or something similar) to prevent if from doing so.

Keeping track of which real changes to code require recompiling which
dependent code is next to impossible for anything but the most trivial
program -- that's why make exists. Worse, you only get one "last
modified" date per file, where the header dependency has an almost
arbitrary level of complexity.

The result is that for most practical purposes, when you change a
header, you really have two possibilities. The first possibility is that
you've made a truly trivial change that can't possibly affect anything
else. If so, you use touch to prevent other code from being recompiled.
Otherwise, you've made a change that might mean something, and you let
make sort out what needs to be recompiled. In the process you live with
the fact that it's only looking at things on a file-by-file basis, so it
can (and often will) recompile things that aren't really affected by the
change you made. While adding finer granularity to the process could
certainly help some things under some circumstances, to do it you'd just
about have to build a parser for each supported language into the make
tool.

Realistically, unless you're going to try to create a new development
environment that works quite a bit differently from most (any?)
currently in use, there's not a lot of point in trying to explore the
subject in drastically more detail. Even if you are going to try to do
such a thing, I'm pretty sure there are a lot more productive ways to
spend your time than on what's likely to be a rather minimal improvement
in build times.
 
N

Noah Roberts

Jerry said:
[ ... ]
Ok, lets we don't talk about virtual member functions. So if I add any
normal function then will it force the recompilation?

Most of these questions are almost entirely theoretical, and even in
theory don't have much in the way of solid answers.

In practice, you use make, which only looks at the last modified time on
the _file_ -- if you make _any_ change in a header (even something like
adding a comment that can't possibly have any real effect) it'll re-
compile everything that you've said depends on that file, unless you use
touch (or something similar) to prevent if from doing so.

This isn't the only reason. This compilation issue is caused by the
object model itself. Nobody's really touched on this. Imagine:

class Object
{
int x;
public:
bool f() const; // implementation in .cpp file
};

Now, you need to change the implementation of Object so that x is no
longer an int. You're not actually changing ANYTHING about the
interface; you're not adding functionality. The changes to f() occur
inside the .cpp file so that it does what it did but with x being a
double now. No other object should need to know that x has changed its
type...you've followed correct design and there is no coupling in your
design here.

The problem is that any time you do this:

Object obj;

The compiler needs to know what Object looks like: how much memory to
allocate on the stack, does it have a virtual function table, etc...

Therefor, if you change x to a double now all areas of code that use the
Object class in any way much be recompiled. This isn't just a make
issue, it really does need to recompile all that code.

The pimpl does fix this problem by hiding the internals of the class in
an opaque type but this adds heap allocation to the object and that can
be unwanted. There are many performance issues associated with heap
allocation.

The only way that the language could fix this problem that I can think
of would be to do exactly that: implement class objects as pimpls and
heap allocate everything. This "problem" is actually an advantage that
C++ has over other languages.

Personally, I feel that the pimpl idiom is actually more important as a
fix to exception safety issues and the compilation time save is just a
bonus. There are several instances where exception safety can only be
provided through the use of a pimpl.
 
M

Michael DOUBEZ

Noah Roberts a écrit :
[snip]
Personally, I feel that the pimpl idiom is actually more important as a
fix to exception safety issues and the compilation time save is just a
bonus. There are several instances where exception safety can only be
provided through the use of a pimpl.

When working on very large codebase, it can really save time ;
especially when working with shared binaries (for example in some
development process with ClearCase) where it would be a pain.
There are even tools that /clean-up/ un-necessary dependencies and point
out where a forward declaration would save a dependency.

I don't see how pimpl increases exception safety issues bu it does
require some care regarding exception safety. Could you give an example
where pimpl is a solution ?

IMO the main advantage of pimpl is *real* information hiding. Just like
opaque types in C with more or less the same tradeoff (heap allocation
overhead as you mentioned but also the extra indirection). Another
application is dynamic polymorphism (but then I would call it the
body-envelop pattern).
 
J

James Kanze

[ ... ]
Ok, lets we don't talk about virtual member functions. So if
I add any normal function then will it force the
recompilation?
Most of these questions are almost entirely theoretical, and
even in theory don't have much in the way of solid answers.
In practice, you use make, which only looks at the last
modified time on the _file_ -- if you make _any_ change in a
header (even something like adding a comment that can't
possibly have any real effect) it'll re- compile everything
that you've said depends on that file, unless you use touch
(or something similar) to prevent if from doing so.
Keeping track of which real changes to code require
recompiling which dependent code is next to impossible for
anything but the most trivial program -- that's why make
exists. Worse, you only get one "last modified" date per file,
where the header dependency has an almost arbitrary level of
complexity.

I believe that Visual Age did this somewhat differently. I
think it actually did a partial compile of the code when you
checked it, and also kept track of exactly what aspects each bit
of client code depended on, and only recompiled the parts where
recompilation was really necessary. I think it also triggered
this recompilation automatically when you checked the header in,
so that it always had up to date versions in its data base. (It
didn't keep the code, object files, etc. in the file system, but
in a data base.) On the other hand, it did enough things
differently that I don't think it was really conform---in
particular, you could forget all of the includes, and it would
still compile.

I don't know if Visual Age is still available for C++. (IBM
bought Rational, and I think that there C++ compiler today
derives from the old Rational Apex C++, and not Visual Age C++.
But I'm far from sure.)

For all other systems I've heard of, your description fits the
bill. (And theoretically: according to the standard, any change
which changes the token sequence or the name binding in the
class in any way requires recompilation of all components which
include the class definition, according to the one definition
rule.)
 
J

James Kanze

Jerry said:
[ ... ]
Ok, lets we don't talk about virtual member functions. So
if I add any normal function then will it force the
recompilation?
Most of these questions are almost entirely theoretical, and
even in theory don't have much in the way of solid answers.
In practice, you use make, which only looks at the last
modified time on the _file_ -- if you make _any_ change in a
header (even something like adding a comment that can't
possibly have any real effect) it'll re- compile everything
that you've said depends on that file, unless you use touch
(or something similar) to prevent if from doing so.
This isn't the only reason. This compilation issue is caused
by the object model itself. Nobody's really touched on this.

That's what everyone has been talking about. Change a
non-static member, and you have to recompile everything.
class Object
{
int x;
public:
bool f() const; // implementation in .cpp file
};
Now, you need to change the implementation of Object so that x
is no longer an int. You're not actually changing ANYTHING
about the interface; you're not adding functionality. The
changes to f() occur inside the .cpp file so that it does what
it did but with x being a double now. No other object should
need to know that x has changed its type...you've followed
correct design and there is no coupling in your design here.

This is a known "weakness" of C++. It's the price we pay for
being able to allocate objects on the stack (and still use the
usual tools for linking, etc.), rather than being forced to
allocate everything dynamically.

If you really, really know the compiler, you can sometimes play
tricks (although I wouldn't do so except for quick and dirty
tests). If you change the int to a float, for example, there
are a number of compilers where you can get away with
back-dating the header and reinvoking make. I actually did this
once: faced with a six hour build time if I followed the rules,
and knowing that the only data in the class was a pointer, and
that all I'd done was add some private non-virtual functions and
changed the type of the pointer (in fact, the entire
representation of the class was changed, but that was hidden
behind the pointer), I back-dated the header, rebuilt, and ran a
few tests. After they worked, I touched the header again,
started the build, and went home (it was late Saturday
afternoon).

This requires pretty good knowledge about the compiler, however,
and even then, shouldn't be done in a production
environment---it's not worth the risk.

[...]
Personally, I feel that the pimpl idiom is actually more
important as a fix to exception safety issues and the
compilation time save is just a bonus. There are several
instances where exception safety can only be provided through
the use of a pimpl.

I presume you are talking globally about exception safety; using
the pimpl idiom doesn't affect exception safety in the
constructors, but it certainly makes it easier in the assignment
operator.

As for compile times... You must work on very small projects if
it doesn't make a significant difference. In the case
mentionned above (admittedly, some time ago, on significantly
older equipment), it made a difference between something like
six hours, and less than a minute. (The total code base on the
project was something over a million lines, and every single
module used the class I'd just modified.)
 
N

Noah Roberts

James said:
As for compile times... You must work on very small projects if
it doesn't make a significant difference.

That depends on your definition of "small". You seem to like to use
different definitions of words than their common use so I have no idea.
 
J

Jerry Coffin

[ ... how make works ]
I believe that Visual Age did this somewhat differently. I
think it actually did a partial compile of the code when you
checked it, and also kept track of exactly what aspects each bit
of client code depended on, and only recompiled the parts where
recompilation was really necessary. I think it also triggered
this recompilation automatically when you checked the header in,
so that it always had up to date versions in its data base. (It
didn't keep the code, object files, etc. in the file system, but
in a data base.) On the other hand, it did enough things
differently that I don't think it was really conform---in
particular, you could forget all of the includes, and it would
still compile.

True, VAC++ worked differently, but the important point is that it still
automated the process. It was enough smarter about its automation to do
a better job of minimizing recompilation that wasn't really required --
but you still just did your editing and the build system figured out
what needed to be recompile based on what you did. Trying to manually
track what changes forced recompilation of what dependent source was
still entirely unnecessary.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,769
Messages
2,569,580
Members
45,054
Latest member
TrimKetoBoost

Latest Threads

Top