Discussion
Team LiB
Previous Section Next Section

Discussion

The more widely distributed your library, and the less control you have over the build environment of all of its clients, the fewer the types that the library can reliably use in its external interface. Interfacing across modules involves binary data exchange. Alas, C++ doesn't specify standard binary interfaces; widely distributed libraries in particular might need to rely on built-in types like int and char to interface with the outer world. Even compiling the same type using different build options on the same compiler can cause binary-incompatible versions of the type.

Typically, either you control the compiler and options used to build the module and all its clients, and you can use any typeor you don't, and you can use only platform-provided types and C++ built-in types (even then, document the size and representation you expect for the latter). In particular, never mention standard library types in the interface of a module unless all other modules that use it will be compiled at the same time and with the same standard library source image.

There is a tradeoff between the problems of using types that can't be correctly understood by all clients, and the problems of using a low level of abstraction. Abstraction is important; if some clients understand only low-level types, and you must therefore use those, consider also supplying alternate operations that use higher-level types. Consider a SummarizeFile function that takes the file to be processed as a parameter. There are three common options for the parameter: It can be a char* that points to a C-style string containing the file's name; a string that containers the file's name; or an istream or a custom File object. Each of these choices is a tradeoff:

  • Option 1: char*. Clearly the char* type is accessible to the widest audience of clients. Unfortunately, it is also the lowest-level option; in particular, it is less robust (e.g., the caller and callee must explicitly decide who allocates the memory and who deallocates it), more open to errors (e.g., the file might not exist), and less secure (e.g., to classic buffer overrun attacks).

  • Option 2: string. The string type is accessible to the more restricted audience of clients that are written in C++ and compiled using the same standard library implementation, the same compiler, and compatible compiler settings. In exchange, it is more robust (e.g., callers and callees can be less explicit about memory management; but see Item 60) and more secure (e.g., string grows its buffer as needed, and is not inherently as susceptible to buffer overrun attacks). But this option is still relatively low-level, and thus open to errors that have to be checked for explicitly (e.g., the file might not exist).

  • Option 3: istream or File. If you're going to jump to class types anyway, thereby requiring clients to be written in C++ using the same compiler and switches, use a strong abstraction: An istream (or custom File object that wraps istream to avoid a direct dependency on one standard library implementation) raises the level of abstraction and makes the API much more robust. The function knows that it's getting a File or a suitable input stream, does not need to manage memory for string filenames, and is immune to many accidental and deliberate errors possible with the other options. Few checks remain: The File must be open, and the contents must be in the right format, but that's about all that can go wrong.

Even when you choose to use a lower-level abstraction in a module's external interface, always use the highest level of abstraction internally and translate to the lower-level abstraction at the module's boundary. For example, if you will have non-C++ clients, you might use opaque void* or int handles to client code, but still use objects internally, and cast only at the module's interface to translate between the two.

    Team LiB
    Previous Section Next Section