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.
by Nathan Myers
ncm-nospam@cantrip.org
http://www.cantrip.org/
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.
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?
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; }
...
};
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) { ... }
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 ...
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.
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.
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;
}
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 ; }
...
};
Now the function looks like:
template <class Num>
void double_if_possible(Num& val)
{
if (val < numeric_limits<Num>::max() /2)
val += val;
}
Internationalization is open-ended:
Locale facilities encapsulate local preferences.
A locale is a collection of facets:
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.
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.
Example: a date class Date:
class Date {
public:
string asString(
const locale& use = locale());
...
};
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);
Passing locale objects around a program adds clutter. Also:
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&)
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
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:
Programs often need to place objects in very odd places:
We would like to use the same objects regardless of where they are.
The standard library encapsulates memory models in class interfaces called 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());
...
};
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>
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.
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.