If you find yourself building a C++ API or SDK for use by third
party developers, a whole host of concerns will enter into the equation
that are not part of building a usual C++ application. If you ship the
source for everything, and the developer builds everyting into one
large, statically linked application, your code looks a lot like the
application code, and most of these concerns go away, but if you want
to ship pre-built libraries, or even DLLs, then here are some rules to
live by.
I've found that making your data members public in interfaces is
very seldom a good idea. Instead, I've found that it helps structure
code to divide things into three kinds:
- Real components.
These are made by calling factories or global
factory functions. You talk to them through interfaces, that are
pure virtual abstract base classes. There exists one or more
implementation of each of these interfaces. The interfaces may
accept "client" interfaces, which is how you describe yourself
to the component. IVertexBuffer is a component of this kind.
Crucial is that "operator==()" works on the pointer values, not
the pointed-at class data, i e you don't define your own, and
always reference these guys through pointers (or references had
by de-referencing pointers).
Typically, these interfaces have protected destructors, and use
reference counting, or a separate "dispose()" method. The constructor
is always protected (and usually inline, empty), because you only
make instances of derived classes.
- Value classes.
These have "operator=()" and possibly public members,
but no virtual functions. They are structs with smarts. They do
not use dynamically allocated memory or complex members. Matrix,
Vector and FileName are of this kind (if FileName is a
fixed-size name with internal data storage).
The underlying data a value class works on MAY be something like
a handle returned by another API, that internally uses dynamic
memory allocation (HWND, say :-); they key is that the object
itself does not.
Crucial is that "operator==()" works on the pointed-at data.
Often, you'll want to implement these inline. If there's a lot of
code to some member functions, you can out-of-line them.
- Helper classes.
These are something like value classes, but are
often implemented as templates. They can use dynamically allocated
memory. std::list<>, std::string<>,
and CComPtr<> all are of this kind.
Because these objects use memory management, there may be a mis-
match if part of the object is in one linkage unit (DLL) and part
is in another, so you have to never pass one of these guys to or
from a function or interface that lives in another linkage unit.
This means that the user is free to use these, but you should not
yourself take these as arguments (i e, no std::string const &
for name arguments!)
A corollary is that Helper classes should not define virtual functions.
The temptation to put objects of kind 3 (helper classes) into DLLs may
at times be strong. The problem is that, if you override global operator
new (for example), that will not override the operator new used by the
DLL on Windows, so you end up with mis-matched allocations. The Microsoft
STL for Visual C++ version 6.0 had a library called MSVCPRT.DLL
which had exactly this problem, and it was a pain to work with!
UNIX dynamic linkage works somewhat differently, in that one operator
new will "win" in case there are multiple available at load time; the
symbol tables are effectively merged. The problem with that is that you
can't know, when building your code, what operator new will actually be
used for memory allocation at run time! In fact, if you use dlopen() to
open plug-ins, and they dynamically link against other shared libraries,
it may even change at run-time!
If you are not scared by this, then that shows you just haven't been bitten
yet. You will -- so try to use these simple design rules in your C++ API,
to make it sting less.
I'm currently busy constructing my third large C++ API, over the course
of ten years, so these lessons are the fruit of hard-earned experience :-)
|