Specialization - Abstraction Mechanisms - The C++ Programming Language (2013)

The C++ Programming Language (2013)

Part III: Abstraction Mechanisms

25. Specialization

It ain’t what you don’t know that gets you into trouble.

It’s what you know for sure that just ain’t so.

– Mark Twain

Introduction

Template Parameters and Arguments

Types as Arguments; Values as Arguments; Operations as Arguments; Templates as Arguments; Default Template Arguments

Specialization

Interface Specialization; The Primary Template; Order of Specialization; Function Template Specialization

Advice

25.1. Introduction

Over the last two decades, templates have developed from a relatively simple idea to the backbone of most advanced C++ programming. In particular, templates are key to techniques for

• improving type safety (e.g., by eliminating the use of casts; §12.5);

• raising the general level of abstraction of programs (e.g., by using standard containers and algorithms; §4.4, §4.5, §7.4.3, Chapter 31, Chapter 32); and

• providing more flexible, type-safe, and efficient parameterization of types and algorithms (§25.2.3).

These techniques all critically rely on the ability of template code to use template arguments without overhead and in a type-safe manner. Most techniques also rely on the type deduction mechanisms offered by templates (sometimes called compile-time polymorphism; §27.2). These techniques are the backbone of C++ use in performance-critical areas, such as high-performance numerical computing and embedded systems programming. For mature examples, see the standard library (Part IV).

This chapter and the following two present simple examples of the advanced and/or specialized language features supporting techniques aimed at uncompromised flexibility and performance. Many of these techniques are primarily developed for and used by library implementers. Like most programmers, I prefer to forget about the more advanced techniques most of the time. Where I can, I keep my code simple and rely on libraries so that I can benefit from the use of advanced features in the hands of experts in a given application domain.

Templates are introduced in §3.4. This chapter is part of a sequence presenting templates and their uses:

Chapter 23 gives a more detailed introduction to templates.

Chapter 24 discusses generic programming, the most common use of templates.

Chapter 25 (this chapter) shows how to specialize a template with a set of arguments.

Chapter 26 focuses on template implementation issues related to name binding.

Chapter 27 discusses the relation between templates and class hierarchies.

Chapter 28 focuses on templates as a language for generating classes and functions.

Chapter 29 presents a larger example of template-based programming techniques.

25.2. Template Parameters and Arguments

A template can take parameters:

Type parameters of “type type”

Value parameters of built-in types such as ints (§25.2.2) and pointers to functions (§25.2.3)

Template parameters of “type template” (§25.2.4)

Type parameters are by far the most common, but value parameters are essential for many important techniques (§25.2.2, §28.3).

A template can take a fixed number of parameters or a variable number. The discussion of variadic templates is postponed until §28.6.

Note that it is common to use short names with initial uppercase letters as names of template type arguments, for example, T, C, Cont, and Ptr. This is acceptable because such names tend to be conventional and restricted to a relatively small scope (§6.3.3). However, when usingALL_CAPS, there is always a chance of clashing with macros (§12.6), so don’t use names that are long enough to clash with likely macro names.

25.2.1. Types as Arguments

A template argument is defined to be a type parameter by prefixing it with typename or class. The result of using either is completely equivalent. Every type (built-in or user-defined) is syntactically acceptable to a template declared to take a type parameter. For example:

template<typename T>
void f(T);

template<typename T>
class X {
//
...
};
f(1); // T deduced to be int
f<double>(1); // T is double
f<complex<double>>(1); // T is complex<double>

X<double> x1; // T is double
X<complex<double>> x2; // T is complex<double>

A type argument is unconstrained; that is, there is nothing in the interface of a class that constrains it to be a certain kind of type or part of a class hierarchy. The validity of an argument type depends exclusively on its use in the templates, providing a form of duck typing (§24.1). You can implement general constraints as concepts (§24.3).

User-defined and built-in types are handled equivalently when used as template arguments. This is essential for allowing us to define templates that work identically for user-defined and builtin types. For example:

vector<double> x1; // vector of doubles
vector<complex<double>> x2; // vector of complex<double>

In particular, there is no space or time overhead implied by using either compared to the other:

• Values of built-in types are not “boxed” into special container objects.

• Values of all types are retrieved directly from a vector without use of potentially expensive (e.g., virtual) “get() functions.”

• Values of user-defined types are not implicitly accessed through references.

To be used as a template argument, a type must be in scope and accessible. For example:

class X {
class M { /*
... */ };
//
...
void mf();
};

void f()
{
struct S { /*
... */ };
vector<S> vs; //
OK
vector<X::M> vm; // error: X::M is private
// ...
}

void M::mf()
{
vector<S> vs; //
error: no S in scope
vector<M> vm; // OK
// ...
};

25.2.2. Values as Arguments

A template parameter that is not a type or a template is called a value parameter and an argument passed to it a value argument. For example, integer arguments come in handy for supplying sizes and limits:

template<typename T, int max>
class Buffer {
T v[max];
public:
Buffer() { }
//
...
};

Buffer<char,128> cbuf;
Buffer<int,5000> ibuf;
Buffer<Record,8> rbuf;

Simple and constrained containers such as Buffer can be important where run-time efficiency and compactness are paramount. They avoid the use of free store implied by the use of a more general string or vector while not suffering from the implicit conversions to a pointer like a built-in array (§7.4). The standard-library array34.2.1) implements this idea.

An argument for a template value parameter can be (§iso.14.3.2):

• An integral constant expression (§10.4)

• A pointer or a reference to an object or a function with external linkage (§15.2)

• A nonoverloaded pointer to member (§20.6)

• A null pointer (§7.2.2)

A pointer used as a template argument must be of the form &of, where of is the name of an object or a function, or of the form f, where f is the name of a function. A pointer to member must be of the form &X::of, where of is the name of a member. In particular, a string literal is notacceptable as a template argument:

template<typename T, char* label>
class X {
//
...
};

X<int,"BMW323Ci"> x1; // error: string literal as template argument
char lx2[] = "BMW323Ci";
X<int,lx2> x2; // OK: lx2 has external linkage

This restriction, like the one against floating-point template value arguments, exists to simplify implementation of separately compiled translation units. It is best to think of template value arguments as a mechanism for passing integers and pointers to functions. Resist the temptation to try something more clever. Unfortunately (for no fundamental reason), literal types (§10.4.3) cannot be used as template value parameters. The value template arguments are the mechanism for some more advanced compile-time computation techniques (Chapter 28).

An integer template argument must be a constant. For example:

constexpr int max = 200;

void f(int i)
{
Buffer<int,i> bx; //
error: constant expression expected
Buffer<int,max> bm; // OK: constant expression
// ...
}

Conversely, a value template parameter is a constant within the template so that an attempt to change the value of a parameter is an error. For example:

template<typename T, int max>
class Buffer {
T v[max];
public:
Buffer(int i) { max = i; } //
error: attempt to assign to template value argument
// ...
};

A type template parameter can be used as a type later in a template parameter list. For example:

template<typename T, T default_value>
class Vec {
//
...
};

Vec<int,42> c1;
Vec<string,""> c2;

This becomes particularly useful when combined with a default template argument (§25.2.5); for example:

template<typename T, T default_value = T{}>
class Vec {
//
...
};

Vec<int,42> c1;
Vec<int> c11; //
default_value is int{}, that is, 0
Vec<string,"fortytwo"> c2;
Vec<string> c22; //
default_value is string{}; that is, ""

25.2.3. Operations as Arguments

Consider a slightly simplified version of the standard-library map31.4.3):

template<typename Key, Class V>
class map {
//
...
};

How do we supply comparison criteria for Keys?

• We can’t hardwire a comparison criterion into the container because the container can’t (in general) impose its needs on the element types. For example, by default, the map uses < for comparison, but not all Keys have a < that we would want to use.

• We can’t hardwire an ordering criterion into the Key type because (in general) there are many different ways of ordering elements based on a key. For example, one of the most common Key types is string and strings can be ordered based on a variety of criteria (e.g., case sensitive and case insensitive).

Consequently, a sorting criterion is not built into the container type or into the element type. In principle, the notion of sorting criteria for a map could be represented as:

[1] A template value argument (e.g., a pointer to a comparison function)

[2] A template type argument to the map template determining the type of a comparison object

At first glance, the first solution (pass a comparison object of a specific type) seems simpler. For example:

template<typename Key, typename V, bool(*cmp)(const Key&, const Key&)>
class map {
public:
map();
//
...
};

This map requires a user to supply the comparison as a function:

bool insensitive(const string& x, const string& y)
{
//
compare case insensitive (e.g., "hello" equals "HellO")
}

map<string,int,insensitive> m; // compare using insensitive()

However, this is not very flexible. In particular, the designer of map will have to decide whether to compare the (unknown) Key type using a pointer to function or a function object of some specific type. Also, because the argument types of the comparison operator must depend on the Keytype, it can be hard to provide a default comparison criterion.

Consequently, the second alternative (pass the type of the comparison as a template type parameter) is the more common and the one used in the standard library. For example:

template<typename Key, Class V, typename Compare = std::less<Key>>
class map {
public:
map() { /*
... */ } // use the default comparison
map(Compare c) :cmp{c} { /* ... */ } // override the default
// ...
Compare cmp {}; // default comparison
};

The most common case, comparing using less-than, is the default. If we want a different comparison criterion, we can supply it as a function object (§3.4.3):

map<string,int> m1; // use the default comparison (less<string>)

map<string,int,std::greater<string>> m2; // compare using greater<string>()

Function objects can carry state. For example:

Complex_compare f3 {"French",3}; // make a comparison object (§25.2.5)
map<string,int,Complex_compare> m3 {f3}; // compare using f3()

We can also use pointers to functions, including lambdas that can convert to a pointer to function (§11.4.5). For example:

using Cmp = bool(*)(const string&,const string&);
map<string,int,Cmp> m4 {insensitive}; //
compare using a pointer to function

map<string,int,Cmp> m4 {[](const string& a, const string b) { return a>b; } };

Passing the comparison operations as a function object has significant benefits compared to passing pointers to functions:

• A simple class member function defined in-class is trivial to inline, whereas inlining a call through a pointer to function requires exceptional attention from a compiler.

• A function object with no data members can be passed with no run-time cost.

• Several operations can be passed as a single object with no additional run-time cost.

The comparison criterion for a map is just an example. However, the technique used to pass it is general and very widely used to parameterize classes and functions with “policies.” Examples include actions for algorithms (§4.5.4, §32.4), allocators for containers (§31.4, §34.4), and deleters for unique_ptr34.3.1). We have the same design alternatives when we need to specify arguments for a function template, such as sort(), and the standard library chooses alternative [2] for those cases also (e.g., see §32.4).

If we had only one use of a comparison criterion in our program, it might make sense to use a lambda to express the function object version a bit more tersely:

map<string,int,Cmp> c3 {[](const string& x, const string& y) const { return x<y; }}; // error

Unfortunately, that doesn’t work because there is no conversion of a lambda to a function object type. We could name the lambda and then use that name:

auto cmp = [](const string& x, const string& y) const { return x<y; }
map<string,int,decltype(cmp)> c4 {cmp};

I find naming operations useful from a design and maintenance point of view. Also, anything named and declared nonlocally might find other uses.

25.2.4. Templates as Arguments

Sometimes it is useful to pass templates – rather than classes or values – as template arguments. For example:

template<typename T, template<typename>class C>
class Xrefd {
C<T> mems;
C<T*> refs;
//
...
};

template<typename T>
using My_vec = vector<T>; //
use default allocator

Xrefd<Entry,My_vec> x1; // store cross references for Entrys in a vector

template<typename T>
class My_container {

// ...
};

Xrefd<Record,My_container> x2; // store cross references for Records in a My_container

To declare a template as a template parameter, we must specify its required arguments. For example, we specify that Xrefd’s template parameter C is a template class that takes a single type argument. If we didn’t, we wouldn’t be able to use specializations of C. The point of using a template as a template parameter is usually that we want to instantiate it with a variety of argument types (such as T and T* in the previous example). That is, we want to express the member declarations of a template in terms of another template, but we want that other template to be a parameter so that it can be specified by users.

Only class templates can be template arguments.

The common case in which a template needs only a container or two is often better handled by passing the container types (§31.5.1). For example:

template<typename C, typename C2>
class Xrefd2 {
C mems;
C2 refs;
//
...
};

Xrefd2<vector<Entry>,set<Entry*>> x;

Here, the value types of C and C2 can be obtained by a simple type function (§28.2) for obtaining the type of elements of a container, for example, Value_type<C>. This is the technique used for the standard-library container adaptors, such as queue31.5.2).

25.2.5. Default Template Arguments

Explicitly specifying the comparison criterion for each use of map is tedious – especially as less<Key> is typically the best choice. We can specify less<Key> to be the default type for the Compare template argument, so that only uncommon comparison criteria have to be explicitly specified:

template<typename Key, Class V, typename Compare = std::less<Key>>
class map {
public:
explicit map(const Compare& comp ={});
//
...
};

map<string,int> m1; // will use less<string> for comparisons
map<string,int,less<string>> m2; // same type as m1

struct No_case {
//
define operator()() to do case-insensitive string comparison
};

map<string,int,No_case> m3; // m3 is of a different type from m1 and m2

Note how the default map constructor creates a default comparison object, Compare{}. That’s the common case. If we want a more elaborate construction, we must do so explicitly. For example:

map<string,int,Complex_compare> m {Complex_compare{"French",3}};

The semantic checking of a default argument for a template parameter is done only if that default argument is actually used. In particular, as long as we refrain from using the default template argument less<Key>, we can compare() values of a type X for which less<X> wouldn’t compile. This point is crucial in the design of the standard containers (e.g., std::map), which rely on a template argument to specify default values (§31.4).

Just as for default function arguments (§12.2.5), the default template arguments can be specified and supplied for trailing arguments only:

void f1(int x = 0, int y); // error: default argument not trailing
void f2(int x = 0, int y = 1); // OK

f2(,2); // syntax error
f2(2); // call f(2,1);

template<typename T1 = int, typename T2>
class X1 { //
error: default argument not trailing
// ...
};

template<typename T1 = int, typename T2 = double>
class X2 { //
OK
// ...
};

X2<,float> v1; // syntax error
X2<float> v2; // v2 is an X2<float,double>

Not allowing an “empty” argument to mean “use the default” was a deliberate tradeoff between flexibility and the opportunity for obscure errors.

The technique of supplying a policy through a template argument and then defaulting that argument to supply the most common policy is almost universal in the standard library (e.g., §32.4). Curiously enough, it is not used for basic_string23.2, Chapter 36) comparisons. Instead, the standard-library string relies on char_traits36.2.2). Similarly, the standard algorithms rely on iterator_traits33.1.3) and the standard-library containers rely on allocators34.4). The use of traits is presented in §28.2.4.

25.2.5.1. Default Function Template Arguments

Naturally, default template arguments can also be useful for function templates. For example:

template<typename Target =string, typename Source =string>
Target to(Source arg) //
convert Source to Target
{
stringstream interpreter;
Target result;

if (!(interpreter << arg) //
write arg into stream
|| !(interpreter >> result) // read result from stream
|| !(interpreter >> std::ws).eof()) // stuff left in stream?
throw runtime_error{"to<>() failed"};

return result;
}

A function template argument needs to be explicitly mentioned only if it cannot be deduced or if there is no default, so we can write:

auto x1 = to<string,double>(1.2); // very explicit (and verbose)
auto x2 = to<string>(1.2); // Source is deduced to double
auto x3 = to<>(1.2); // Target is defaulted to string; Source is deduced to double
auto x4 = to(1.2); // the <> is redundant

If all function template arguments are defaulted, the <> can be left out (exactly as in function template specializations; §25.3.4.1).

This implementation of to() is a bit heavyweight for combinations of simple types, such as to<double>(int), but improved implementations can be supplied as specializations (§25.3). Note that to<char>(int) will not work because char and int do not share a string representation. For conversion among scalar numeric types, I tend to prefer narrow_cast<>()11.5).

25.3. Specialization

By default, a template gives a single definition to be used for every template argument (or combination of template arguments) that a user can think of. This doesn’t always make sense for someone writing a template. I might want to say, “If the template argument is a pointer, use this implementation; if it is not, use that implementation,” or “Give an error unless the template argument is a pointer derived from class My_base.” Many such design concerns can be addressed by providing alternative definitions of the template and having the compiler choose between them based on the template arguments provided where they are used. Such alternative definitions of a template are called user-defined specializations, or simply user specializations. Consider likely uses of a Vector:

template<typename T>
class Vector { //
general vector type
T* v;
int sz;
public:
Vector();
explicit Vector(int);

T& elem(int i) { return v[i]; }
T& operator[](int i);

void swap(Vector&);
//
...
};

Vector<int> vi;
Vector<Shape*> vps;
Vector<string> vs;
Vector<char*> vpc;
Vector<Node*> vpn;

In such code, most Vectors will be Vectors of some pointer type. There are several reasons for this, but the primary reason is that to preserve run-time polymorphic behavior, we must use pointers (§3.2.2, §20.3.2). That is, anyone who practices object-oriented programming and uses type-safe containers (such as the standard-library containers) will end up with a lot of containers of pointers.

The default behavior of most C++ implementations is to replicate the code for template functions. This is usually good for run-time performance, but unless care is taken, it leads to code bloat in critical cases such as the Vector example.

Fortunately, there is an obvious solution. Containers of pointers can share a single implementation. This can be expressed through specialization. First, we define a version (a specialization) of Vector for pointers to void:

template<>
class Vector<void*>{ //
complete specialization
void** p;
//
...
void*& operator[](int i);
};

This specialization can then be used as the common implementation for all Vectors of pointers. Another use would be to implement unique_ptr<T> based on a single shared implementation class storing a void*.

The template<> prefix says that this is a specialization that can be specified without a template parameter. The template arguments for which the specialization is to be used are specified in <> brackets after the name. That is, the <void*> says that this definition is to be used as the implementation of every Vector for which T is void*.

The Vector<void*> is a complete specialization. That is, there is no template parameter to specify or deduce when we use the specialization; Vector<void*> is used for Vectors declared like this:

Vector<void*> vpv;

To define a specialization that is used for every Vector of pointers and only for Vectors of pointers, we can write:

template<typename T>
class Vector<T*> : private Vector<void*>{ //
partial specialization
public:
using Base = Vector<void*>;

Vector() {}
explicit Vector(int i) : Base(i) {}

T*& elem(int i) { return reinterpret_cast<T*&>(Base::elem(i)); }
T*& operator[](int i) { return reinterpret_cast<T*&>(Base::operator[](i));}

//
...
};

The specialization pattern <T*> after the name says that this specialization is to be used for every pointer type; that is, this definition is to be used for every Vector with a template argument that can be expressed as T*. For example:

Vector<Shape*> vps; // <T*> is <Shape*> so T is Shape
Vector<int**> vppi; // <T*> is <int**> so T is int*

A specialization with a pattern containing a template parameter is called a partial specialization in contrast to complete specializations (as in the definition of vector<void*>), where “the pattern” is simply a specific type.

Note that when a partial specialization is used, a template parameter is deduced from the specialization pattern; the template parameter is not simply the actual template argument. In particular, for Vector<Shape*>, T is Shape and not Shape*.

Given this partial specialization of Vector, we have a shared implementation for all Vectors of pointers. The Vector<T*> class is simply an interface to Vector<void*> implemented exclusively through derivation and inline expansion.

It is important that this refinement of the implementation of Vector be achieved without affecting the interface presented to users. Specialization is a way of specifying alternative implementations for different uses of a common interface. Naturally, we could have given the general Vectorand the Vector of pointers different names. However, when I tried that, many people who should have known better forgot to use the pointer classes and found their code much larger than expected. In this case, it is much better to hide the crucial implementation details behind a common interface.

This technique proved successful in curbing code bloat in real use. People who do not use a technique like this (in C++ or in other languages with similar facilities for type parameterization) have found that replicated code can cost megabytes of code space even in moderately sized programs. By eliminating the time needed to compile those additional versions of the Vector operations, this technique can also cut compile and link times dramatically. Using a single specialization to implement all lists of pointers is an example of the general technique of minimizing code bloat by maximizing the amount of shared code.

Some compilers are getting smart enough to perform this particular optimization without help from the programmer, but the technique is generally applicable and useful.

Variants of the technique of using a single run-time representation for values of a number of types and relying on the (static) type system to ensure that they are used only according to their declared type has been called type erasure. In the context of C++, it was first documented in the original template paper [Stroustrup,1988].

25.3.1. Interface Specialization

Sometimes, a specialization is not an algorithmic optimization, but a modification of an interface (or even a representation). For example, the standard library complex uses specializations to adjust the set of constructors and the argument types for important operations for important specializations (such as complex<float> and complex<double>). The general (primary) template (§25.3.1.1) looks like this:

template<typename T>
class complex {
public:
complex(const T& re = T{}, const T& im = T{});
complex(const complex&); //
copyconstructor
template<typename X>
complex(const complex<X>&); //
conversion from complex<X> to complex<T>

complex& operator=(const complex&);
complex<T>& operator=(const T&);
complex<T>& operator+=(const T&);
//
...
template<typename X>
complex<T>& operator=(const complex<X>&);
template<typename X>
complex<T>& operator+=(const complex<X>&);
//
...
};

Note that the scalar assignment operators take reference arguments. That’s not efficient for floats, so complex<float> passes those by value:

template<>
class complex<float> {
public:
//
...
complex<float>& operator= (float);
complex<float>& operator+=(float);
//
...
complex<float>& operator=(const complex<float>&);
//
...
};

For complex<double>, that same optimization applies. In addition, conversions from complex<float> and complex<long double> are provided (as described in §23.4.6):

template<>
class complex<double> {
public:
constexpr complex(double re = 0.0, double im = 0.0);
constexpr complex(const complex<float>&);
explicit constexpr complex(const complex<long double>&);
//
...
};

Note that these specialized constructors are constexpr, making complex<double> a literal type. We could not do that for the general complex<T>. Also, this definition takes advantage of the knowledge that conversion from complex<float> to complex<double> is safe (it never narrows), so that we can have an implicit constructor from complex<float>. However, the constructor from complex<long double> is explicit to make narrowing less likely.

25.3.1.1. Implementation Specialization

Specialization can be used to provide alternative implementations of a class template for a specific set of template parameters. In that case, a specialization can even provide a representation that differs from that of the general template. For example:

template<typename T, int N>
class Matrix; //
N-dimensional Matrix of Ts

template<typename T,0>
class Matrix { //
specialization for N==1
T val;
//
...
};

template<typename T,1>
class Matrix { //
specialization for N=1
T* elem;
int sz; //
number of elements
// ...
};

template<typename T,2>
class Matrix { //
specialization for N=2
T* elem;
int dim1; //
number of rows
int dim2; // number of columns
// ...
};

25.3.2. The Primary Template

When we have both a general definition of a template and specializations defining implementations for specific sets of template arguments, we refer to the most general template as the primary template. The primary template defines the interface for all specializations (§iso.14.5.5). That is, the primary template is the one used to determine if a use is valid and takes part in overload resolution. Only after a primary template has been chosen are specializations considered.

The primary template must be declared before any specialization. For example:

template<typename T>
class List<T*>{
//
...
};

template<typename T>
class List { //
error: primary template after specialization
// ...
};

The critical information supplied by the primary template is the set of template parameters that the user must supply to use it or any of its specializations. If we have defined a constraints check for a template (§24.4), the primary template is where it belongs because concepts are something a user cares about and must understand to use the template. For example:

template<typename T>
class List {
static_assert(Regular<T>(),"List<T>: T must be Regular");
//
...
};

For technical reasons (because the language doesn’t recognize constraints checks for what they are), a constraints check needs to be replicated in every specialization.

A declaration of the primary template is sufficient to allow the definition of a specialization:

template<typename T>
class List; //
not a definition

template<typename T>
class List<T*>{
//
...
};

If used, the primary template must be defined somewhere (§23.7). If the primary template is never instantiated, it need not be defined. This can be used to define a template for which only a fixed set of alternative arguments are accepted. If a user specializes a template, that specialization must be in scope for every use of the template with the type for which it was specialized. For example:

template<typename T>
class List {
//
...
};

List<int*> li;

template<typename T>
class List<T*>{ //
error: specialization used before defined
// ...
};

Here, List was specialized for int* after List<int*> had been used.

It is essential that every use of a template for a given set of template arguments be implemented by the same specialization. If not, the type system is broken, so that identical uses of a template in different places may yield different results and objects created in different parts of a program may not be compatible. Clearly that would be disastrous, so a programmer must take care that explicit specialization is consistent throughout a program. In principle, implementations are capable of detecting inconsistent specialization, but the standard does not require them to and some don’t.

All specializations of a template must be declared in the same namespace as the primary template. If used, a specialization that is explicitly declared (as opposed to generated from a more general template) must also be explicitly defined somewhere (§23.7). In other words, explicitly specializing a template implies that no (other) definition is generated for that specialization.

25.3.3. Order of Specialization

One specialization is more specialized than another if every argument list that matches its specialization pattern also matches the other, but not vice versa. For example:

template<typename T>
class Vector; //
general; the primary template
template<typename T>
class Vector<T*>; //
specialized for any pointer
template<>
class Vector<void*>; //
specialized for void*

Every type can be used as a template argument for the most general Vector, but only pointers can be used for Vector<T*> and only void*s can be used for Vector<void*>.

The most specialized version will be preferred over the others in declarations of objects, pointers, etc. (§25.3).

A specialization pattern can be specified in terms of types composed using the constructs allowed for template parameter deduction (§23.5.2).

25.3.4. Function Template Specialization

Specialization is also useful for template functions (§25.2.5.1). However, we can overload functions, so we see less specialization. Furthermore, C++ supports only complete specialization for functions (§iso.14.7), so we use overloading where we might have tried partial specialization.

25.3.4.1. Specialization and Overloading

Consider the Shell sort from §12.5 and §23.5. Those versions compare elements using < and swap elements using detailed code. A better definition would be:

template<typename T>
bool less(T a, T b)
{
return a<b;
}

template<typename T>
void sort(Vector<T>& v)
{
const size_t n = v.size();

for (int gap=n/2; 0<gap; gap/=2)
for (int i=gap; i!=n; ++i)
for (int j=i–gap; 0<=j; j–=gap)
if (less(v[j+gap],v[j]))
swap(v[j],v[j+gap]);
}

This does not improve the algorithm itself, but it allows improvements to its implementation. We now have less and swap as named entities for which we can provide improved versions. Such names are often referred to as customization points.

As written, sort() will not sort a Vector<char; > correctly because < will compare the two char*s. That is, it will compare the addresses of the first char in each string. Instead, we would like it to compare the characters pointed to. A simple specialization of less() for const char* will take care of that:

template<>
bool less<const char*>(const char* a, const char* b)
{
return strcmp(a,b)<0;
}

As for classes (§25.3), the template<> prefix says that this is a specialization that can be specified without a template parameter. The <const char*> after the template function name means that this specialization is to be used in cases where the template argument is const char*. Because the template argument can be deduced from the function argument list, we need not specify it explicitly. So, we can simplify the definition of the specialization:

template<>
bool less<>(const char* a, const char* b)
{
return strcmp(a,b)<0;
}

Given the template<> prefix, the second empty <> is redundant, so we would typically simply write:

template<>
bool less(const char* a, const char* b)
{
return strcmp(a,b)<0;
}

I prefer this shorter form of declaration. We can go further still. With this last version the distinction between specialization and overloading has become razor thin and largely irrelevant, so we can simply write:

bool less(const char* a, const char* b)
{
return strcmp(a,b)<0;
}

Now that we have “specialized” less() to a version that is semantically correct, we can consider what we might do for swap(). The standard-library swap() is correct for our use and has already been optimized for every type that has efficient move operations. Therefore, when we used swap()instead of the three potentially expensive copy operations, we improved performance for a large number of argument types.

Specialization comes in handy when an irregularity of an argument type causes the general algorithm to give an undesired result (such as less() for C-style strings). These “irregular types” are often the built-in pointer and array types.

25.3.4.2. Specialization That Is Not Overloading

How does a specialization differ from overloading? From a technical point of view, they differ because individual functions take part in overloading whereas only the primary template takes part in specialization (§25.3.1.1). However, I can’t think of an example where that makes a practical difference.

There are a few uses of function specializations. For example, we can select among functions taking no arguments:

template<typename T> T max_value(); // no definition

template<> constexpr int max_value<int>() { return INT_MAX; }
template<> constexpr char max_value<char>() { return CHAR_MAX; }
//
...

template<typename Iter>
Iter my_algo(Iter p)
{
auto x = max_value<Value_type<Iter>>(); //
works for types with specialized max_value()
// ...
}

I used the type function Value_type<> to get the type of the object pointed to by an Iter24.4.2).

To get a roughly equivalent effect with overloading, we would have to pass a dummy (unused) argument. For example:

int max2(int) { return INT_MAX; }
char max2(char) { return INT_MAX; }


template<typename Iter>
Iter my_algo2(Iter p)
{
auto x = max2(Value_type<Iter>{}); //
works for the types for which we overload max2()
// ...
}

25.4. Advice

[1] Use templates to improve type safety; §25.1.

[2] Use templates to raise the level of abstraction of code; §25.1.

[3] Use templates to provide flexible and efficient parameterization of types and algorithms; §25.1.

[4] Remember that value template arguments must be compile-time constants; §25.2.2.

[5] Use function objects as type arguments to parameterize types and algorithms with “policies”; §25.2.3.

[6] Use default template arguments to provide simple notation for simple uses; §25.2.5.

[7] Specialize templates for irregular types (such as arrays); §25.3.

[8] Specialize templates to optimize for important cases; §25.3.

[9] Define the primary template before any specialization; §25.3.1.1.

[10] A specialization must be in scope for every use; §25.3.1.1.