From oleg Tue Jun 14 11:00:22 CDT 1994
Newsgroups: comp.lang.c++
Subject: Opaque bases/variables in a class: kludges, suggestions, and dreams
Summary: Emulating opaque private variables / base classes
Followup-To:
Distribution:
Organization: University of North Texas, Denton
Keywords: opaque class variables, opaque base classes, kludges/emulation
Cc:
Status: RO
There has been some discussion recently about access to private
variables. Here's smth on a related topic (sorry, not *so* related)
Consider the following toy class 'foo' declared in file 'foo.h', with
the guts in the file 'foo.cc' and used in the file 'main.cc':
>>> Definition file 'foo.h':
class foo
{
private:
FILE *fp;
int item;
public:
foo(const char * file_name);
~foo(void) {}
void write(void) const;
};
>>>Implementation file 'foo.cc':
#include
#include "foo.h"
foo::foo(const char * file_name) : item(0)
{ assert( fp = fopen(file_name,"w") ); }
void foo::write(void) const
{ assert(fwrite((void *)&item,sizeof(item),1,fp) == 1); }
>>>File that uses it, 'main.cc':
#include "foo.h"
#include
main(void)
{
foo a_foo("foo_file");
a_foo.write();
}
As you see, fp is a private variable of the class foo, all i/o for the
class is done internally, and main.cc does not use files by
itself. However, main.cc must #include just to define FILE
used in the private declaration of foo. Note, that stdio.h got quite a
bit of stuff, which takes some time to process, and which is
absolutely useless for main.cc since it uses none of it.
Well, since struct FILE is referenced only by pointer, I can do the
following trick:
>>> File foo.h, version 2:
class foo
{
private:
#ifdef _stdio_h
FILE *fp;
#else
void * fp;
#endif
int item;
public:
foo(const char * file_name);
void write(void) const;
};
File foo.cc remains the same, but I don't need to #include
in main.cc any more. So, if main.cc doesn't use stdio, it doesn't
need even to include stdio.h . This results in faster compilation,
and looks cooler: don't include stuff we don't use/need.
The situation gets complicated in the following example
>>> File foo.h, 3d version:
class foo: private ofstream
{
int item;
public:
foo(const char * name);
void write(void);
};
>>> File foo.cc, 3d version:
#include
#include "foo.h"
foo::foo(const char * name) : ofstream(name), item(0) {}
void foo::write(void) { *this << item; }
Here again, all i/o for the class foo is done internally, that is,
through an explicitly defined method write(); no access to ofstream of
class foo is possible (that is, by any function that uses foo). But
still, main.cc has *got* to include fstream.h (and it's *a lot* to
process), because in order to construct an object of class foo, the
compiler needs to know the size of the ofstream object, even if the
object itself is not used in the code. Moreover, there are far more
serious ramifications than just overhead from including unnecessary
stuff and general coolness: if someone one day tinkers with the
behavior of the class ofstream and, say, add a new method to it, we
ought to recompile main.cc (because it depends on "fstream.h",
formally speaking). But in reality, we can't use this new method of
ofstream anyway, so there is no real ("behavioral") dependency. Well,
we still need to recompile main.cc if the size of ofstream is changed
(as the result of someone's messing with the stream library).
Well, one can fall over backwards and write the following
>>> File foo.h, version 4:
class foo
{
#ifdef _fstream_h
ofstream& my_fstream;
#else
char& my_fstream;
#endif
int item;
public:
foo(const char * name);
~foo(void);
void write(void) const;
};
>>> File foo.cc, version 4:
#include
#include "foo.h"
foo::foo(const char * name) : my_fstream(*(new ofstream(name))), item(0) {}
void foo::write(void) const { my_fstream << item; }
foo::~foo(void) { delete &my_fstream; }
Here again we refer to the ofstream only by reference, which always
takes only 4 bytes (on UNIX). So, should one change the definition of
ofstream, we still don't need to recompile main.cc because we don't
use ofstream directly in main.cc. Happily, the size of the reference
does not depend on the size of the object it refers to. We don't even
need to #include in main.cc any more. Well, that code
above, version 4, is an obvious kludge (and everybody would boo at
that). Yet it's a working kludge, I wrote once smth along these lines,
and it worked perfectly. Though uncool.
Here is my wish: imagine I didn't have to resort to these
ploys with the cpp, because the compiler would do a similar kludges
for me. Imagine there be a keyword 'opaque' in C++.
So, if I write
class foo {
opaque XYZ * bar;
}
and XYZ is undefined, then the compiler wouldn't spit at me, providing
'bar' is not referenced in that particular compilation unit. Why
should compiler even care as to the exact type of the pointer, if it's
not used anyway. Stretching the suggestion further, why won't we wish
that compiler interpreted
class foo: opaque private Bar
{
};
as
class foo {
opaque Bar& bar;
};
Well, a bit more tinkering is actually necessary, but it's all
feasible: actually, virtual base classes are sometimes implemented in
a similar way. Well, we lose a bit in efficiency, we can't have
inline methods that use Bar (or bar), but we save in reducing
dependency and we cut down on processing "unnecessary" #include's (in
a real example where I came across this situation, #including really
takes a while, sometimes conflicts arise and the compiler chokes up).
In a far-fetched wish, I dream of a day when C++ would get rid
of .h files and would talk about standard contexts (or even levels of
standard contexts/environments), smth similar to what was done in
Algol 68. What I mean is an efficient database of classes (and sundry
functions). The compiler queries whenever it needs a
declaration/description of a particular class/object; the compiler
doesn't have to entirely load it up. Unlike regular precompiled
headers, one doesn't have to rebuild the database if one declaration
changes, the database can allow duplication (with some mechanism: be
it a PATH or a project tag to resolve ambiguity), and the database can
distinguish between changes that affect the size of an object, and
changes (like adding/altering a member function) that essentially does
not "change" the object per se, only its behavior. [ disclaimer: it
was just a dream, it doesn't have to be logical nor feasible ].
Oleg
P.S. I'm not a frequent reader of this newsgroup, please send comments
if any to oleg@ponder.csci.unt.edu or oleg@unt.edu