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



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


Interface Techniques

How do we achieve these goals?

Interface Techniques


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:

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 {
    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 {
      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 {
      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 {
    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 << 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.


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 {
      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< ...  >

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")

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



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:
Copyright ©1997 by Nathan Myers. All Rights Reserved. URL: <>