These are notes from a talk given to both the New York C++ SIG and the SFBA Center for Advanced Technology C++ Industrial Seminar Group, in 1995 and 1996.


Interface Design in the Standard C++ Library:
The Grand Challenge

by Nathan Myers
ncm-nospam@cantrip.org
http://www.cantrip.org/


Abstract


Overview


Standard C++ Status


Standard C++


The Standard C++ Library


Standard Library Goals

The Standard Library must be: (Non-goal: cfront-compatible)


The Standard Library Must Be General

Must support:


The Standard Library Must Be Efficient


The Standard Library Must Be Flexible

Support extensions & variations:


The Standard Library Must Be Exemplary


Non-goals


Interface Techniques

How do we achieve these goals?


Interface Techniques

Overview:


Requirements: Convenience

The overwhelming majority of library uses are of the simplest form:

Ordinary use must be so convenient that users can ignore the alternatives, e.g.

  string str;         // this...

  basic_string<char, char_traits<char>, allocator<char> >
         str(allocator());  // not this.


Requirements: Flexibility


Requirements: Backward Compatibility


Problem 1

iostream must allow alternate character types without changing the basic interface.

    template <class charT, ... >
      class basic_streambuf { ... };

    typedef basic_streambuf<char>    streambuf;
    typedef basic_streambuf<wchar_t> wstreambuf;

...but character types have extra semantics that iostream depends on:

  streambuf::sgetc()
must return the int value EOF.

Are int and EOF right for other character types?


Solution 1: "traits"

Define a separate, template class, specialized for the character type:

   template <class charT>
     struct char_traits { };  // empty

Specialize it for char, wchar_t, etc.

   struct char_traits<char> {
     typedef int int_type;
     static inline int_type eof() { return EOF; }
     ...
   };


Using "traits" (1)

Use the "traits" template in the streambuf:

template <class charT>
  class basic_streambuf {
   public:
    typedef charT char_type;
    typedef typename char_traits::int_type int_type;

    int_type sgetc(); // returns char_traits::eof() at end-of-file.
    ...
  };

Old code still works, as fast as before:

    while ((ch = sbuf.getc()) != EOF) { ... }


Using "traits" (2)

Greater flexibility is possible. Suppose we declare basic_streambuf so:

  template <class charT,
            class traits = char_traits<charT> >
    class basic_streambuf {
     public:
      typedef typename traits::int_type int_type;
      int_type sgetc();
      ...
    };

Now users can substitute some other EOF ...


Using "traits" (3)

For example, a control-Z?

 struct MyTraits : public char_traits<char> {
   static inline int_type eof() { return 26; }
 };

 typedef ifstream<char,MyTraits> MyIfstream;

 MyIfstream book("book.txt", ios::in);
 string line;
 while (getline(book, line)) { ... }

In a MyIfstream, control-Z is recognized as end-of-file.


An Aside: public typedefs

Notice the public typedefs in the template:

  template <class charT ,
            class traits = char_traits<charT> >
    class basic_streambuf {
     public:
      typedef charT char_type;     // <-- here
      typedef traits traits_type;  // <-- and here
      ...
    };

Always, always, always do this.


Numeric Traits

When writing numeric templates, you sometimes need the equivalent of FLT_MAX for the element type used:

   template <class Num>
     void double_if_possible(Num& val)
     {
       if (val < ???_MAX/2)
          val += val;
     }


Numeric Traits (2)

The standard header <limits> contains:

  template <class Num> numeric_limits {};

  template <> struct numeric_limits< float > {   // specialize
    static inline float max()
      { return FLT_MAX ; }
    ...
  };
  struct numeric_limits< double > {  // specialize
    static inline double max()
      { return DBL_MAX ; }
    ...
  };


Numeric Traits (3)

Now the function looks like:

  template <class Num>
     void double_if_possible(Num& val)
     {
       if (val < numeric_limits<Num>::max() /2)
         val += val;
     }


Problem #2: Type-safe Extensibility

Internationalization is open-ended:


Type-safe Extensibility (2)

Locale facilities encapsulate local preferences.


Type-safe Extensibility (3)

A locale is a collection of facets:


Type-safe Extensibility (4)

Template notation provides the interface:

  const Facet & use_facet<Facet>(const locale&);

Given a locale loc and a facet supporting, e.g., string comparison: collate<char>::compare( ... ), call it as:

  use_facet< collate<char> >(loc).compare( ... );

Similar to a cast: use_facet throws an exception if the facet is not present in the argument locale object.


An Aside: Use with Defaults

Users see locale as a unit, pass it along to functions that use it; usually only class designers look at the facets of a locale.

The default for normal operations is the global locale, locale(), so users need mention a locale only if they have one:

  string f(const locale& = locale());
  ...
  f();   // use the global locale, locale()
  f(loc) // use a private locale, instead.


An Aside: Use with Defaults (2)

Example: a date class Date:

  class Date {
   public:
    string asString(
      const locale& use = locale());
    ...
  };


An Aside: Use with Defaults (3)

To convert a Date to a string using (as in the common case) the default global locale:

    Date today = Date::now();
    string s = today.asString();

To convert using a specific locale, e.g. french:

    locale french("fr_FR");
    Date today = Date::now();
    string s = today.asString(french);


Another aside: imbue

Passing locale objects around a program adds clutter. Also:


Another aside: imbue (2)

Sometimes an object can "hitchhike" on an existing argument, particularly a long- lived argument like a stream.

Standard iostream provides a member :

    locale ios::imbue(const locale&)


Another aside: imbue (3)

With imbue(), locales can be carried along in the iostream object to affect operations far down the call chain, without cluttering the interface:

   Date today = Date::now();
   cout << today;  // use global locale
   cout.imbue(locale("fr_FR"));
   cout << today;  // use french locale


Another aside: imbue (4)

The operator<< on Date retrieves the imbued locale from the stream and uses it to do formatting.

Imbuing (hitchhiking) is a good way to help keep interfaces simple and general.


Pause...

In all the examples thus far, notice a common theme:


Problem: Memory Models

Programs often need to place objects in very odd places:


Problem: Memory Models (2)

We would like to use the same objects regardless of where they are.


Solution: Memory Models

The standard library encapsulates memory models in class interfaces called allocators.


Use of Allocators

Standard Collection templates take a defaulted Allocator parameter:

  template <class T, class Allocator = allocator<T> >
    class list {
     public:
      typedef T element_type;
      typedef Allocator allocator_type;
      explicit list(
        Allocator& = Allocator());
      ...
    };


Use of Allocators (2)

Ordinary uses of collections can ignore allocators:

   new list<string>

Specialized uses can substitute another regime:

   typedef basic_string< ...  >
     HugeString;
   new(HugeAlloc())
     list<HugeString,HugeAlloc>


Use of Allocators (3)

Memory models that require a choice at runtime are passed to the constructor, too:

  typedef basic_string< ...  > OString;
  OdbAlloc parts("parts.odb")
  new(parts)
    list<Ostring,OdbAlloc>(parts)

List elements come from the specified Object Database storage allocator object.


Allocators

NOTE:

Standard C++ Library allocators are not the same as those in current commercial and public-domain STL implementations. Don't depend on that allocator interface.


In Conclusion...


Return to The Cantrip Corpus. Send email: ncm-nospam@cantrip.org
Copyright ©1997 by Nathan Myers. All Rights Reserved. URL: <http://www.cantrip.org/stdlibif.html>