Previous section   Next section

Imperfect C++ Practical Solutions for Real-Life Programming
By Matthew Wilson
Table of Contents
Chapter 20.  Shims


20.4. Control Shims

Definition: Control Shims

Control shims define operations that are applied to the instances of the types for which they are defined.

The names of control shims take the form of an imperative element coupled with the particular attribute or state being modified. Examples would be make_empty, dump_contents.


Control shims are less widely used than attribute and logical shims, because generalization of operations is less ubiquitous than the accessing of attributes or states. (We saw a pair of control shims in section 6.2 when we talked about state scoping.) Control shims are not only used for designing flexible policy-based general components; they are also exceedingly useful tools for assisting in maintenance, especially in porting code.

Consider having some nontrivial code that makes extensive use of instances of one or more container types. Say it's the source for a Linux PC game. You then have a requirement to port it to an embedded game platform. As such you want to change the container type(s) used, and make use of a highly optimized in-house container library, specifically written for the new platform. Unfortunately, the container library was written a long time before the Standard Template Library (STL) Sequence and Associative Container concepts [Aust1999, Muss2001] became a widely accepted form, and so does not conform. What do you do?

Well, you could insist that the container library be rewritten to conform to the STL. Regrettably, the original authors long since left the company, and although they left a very comprehensive test suite[5] that gives you confidence in the quality of the container library, no one in the company has much expertise in the internals of this library. The changes would be nontrivial, deadlines are tight, and you cannot justify the risk of wholesale changes.

[5] I know how realistic this is, but hey, I'm having a fantasy here.

Alternatively, you could populate the source with #ifdefs, specializing client-code use of the container libraries (remember we're now conceptually dealing with two) via the preprocessor. This is a workable solution in the short term, but as an experienced cross-platform developer, you know this is just setting yourself up for more grief down the line.

The classic advice in these scenarios is to abstract all platform-specific code into separate implementation files, and use link-time selection to keep the messiness to a minimum. While this is great for truly platform-specific areas—file systems, memory mapping, synchronization objects—in some cases it is hopelessly naïve. In this case it is a bad idea because:

  • You don't have time. This is the real world and you're playing catch-up with marketing.

  • The code is extremely well meshed with the existing container class library.

  • The speed advantage of the embedded container library relies in large measure on the use of inline functions, intrinsics, and inline assembler. Adding function call overhead would reduce speed and increase size, and you can afford neither.

Given these choices, most of us would go for the second option, and build in all the #ifdefs under the sun. The product would ship on time, but would become a maintainer's nightmare.[6] As soon as the developer who handled the first port to the new platform left the company, moved to another project, or perhaps even just got a full night's sleep, it would be impossible to enhance without adding bugs to one or the other (or both) implementations. Never mind adding another target platform!

[6] In my first job in the commercial sector I worked on a code-base—used to implement ISDN basic rate servers and clients and primary-rate servers—that had 13-way conditional compilation for three processor architectures, four operating systems, and three physical guises. I witnessed changes being implemented on up to 20 different branches at any one time! Anything you can ever imagine about source management problems, I've seen it. Never again!

So what's the answer? Shims, of course: attribute shims to get hold of elements; logical shims to ascertain container and element states; control shims to change the contents of the containers. make_empty() to clear the container of its elements; insert_element() to insert an element; remove_element() to remove an element; get_nth_element() to access an element by index.[7]

[7] Be aware that shims are not the be all and end all. See section 10.11 for a note of realism/caution.

You can have a single code base, with perhaps only a single preprocessor conditional to typedef the container types, and you write and use shims that are highly efficient and are inlined with no cost by the compiler. The code barely has to change, and does so in ways that are either meaning positive or neutral at worst—make_empty(cont); is a lot more obvious to a non-STL developer than cont.erase(cont.begin(), cont.end());—such that you can test with all existing test cases on the original platform, knowing that you have effected no semantic changes whatsoever. When it comes time to add new container types for your next platform, you can do so without changing a character of the valuable and fragile application code.


      Previous section   Next section