Previous section   Next section

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


18.2. What's in a Definition?

The use of typedef is ubiquitous throughout C++ (and C) code. However, little attention is drawn to the distinct uses of type definitions, so let's do that now by defining two typedef-related concepts.

18.2.1 Conceptual Type Definitions

Let's consider a class used in implementing IP-based communications. We might see something such as the following:



// Socket.h


typedef int   AddressFamily;


typedef int   SocketType;


typedef int   Protocol;





class Socket


{


public:


  Socket(AddressFamily family, SocketType type, Protocol protocol);


  . . .



Here we see the definition of three typedefs—AddressFamily, SocketType, and Protocol—which identify the three defining characteristics required for the creation of a socket [Stev1998]. In this context, typedef has been used to define three distinct logical types, albeit they are, in this case, the same actual type: int. Hence:

Definition: Conceptual type definitions define logically distinct types.


Clearly the Socket constructor is more self-documenting than if the three parameters were just defined as int. One advantage of conceptual type definitions is that they provide this (modest) degree of self-documentation.

Another advantage to conceptual type definitions is that they provide a degree of platform independence. Consider the standard types size_t and ptrdiff_t, which are usually defined as:



typedef unsigned int  size_t;


typedef int           ptrdiff_t;



When you see ptrdiff_t in code, you are immediately cognizant that something here is going to be evaluating pointer differences. The same mental highlight is not achieved by just seeing int, which more commonly makes one think of basic arithmetic or indexing operations. Similarly, size_t inclines one to think about number of bytes, as opposed to an arbitrary integral measure. However, such typedefs serve an additional, and very important, function. Because an (unsigned) int may not be the appropriate type to represent a number of bytes or a pointer difference on a given platform, an implementation may redefine these types as appropriate for its supported platforms and there will be no need for user code to be changed. This is exactly how the fixed-sized integers of C99 are made portable.

By the way, both C and C++ support multiple definitions of the same typedef, so long as all definitions are identical. However, it is not generally good form to redefine any typedef and you should avoid doing so. There should be a single point of definition, usually within a shared included file, rather than having independent definitions, which can lead to very unpleasant side effects. If divergent definitions eventuate in different compilation units that are linked together, especially when that linking is dynamic, things can get grim, as we saw in Chapter 9. Don't do it!

18.2.2 Contextual Type Definitions

Where conceptual type definitions define concepts independent of context (for a given platform), contextual type definitions do precisely the opposite: they (re-)define a well-known[1] concept in multiple contexts.

[1] Well-known insofar as constructs external to the definition context expect such definitions to exist and to represent a known taxonomy of type and/or behavior.

Anyone who's had even a sniff of C++ programming since 1998 should be aware of the Standard Template Library[2] (STL), and will probably have seen code similar to that shown in Listing 18.4.

[2] Actually it's now officially just part of the C++ standard library, but everyone still refers to it as the STL.

Listing 18.4.


template< typename C


        , typename F


        >


F for_all_postinc(C &c, F f)


{


  typename C::iterator b = c.begin();


  typename C::iterator e = c.end();


  for(; b != e; b++)


  {


    f(*b);


  }


  return f;


}



This algorithm applies a caller-supplied function f to all the elements within the container c defined by the asymmetric range [Aust1999]: [c.begin(), c.end()). The iterators returned from the begin() and end() methods are stored in the variables b and e, and the range is traversed by applying the postincrement operator to b.[3] The important thing to note in this algorithm is the type of the iterator variables. They are declared as being of type C::iterator. This means the algorithm can be applied to any type C that defines this type. iterator is thus a member type, as shown in Listing 18.5.

[3] This is a test function I use when writing STL components to ensure that postincrement semantics, which are less efficient (and therefore less frequently used) and harder to emulate, are valid for supporting iterator types.

Listing 18.5.


class String


{


public:


  typedef char *iterator; // 'iterator' is a simple pointer


  . . .





namespace std


{


  class list_iterator;


  class list


  {


  public:


    typedef list_iterator iterator; // 'iterator' is an external class


    . . .



All the standard library containers (including std::vector, std::deque, std:: string) supply the iterator member type,[4] and there are a whole host of third-party library components that similarly support this requirement. For all these types, the iterator member represents a promise to the external context—in this case the for_all_postinc() algorithm but potentially any code that needs to define iteration variables—of certain attributes and behavior.

[4] In fact this is one of the requirements of the STL Container concept [Aust1999, Muss2001].

The actual type which the iterator typedef aliases may vary enormously between its host containers: in some it will be a pointer, in others a complex class type providing an appropriate set of operations. However, each actual type conforms to a known concept—the Iterator concept [Aust1999] in this case—and it is the member type that provides the expected type (and its implied behaviors) to the external context. Indeed, member types may even be nested classes (or enums, though that would not work here in this case) and not typedefs at all.



class environment_variable_sequence


{


public:


  class iterator // 'iterator' is a nested class


  {


  . . .



Hence, iterator is a contextual type definition because it (re-)defines a well-known concept in several contexts.

Definition: Contextual type definitions define types that correspond to well-known concepts, relative to specific contexts. Such types act as descriptions of the nature (type and/or behavior) of their host contexts.


Contextual type definitions are not limited to being provided in template classes or to being used by template algorithms. Also, they are not necessarily members of classes. They can also be members of namespaces or local definitions within functions or even within individual blocks.

As well as being a mechanism that supports generic programming (via the type lookup mechanism we've just seen), they can also be a great aid in portability. Consider a situation where you're implementing a library to enumerate the contents of directories within the host file-system. On a platform where the constant PATH_MAX is defined, you may assume that all paths are bounded to the length given by its value.[5] You might, therefore, decide to implement your library for your fixed path-length platform, within the fsearch::fixed_platform namespace, using a fixed string class, as in:

[5] On UNIX platforms that do not define PATH_MAX you must call pathconf()to get the path limit at runtime. The implementation of the UNIXSTL basic_file_path_buffer<> class, included on the CD, illustrates how this compile-time/runtime evaluation may be abstracted.



namespace fsearch


{


  namespace fixed_platform


  {


    typedef acme_lib::basic_fixed_string<char, PATH_MAX>  string_t;


    class directory_sequence


    {


      . . .


      bool get_next(string_t &entry);



In this case string_t is a contextual typedef for the namespace fsearch:: fixed_platform. Client code may look something like the following:



using fsearch::fixed_platform::string_t;


using fsearch::fixed_platform::directory_sequence;





int main()


{


  directory_sequence ds("/usr/include");


  string_t          entry;


  while(ds.get_next(entry))


  {


    puts(entry.c_str());


  }


  . . .



Of course, our directory utility becomes so useful that we want to port it to other platforms. Another platform, BigOS, can have paths of any length, so using a fixed string is not appropriate. (Note that I'm electing to take this particular porting strategy to illustrate the contextual type definition concept; in real life there are several ways to implement cross-platform APIs, and choosing between them is a nontrivial balancing of many factors that is outside the scope of this book.[6]) The BigOS namespace might look something like this:

[6] Maybe if you get all your friends to buy this one we can persuade the publisher to commission a book on porting.



namespace fsearch


{


  namespace bigos_platform


  {


    typedef std::string   string_t;


    class directory_sequence


    {


      . . .


      bool get_next(string_t &entry);



The advantage now is that because both operating-system variants of the fsearch library have logically equivalent interfaces, the changes to client code are exceedingly minimal:



#ifdef __BIGOS__


 namespace fsearch_platform = fsearch::bigos_platform;


#elif defined(__unix__)


 namespace fsearch_platform = fsearch::fixed_platform;


#elif . . .


 . . .


#endif /* operating system */





using fsearch_platform::string_t;


using fsearch_platform::directory_sequence;





int main()


{


  . . .



The discrimination of the platform may well be placed in a library header, which means that client code can be entirely independent of the platform (excepting representational differences between the file systems, of course).


      Previous section   Next section