Previous section   Next section

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


18.4. True Typedefs

If we look at typedefs as a transfer of type from one type name to another, we can see that we want to allow two-way transfer for contextual typedefs, but only a one-way transfer for conceptual type definitions. What we would like would be to have two distinct keywords to represent this concept. I would suggest that the alias keyword would represent the two-way transfer and would behave as typedef currently does. Hence:



alias int IntType;





int     i1;


IntType i2;





i1 = i2; // int and IntType are fully interchangeable, and


i2 = i1; // they are in fact the same type



The typedef would be the one-way transfer. Hence:



typedef int IntType;





int     i1;


IntType i2;





i1 = i2; // Invalid! Cannot convert IntType to int


i2 = i1; // Invalid! Cannot convert int to IntType



But this is the real world: typedef carries the former meaning, and there is no way we will be able to foist our idealization on 5 million developers and billions of lines of code.[7] So, this being a book about proactively addressing C++'s foibles, what can we do about it?

[7] Actually, the new language D [Brig2002] uses exactly these definitions for these two keywords and, therefore, provides conceptual type definitions as a built-in feature of the language.

In the spirit of the imperfect practitioner, I applied tenet #4—Never give up!—and after many attempts around the problem arrived at the solution, in the form of the true_typedef template class (see Listing 18.6).

Listing 18.6.


template< typename T


        , typename U>


class true_typedef


{


public:


  typedef T       &reference;


  typedef T const &const_reference;


// Construction


public:


  true_typedef()


    : m_value(T())


  {}


  explicit true_typedef(T const &value)


    : m_value(value)


  {}


  true_typedef(true_typedef const &rhs)


    : m_value(rhs.m_value)


  {}


  true_typedef const &operator =(true_typedef const &rhs)


  {


    m_value = rhs.m_value;


    return *this;


  }


// Accessors


public:


  const_reference base_type_value() const


  {


    return m_value;


  }


  reference base_type_value()


  {


    return m_value;


  }


// Members


private:


  T  m_value;


// Not to be implemented


private:


  // Not provided, as the syntax is less ambiguous when


  // assignment from an explicit temporary is made


  true_typedef const &operator =(T const &value);


};



As you can see, it's pretty simple. It contains a single member of the primary parameterizing type T, which is called the base type. It is also parameterized by an unused type U, which is the unique type. The unique type ensures that the instantiated types are unique, as we can see in the new definitions of our socket API types:



acmelib_gen_opaque(AddressFamily_u)


acmelib_gen_opaque(SocketType_u)


acmelib_gen_opaque(Protocol_u)





typedef true_typedef< int


                    , AddressFamily_u>   AddressFamily;


typedef true_typedef< int


                    , SocketType_u>      SocketType;


typedef true_typedef< int


                    , Protocol_u>        Protocol;



The opaque type generator macro acmelib_gen_opaque()—a unique type generator (see section 7.4.4)—is used to define a unique type. By convention, I apply the _u postfix.

Now when we attempt to construct a Socket with family and type parameters in the wrong order, the compiler informs us that they parameters are incompatible, and we can fix the bug immediately.

We can now also overload on logical type, irrespective of any commonality of the underlying types, so the Network class can be correctly defined as it was intended.

You're probably asking how easy True Typedefs are to use. By looking at the template definition given earlier, it is clear that to access the value we need to call base_type_value(). This can't be good, can it? Thankfully we can adopt some of Scott Meyer's magic, patented cure-all free-function snake oil [Meye2000] and make things a great deal simpler. The true_typedef header file contains (as of the time of writing) 73 template free functions such as those shown in Listing 18.7.

Listing 18.7.



template< typename T, typename U>


true_typedef<T, U> const operator ++(true_typedef<T, U> &v, int)


{


  true_typedef<T, U>  r(v);


  v.base_type_value()++;


  return r;


}





template< typename T, typename U>


bool operator <=( true_typedef<T, U> const &lhs, T const &rhs)


{


    return lhs.base_type_value() <= rhs;


}





template< typename T, typename U>


true_typedef<T, U> operator ~(true_typedef<T, U> const &v)


{


    return true_typedef<T, U>(~v.base_type_value());


}





template< typename T, typename U>


true_typedef<T, U> const &operator <<=( true_typedef<T, U> &v


                                , true_typedef<T, U> const &rhs)


{


    v.base_type_value() <<= rhs.base_type_value();


    return v;


}



This means that instances of True Typedefs that are based on fundamental types can be used in almost all expressions that their fundamental types can. For example,



true_typedef<int, . . .>  i1 = 1000;


int                       i2 = 1001;


true_typedef<int, . . .>  i3 = i2;





i1 <<= 2;


i1 = ~i3;


i3 = i2 + i3;



Naturally this is not possible for class types, unless they have those operators defined, but access to the base type value is pretty straightforward, via base_type_value(). And it's just as valid to take a reference to the base type value, as it would be to take a reference to an instance of that type. What is important is that the types are implicitly treated differently.

Both const and non-const access is provided. It is valid to include modifying operations, since the intention behind True Typedefs is to create strongly typed types and not strongly valued types (e.g., enums).

True Typedefs can also be used for underlying types other than the fundamental types, including class types.



typedef true_typedef<std::string, . . .> Forename;


typedef true_typedef<std::string, . . .> Surname;





bool lookup_programmer( Forename const &fn


                      , Surname const  &sn


                      , int           &iq);





Forename  fn("Archie");


Surname   sn("Goodwin");


int       iq;





fn = sn; // Error – types are different





if(lookup_programmer(sn, fn, iq)) // Error: fn and sn reversed


{


  printf("%s %s: %d\n", sn.base_type_value().c_str()


                      , sn.base_type_value().c_str()


                      , iq);


  // and would be disappointing if it did work ...


}



The True Typedefs concept completely answers the problems of weak conceptual type definitions: It prevents types with identical base types from being implicitly interconvertible, and it facilitates overloading of logically distinct types. Furthermore, it does this without sacrificing any efficiency, since the implementation is light, and all methods are inline. (I've not yet found a compiler that generates any differences in performance between True Typedef code and the plain typedef equivalents. This is so even for nontrivial base types.)

Let's now have a recap of the problem of implicit integer conversion in the serialization component in section 13.2.1. Using True Typedefs, we can rewrite the Serializer class, as shown in Listing 18.8.

Listing 18.8.


// serialdefs.h


acmelib_gen_opaque(sint8_u)


acmelib_gen_opaque(uint8_u)


acmelib_gen_opaque(sint16_u)


acmelib_gen_opaque(uint16_u)


acmelib_gen_opaque(sint32_u)


acmelib_gen_opaque(uint32_u)


acmelib_gen_opaque(sint64_u)


acmelib_gen_opaque(uint64_u)


typedef true_typedef<int8_t, sint8_u>     sint8_type;


typedef true_typedef<uint8_t, uint8_u>    uint8_type;


typedef true_typedef<int16_t, sint16_u>   sint16_type;


typedef true_typedef<uint16_t, uint16_u>  uint16_type;


typedef true_typedef<int32_t, sint32_u>   sint32_type;


typedef true_typedef<uint32_t, uint32_u>  uint32_type;


typedef true_typedef<int64_t, sint64_u>   sint64_type;


typedef true_typedef<uint64_t, uint64_u>  uint64_type;





// Serializer.h


class Serializer


{


  . . .


// Operations


public:


  void Write(sint8_type i);


  void Write(uint8_type i);


  void Write(sint16_type i);


  void Write(uint16_type i);


  void Write(sint32_type i);


  void Write(uint32_type i);


  void Write(sint64_type i);


  void Write(uint64_type i);


  // No need to define any other (proscribed) methods


  . . .


};





void fn()


{


  Serializer    s;


  sint8_type    i8(0);    // Must use initialisation syntax ...


  uint64_type   ui64(0);  // ... rather than assignment syntax.


  int           i    = 0;





  s.Write(si8);


  s.Write(ui64);


  s.Write(i);  // Error, plain and simple – no ambiguity


  s.Write(0);  // ERROR: Ambiguous call





  uint64_t      ui = ui64.base_type_value(); // Must use method


}



Now we have 100 percent type enforcement. Admittedly using the integer true typedefs is a bit inconvenient—in that you have to use initialization syntax to construct one from a base type variable or literal, and base_type_value() if you need to convert back to the base type—but is a small price to pay. And when you're working with cross-platform serializing types, a bit of explicit rigor is often helpful, both when writing and when reading/maintaining the serialization code.


      Previous section   Next section