Good Design in C: Encapsulation

B

bluejack

Ahoy:

For as long as I've been using C, I've vacillated on the optimal
degree of encapsulation in my designs. At a minimum, I aggregate data
and code that operate on that data into classlike files; but now and
then I go on an opaque type joyride, and create minimalist header
files that define very clean interfaces.

The problem with that is that it prevents some optimizations:

* Can no longer allocate structures on the stack;
* Can no longer put inline functions in the header file;
* Incurs the overhead of a function call for any data-access.

I'm curious as to how C experts feel about this design concept;
reviewing some old (very old) threads about encapsulation, I see that
many dismiss it as though it were a tenet of some foolish philosophy;
and yet, encapsulation has all sorts of benefits to the author of
libraries in terms of the ability to extend, adapt, & refactor code;
it has also helped me in my own designs by illuminating design
mistakes before they get very far.
 
I

Ian Collins

bluejack said:
Ahoy:

For as long as I've been using C, I've vacillated on the optimal
degree of encapsulation in my designs. At a minimum, I aggregate data
and code that operate on that data into classlike files; but now and
then I go on an opaque type joyride, and create minimalist header
files that define very clean interfaces.

The problem with that is that it prevents some optimizations:

* Can no longer allocate structures on the stack;
* Can no longer put inline functions in the header file;
* Incurs the overhead of a function call for any data-access.

I'm curious as to how C experts feel about this design concept;
reviewing some old (very old) threads about encapsulation, I see that
many dismiss it as though it were a tenet of some foolish philosophy;
and yet, encapsulation has all sorts of benefits to the author of
libraries in terms of the ability to extend, adapt, & refactor code;
it has also helped me in my own designs by illuminating design
mistakes before they get very far.
Well I'm an opaque type and encapsulation fan. The last three largish C
projects I worked on used the idiom throughout, with each module having
its own subdirectory with the public headers and nested within that a
subdirectory for private headers and source files.

The main benefits I see are in developer productivity and reduced
dependencies (with tends to improve productivity). Being able to change
core structures without forcing a recompile is a big plus, not so much
on modern systems with small to medium projects, but definitely big
projects and on older tool chains that don't support parallel or
distributed building.

There is a small runtime price to pay, but it has always been one I have
been happy to pay.

By the way, you can avoid dynamic allocation of structures by using
static variables.
 
R

Richard Heathfield

bluejack said:

but now and
then I go on an opaque type joyride, and create minimalist header
files that define very clean interfaces.

The problem with that is that it prevents some optimizations:

* Can no longer allocate structures on the stack;
* Can no longer put inline functions in the header file;
* Incurs the overhead of a function call for any data-access.

I'm curious as to how C experts feel about this design concept;

Ambivalent, which is why I tend to do this in the public header:

#if ABANDON_OPACITY_AND_DAMN_THE_CONSEQUENCES
#include "opaquetypesinternalheader.h"
#endif
 
B

bluejack

By the way, you can avoid dynamic allocation of structures by using
static variables.

You mean, "pre-allocate" a bunch of said structures in the source that
also contains the definition of the structure? True: but not terribly
practical for some uses, and would certainly add complexity to many
environments, esp. multithreaded.

-b
 
B

bluejack

#if ABANDON_OPACITY_AND_DAMN_THE_CONSEQUENCES
#include "opaquetypesinternalheader.h"
#endif

Heh. I like that. Well, not *like* exactly: it makes for a few extra
files, eh? But it's a clever approach.

-b
 
I

Ian Collins

bluejack said:
You mean, "pre-allocate" a bunch of said structures in the source that
also contains the definition of the structure? True: but not terribly
practical for some uses, and would certainly add complexity to many
environments, esp. multithreaded.
One of the projects I mentioned was a static design (16 bit embedded
with limited RAM), so we had to preallocate everything!
 
W

William Ahern

Ahoy:

For as long as I've been using C, I've vacillated on the optimal
degree of encapsulation in my designs. At a minimum, I aggregate data
and code that operate on that data into classlike files; but now and
then I go on an opaque type joyride, and create minimalist header
files that define very clean interfaces.

The problem with that is that it prevents some optimizations:

* Can no longer allocate structures on the stack;

The problem with malloc() and free() is that they're these global
mechanisms; very blunt. I wrote a library which, aside from implementing
some common alternative allocation strategies, defines a strict interface
for providing other alternatives. Almost all of my new code uses this, and
the instantiation methods for encapsulated code always take a 'struct
arena_prototype' as an argument. Providing a stack allocator would be a
cinch. Moral: if you encounter the same problem over and over again,
abstract ;)
* Can no longer put inline functions in the header file;

Why not? If you think about it, functions you typically want to inline
are short and sweet, and thus should be relatively stable. Just an
observation, not an answer.
* Incurs the overhead of a function call for any data-access.

If your encapsulating something, ostensibly the functionality provides
some useful abstraction over the data. In that case, function call
overhead is probably marginal from the get-go.

Also, ubiquitous inter-unit optimizations (including inlining)
is almost upon us. Say, another two or three years to cover 2/3
of environments? Not so bad.
I'm curious as to how C experts feel about this design concept;
reviewing some old (very old) threads about encapsulation, I see that
many dismiss it as though it were a tenet of some foolish philosophy;
and yet, encapsulation has all sorts of benefits to the author of
libraries in terms of the ability to extend, adapt, & refactor code;
it has also helped me in my own designs by illuminating design
mistakes before they get very far.

Well, that's probably the most important criterion of all.
 
I

Ian Collins

William said:
Why not? If you think about it, functions you typically want to inline
are short and sweet, and thus should be relatively stable. Just an
observation, not an answer.
My take on the above was "Can no longer put inline functions *that
access the opaque type* in the header file".
 
W

William Hughes

Ahoy:

For as long as I've been using C, I've vacillated on the optimal
degree of encapsulation in my designs. At a minimum, I aggregate data
and code that operate on that data into classlike files; but now and
then I go on an opaque type joyride, and create minimalist header
files that define very clean interfaces.

The problem with that is that it prevents some optimizations:

* Can no longer allocate structures on the stack;
* Can no longer put inline functions in the header file;
* Incurs the overhead of a function call for any data-access.

I'm curious as to how C experts feel about this design concept;
reviewing some old (very old) threads about encapsulation, I see that
many dismiss it as though it were a tenet of some foolish philosophy;
and yet, encapsulation has all sorts of benefits to the author of
libraries in terms of the ability to extend, adapt, & refactor code;
it has also helped me in my own designs by illuminating design
mistakes before they get very far.


I have not found the run time consequences of encapsulation to
be onerous (or even visible). To a certain extent I consider
that if you need to allocate structures on the stack or use inline
functions on the structure or if the cost of a data-access is
significant, you need to rethink the design.

More problematic is the effect on program modification. With
several layers of encapsulation, making a minor but unanticipated
change can require modification to a dozen files, spread
over several directories.

Encapsulation is great if you can make sure that the user does not
need to see the internals.

- William Hughes
 
W

William Ahern

My take on the above was "Can no longer put inline functions *that
access the opaque type* in the header file".

Ah. Good point. I defer to Richard Heathfield's admission, then ;)

When I would like to use [static] inline functions or macros, I'm
not averse to put some or all of the structure definitions in the header.

The definition for the FILE type is often exposed in stdio.h, but it
doesn't seem that people stick their noses in it that often.

Also, in some circumstances I'm okay with leaving instantiation up to the
user. In that case, I provide static initializers:

struct foo my_foo = foo_initializer;

foo_do_something(&my_foo);

The POSIX threading library works that way:

pthread_mutex_t my_mutex = PTHREAD_MUTEX_INITIALIZER;

assert(0 == pthread_mutex_lock(&my_mutex));

(POSIX condition variables are actually a better example, actually.)
 
B

bluejack

More problematic is the effect on program modification. With
several layers of encapsulation, making a minor but unanticipated
change can require modification to a dozen files, spread
over several directories.

Tell me more:

I generally see good encapsulation as being something that addresses
this very problem. If the *API* is stable, then minor changes to the
implementation should be *less* likely to require changes outside the
file. And if the API is changing, I would imagine changes to files
that access the API to be less intrusive than if you are changing the
structure *and* the APIs.

Are you picturing a situation in which object A encapsulates object B
which encapsulates object C, such that a significant change in C will
require alterations to higher level APIs as well? I have a hard time
picturing a case where encapsulation would make that worse than if the
structure members were accessed directly. Do you have an example?

Or were you thinking of something else?
 
W

William Hughes

Tell me more:

I generally see good encapsulation as being something that addresses
this very problem. If the *API* is stable, then minor changes to the
implementation should be *less* likely to require changes outside the
file. And if the API is changing, I would imagine changes to files
that access the API to be less intrusive than if you are changing the
structure *and* the APIs.

Are you picturing a situation in which object A encapsulates object B
which encapsulates object C, such that a significant change in C will
require alterations to higher level APIs as well? I have a hard time
picturing a case where encapsulation would make that worse than if the
structure members were accessed directly. Do you have an example?

Or were you thinking of something else?

I was thinking of the case where the modification was to something
that used A, but the modification needed information from C.

E.g. a piece of state in C is needed
by somthing using A, but the need for this piece of state
was not anticipated. If there is no encapsulation I can write
something like

needed_state = a_pointer->b_pointer->c_pointer->low_level_state


With encapsulation I need to write something like

needed_state = a_pointer_get_needed_state(a_pointer)
which needs
needed_state = b_pointer_get_needed_state(b_pointer)
which needs
needed_state = c_pointer_get_needed_state(c_pointer)


and I have to modify a bunch of files.

- William Hughes
 
D

David Tiktin

For as long as I've been using C, I've vacillated on the optimal
degree of encapsulation in my designs. At a minimum, I aggregate
data and code that operate on that data into classlike files; but now
and then I go on an opaque type joyride, and create minimalist header
files that define very clean interfaces.

[snip]
I'm curious as to how C experts feel about this design concept;

Good question!

I default to opaque types and function based interfaces. I've found
over the years that this has advantages that outweigh everything
else, primarily because I create a lot of reusable library code. I
can fix bugs, augment functionality and change implementation without
"recompiling the world" due to a change to a struct defined in a
header. I *know* no module knows the structure's definition when
it's defined in the .c file!

But every rule has it's exceptions, often for the reasons you gave.
I have one module which is fundamental to many of my projects, an
abstract type Buffer. It's used in ISRs in device drivers, among
other places, so function call overhead for accessing variables is
out of the question, and the code must manipulate the Buffer
internals anyway. In that case, I swallow hard, export the struct
definition and provide function macros which do exactly what the
functions do (and implement the functions using the macros to make
sure). So buf_Count(b) (the function) is implemented as:

return buf_COUNT(b);

and buf_COUNT(b) is implemented (in buffer.h) as:

#define buf_COUNT(b) ((b)->end - (b)->start)

(Most of the compilers I use don't support inline functions.) I use
the function where I can and the macro where I must.

But really, cases like this where I've needed to export module
internals are pretty rare, and for me, the reduced coupling provided
by opaque types overwhelms almost every other consideration.

Dave
 
B

Ben Pfaff

bluejack said:
For as long as I've been using C, I've vacillated on the optimal
degree of encapsulation in my designs. At a minimum, I aggregate data
and code that operate on that data into classlike files; but now and
then I go on an opaque type joyride, and create minimalist header
files that define very clean interfaces.

I use fully opaque types when maintainability is more important
than performance. As performance becomes more important, I
reduce the degree of opacity as necessary.

I prefer to use profiling as the indicator of when performance is
important.
 
R

Roland Pibinger

I go on an opaque type joyride, and create minimalist header
files that define very clean interfaces.
The problem with that is that it prevents some optimizations:

* Can no longer allocate structures on the stack;

You can, e.g.

struct my_struct {
PROPERLY_ALIGNED_TYPE data[32];
};

PROPERLY_ALIGNED_TYPE is just a placeholder. In your *.c file you need
to cast the my_struct object to your hidden real implementation
struct.
* Can no longer put inline functions in the header file;
* Incurs the overhead of a function call for any data-access.
I'm curious as to how C experts feel about this design concept;
reviewing some old (very old) threads about encapsulation, I see that
many dismiss it as though it were a tenet of some foolish philosophy;

Encapsulaton isn't useful for all objects but in general I'd agree
with you: the more the better.

Best regards,
Roland Pibinger
 
C

CBFalconer

David said:
.... snip ...

But every rule has it's exceptions, often for the reasons you gave.
I have one module which is fundamental to many of my projects, an
abstract type Buffer. It's used in ISRs in device drivers, among
other places, so function call overhead for accessing variables is
out of the question, and the code must manipulate the Buffer
internals anyway. In that case, I swallow hard, export the struct
definition and provide function macros which do exactly what the
functions do (and implement the functions using the macros to make
sure). So buf_Count(b) (the function) is implemented as:

return buf_COUNT(b);

and buf_COUNT(b) is implemented (in buffer.h) as:

#define buf_COUNT(b) ((b)->end - (b)->start)

Here you can possibly discourage use of the exported struct
definition via some relatively long prefix infested names. Now the
macro becomes:

#define buf_COUNT(b) b->nastyprefix_buffer_the_absolute_end \
- b->nastyprefix_buffer_the_raw_beginning

You can also provide an auxiliary function that exports some
selected offsets in the structure, which only need be called once
to get those values. After which the macro can operate with those
values. My nmalloc package is an example of this, which allows
clean connection of run-time debuggery and does not restrain
altering the main package. See:

<http://cbfalconer.home.att.net/download/>

The malldbg module uses the facility.
 
E

E. Robert Tisdale

bluejack said:
For as long as I've been using C, I've vacillated on the optimal
degree of encapsulation in my designs. At a minimum, I aggregate data
and code that operate on that data into classlike files; but now and
then I go on an opaque type joyride, and create minimalist header
files that define very clean interfaces.

The problem with that is that it prevents some optimizations:

* Can no longer allocate structures on the stack;
* Can no longer put inline functions in the header file;
* Incurs the overhead of a function call for any data-access.

I'm curious as to how C experts feel about this design concept;
reviewing some old (very old) threads about encapsulation, I see that
many dismiss it as though it were a tenet of some foolish philosophy;
and yet, encapsulation has all sorts of benefits to the author of
libraries in terms of the ability to extend, adapt, & refactor code;
it has also helped me in my own designs by illuminating design
mistakes before they get very far.
Take a look at The ANSI C Numerical Class Library

http://www.netwood.net/~edwin/svmtl/

Look at a header prototype file like

cncl/src/matrix/matrix.hP

You can use this mechanism to help hide the actual object definition
without using opaque data types.
But, unless you *really* need high performance
such as may be requiredfor a numerical class library,
opaque data types usually cost very little.
 
R

Richard Heathfield

Ben Pfaff said:

I prefer to use profiling as the indicator of when performance is
important.

Well, of course profiling /can't/ tell you when performance is
important. What it /can/ do is tell you what the performance /is/.
Whether it's within acceptable parameters is a decision we have to make
for ourselves; it is not something our profilers can decide on our
behalf.
 
B

Ben Pfaff

Richard Heathfield said:
Ben Pfaff said:

Well, of course profiling /can't/ tell you when performance is
important. What it /can/ do is tell you what the performance /is/.

Profiling can tell me whether the performance of any given
component is important to the overall performance of a system.
 
R

Richard Tobin

Ben Pfaff said:
Profiling can tell me whether the performance of any given
component is important to the overall performance of a system.

And often it gives a clue as to whether there is any worthwhile
optimisation to be done: if the time spent is widely spread out, it
may be hard to find any code that repays attention. On the other
hand, if you find that 90% of the time is in one unexpected function,
it's often easy to make a big improvement.

-- Richard
 

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

Forum statistics

Threads
473,769
Messages
2,569,582
Members
45,065
Latest member
OrderGreenAcreCBD

Latest Threads

Top