Article from Traits: a new and useful template technique

It should be a very old (1995) article, but it is very suitable as a material for getting started with Template.

The ANSI/ISO C++ standard library wanted to support internationalization from the very beginning. Although the specific details were not well understood at the beginning, it has gradually become a bit more prominent in the past five years. The conclusion now is that templates should be used to parameterize tools that require character manipulation.

Parameterizing existing iostream and string types is actually quite difficult and requires inventing a new technique. Fortunately, this technique can be used well elsewhere.

question

In iostream, streambuf needs an EOF to complete its function. In general libraries, EOF is just an int; some functions that need to read characters only return int values.

 class streambuf {
    ...
    int sgetc();  // return the next character, or EOF.
    int sgetn(char*, int N);  // get N characters.
  };

But what happens if the streambuf is parameterized with a character type? Generally speaking, the program does not need this type, but the value of EOF of this type. Try it first:

 template <class charT, class intT>
  class basic_streambuf {
    ...
    intT sgetc();
    int sgetn(charT*, int N);
  };

This extra intT is a bit annoying. The user of the library does not care what the so-called EOF is. This is not the only question, what should sgetc return when it encounters EOF? Could it be another template parameter? It seems this way doesn't work.

Traits Techniques

At this time, it is necessary to introduce Traits . With traits, you don't need to take parameters directly from the original template, but define a new template. Users generally do not use this new template directly, so the name of this template can be humanized.

 template <class charT>
  struct ios_char_traits { };

The default traits are an empty class. For real character types, this template can be specialized to provide meaningful semantics.

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

ios_char_traits<char> has no data members, only some definitions. Let's see how streambuf is defined.

 template <class charT>
  class basic_streambuf {
    public:
      typedef ios_char_traits<charT> traits_type;
      typedef traits_type::int_type int_type;
      int_type eof() { return traits_type::eof(); }
      ...
      int_type sgetc();
      int sgetn(charT*, int N);
  };

Except for the typedef, this is very similar to the original definition. There is now 1 template parameter, and it is the only template parameter that the user needs to care about. The compiler will look for the information it needs from this trait class. Except for some variable declarations that need to be adjusted, the user's usage code does not look much different from before.

If streambuf uses another character type, just re-specialize ios_char_traits. To support wchar_t you can write:

 struct ios_char_traits<wchar_t> {
    typedef wchar_t char_type;
    typedef wint_t  int_type;
    static inline int_type eof() { return WEOF; }
  };

The string class can be parameterized in the same way.

Applicable scenarios of this technique: 1. Template parameterization of primitive types; 2. Customization of classes that cannot be changed

another example

Before going any further, let's look at another example (from the ANSI/ISO C++ [Draft] Standard).

There is a numerical computing library that uses the types float, double and long double, and each type has an associated "epsilon", exponent, and base. These values are defined in <float.h>, but some tools in the library don't know whether to use FLT_MAX_EXP or DBL_MAX_EXP instead. This problem can be solved cleanly with traits technology.

 template <class numT>
  struct float_traits { };

  struct float_traits<float> {
    typedef float float_type;
    enum { max_exponent = FLT_MAX_EXP };
    static inline float_type epsilon() { return FLT_EPSILON; }
    ...
  };

  struct float_traits<double> {
    typedef double float_type;
    enum { max_exponent = DBL_MAX_EXP };
    static inline float_type epsilon() { return DBL_EPSILON; }
    ...
  };

Now you can get max_exponent directly without knowing the specific type (float/double/long double). Give an example of a matrix

 template <class numT>
  class matrix {
    public:
      typedef numT num_type;
      typedef float_traits<num_type> traits_type;
      inline num_type epsilon() { return traits_type::epsilon(); }
      ...
  };

In the examples so far, each template parameter has a set of public typedefs, and the classes that use these parameters are strongly dependent on these typedefs. This is no accident: in most cases, traits that are passed as arguments must provide public typedefs in order to be properly instantiated using the templates of these traits.

One thing to learn: Be sure to provide these public typedefs.

Default template parameters

As of 1993, compilers supported the above usage. After November 1993, a better solution emerged: default template parameters could be specified. While many compilers already support values as default template parameters, the new solution goes one step further and allows types as default template parameters.

Stroustrup's Design and Evolution of C++ (page 359) has an example. First define a trait: CMP, and a simple parameterized string.

 template <class T> class CMP {
    static bool eq(T a, T b) { return a == b; }
    static bool lt(T a, T b) { return a < b; }
  };
  template <class charT> class basic_string;

At this point, you can customize the compare operation for the string:

 template <class charT, class C = CMP<charT> >
  int compare(const basic_string<charT>&,
              const basic_string<charT>&);

The specific implementation details are not discussed here, but the second template parameter needs attention. First of all, this C is not just a class, but an instantiated class. Second, the second template parameter (C) itself requires parameters, and the required parameter is the first template parameter (charT) of compare. This is not possible in function declarations, but it is possible in template declarations.

This approach allows the user to customize the comparison process. Take a look at applying this technique to our own streambuf template:

 template <class charT, class traits = ios_char_traits<charT> >
  class basic_streambuf {
    public:
      typedef traits traits_type;
      typedef traits_type::int_type int_type;
      int_type eof() { return traits_type::eof(); }
      ...
      int_type sgetc();
      int sgetn(charT*, int N);
  };

This gives us the opportunity to customize traits for a specific char. This is important to users of the library, because EOF may be different in different character set mappings.

Traits at runtime

Going a step further, look at the constructor of streambuf:

 template <class charT, class traits = ios_char_traits<charT> >
  class basic_streambuf {
      traits traits_;  // member data
      ...
    public:
      basic_streambuf(const traits& b = traits())
        : traits_(b) { ... }

      int_type eof() { return traits_.eof(); }
  };

Now our traits also work at runtime, not just compile time. In this example, traits_.eof() may be a static function, or an ordinary member function. If it is an ordinary member function, eof() may use some parameters when constructing traits. (This technique has practical usage scenarios, such as the allocator that all containers in the standard library have)

It is worth noting that for the user, there is no change now, and the default parameters can meet most of the usage needs. But when there are special needs of their own, the current template definition can also provide the opportunity to modify. In any case, we will generate optimal code, and if there are no additional costs, we will not introduce these additional costs!

Summarize

As long as the compiler supports templates, the traits skills can be used directly.

Traits can associate related types, values, functions, etc. with template parameters without introducing too much noise. This language feature (default template parameters) greatly expands the language capabilities and provides enough flexibility without compromising performance.

refer to

  • Stroustrup, B. Design and Evolution of C++, Addison-Wesley, Reading, MA, 1993.
  • Barton, JJ, and LR Nackman, Scientific and Engineering C++, Addison-Wesley, Reading, MA, 1994.
  • Veldhuizen, T. "Expression Templates", C++ Report, June 1995, SIGS Publications, New York.

ivkus
12 声望7 粉丝