Skip to content

Chapter 6: Templates

6.1

A template is a class or a function that we parameterize with a set of types or values. We use templates to represents ideas that are best understaoood as something genral from which we can generate specific types and functions by specifying arguments, such as the vectors element type double

6.2

The template<typename T> prefix makes T a parameter of the declaration it prefixes. If you want the mathematical "for all T, such that P(T)", you need concept. Using class to introduce a type parameter is equivalent to using typename, and in older code we often see template<class T> as the prefix.

To support the range-for loop for our Vector, we must define suitable begin() and end().

A template plus a set of template arguemnts is called an instantiation or a specialization. Late in the compilation process, at instantiation time, code is generated for each instantiation used in a program.

6.2.1

C++20 introduced "constrained template arguments"

Most often, a template will make sense only for template arguments that meet certain criteria. For example, a Vector typically offers a copy operation, and if it does, it must require that its elements must be copable. That is , we must require that Vector's template argument is not just a typename, but an Element where "Element" specifies the requirements of a type that can be an element:

template<Element T>
class Vector {
private:
    T* elem;
    int sz;
};

This template<Element T> prefix is C++'s version of methematic's "for all T such taht Element(T)"; that is, Element is a predicate that checks whether T has all the properties that a Vector requires. Such a predicate is called a concept. A template argument for which a concept is a specified is called a constrained argument and a template for which an argument is constrained is called a constrained template*

6.2.2

a template can also take value arguments.

template<typename T int N>
struct Buffer{
    using value_type = T;
    constexpr int size() { return N; }
    T[N] data;
};

The alias value_type and the constexpr function are prodived to allow users (read-only) access to the template arguments. Here Buffer allows us to create arbitrarily sized buffers with no use of the free store (dynamic memory)

6.2.3

Type deduction

std::pair p = {1.5, 2};  // p is a std::pair<double, int>

Deduction can cause surprised (both for make_ functions and constructors)

vector<string> vs1{"Hello", "World"}; // vector<string>
vector vs {"Hello", "World"}; // deduces to vector<const char*>
vector vs2 {"Hello"s, "World"s}; // deduces to vector<string>
vector vs3 {"Hello"s, "World"}; // compiler error: the initializer list si not homogenous
When a template argument cannot be deduced from the constructor argument, we can help by providing a deduction guide.

template<typename T>
class vector2 {
    using value_type = T;
    vector2(initializer_list<T>);

    template<typename Iter>
    vector2(Iter b, Iter e);
};

// deduction guide
template<typename Iter>
vector2(Iter, Iter) -> vector2(typename iter::value_type);

When without language support for concepts, the compiler cannot assume anything about that type. To allow deduction, we can add a deduction guide after the declaration of vector2. That is, if we see a vector2 initialized by a pair of iterators, we should deduce vector2::value_type to be the iterator's value type.

6.3

There are three ways of expressing an operation parameterized by types or values; * a function template * a function object: an object that can carry data and can be called like a function * A lambda expression: a shorthand notation for a function object

6.3.1

template<typename Sequence, typename Value>
Value sum(const Sequence&s Value v){
    for (auto x: s) v+=x;
    return v;
}

std::vector<int> vi;
std::vector<double> vd;
int x = sum(vi, 0); // Sequence:std::vector<int>, Value:int
double d = sum(vi, 0.0); // Sequence:std::vector<int>, Value:double
// no need to write double d = sum<std::vector<int>, double>(vi, 0.0)

the point of adding ints in a double would be to gracefully handle a number larger than the largest int. We do not need to explicitly specify those types.

A function template can be a member function, but not a virtual member. The compiler would not know all instantiations of such a template in a program, so it could not generate a vtbl

6.3.2

One particularly useful kind of template is the function object (in c++, sometimes it is called functor.)

template<typename T>
class Less_than{
    const T val; // value to compare against
public:
    Less_than(const T& v) : val(v) {}
    bool operator()(const T& x) const { return x< val; }
};

This is useful because this functor can be a predicate, such as

void f(const Vector<int>& vec, const list<string>& lst, int x, const string&s){
    cout << count(vec, Less_than(x)); // Less_than is used as a predicate
}

Here, Less_than{x} constructs an object of type Less_than<int>, for which the call operator compares to the int called x; Also, for a simple function object like Less_than, inlining is simple, so a call of Less_than is far more efficient than an indirect function call.

Function objects used to specify the meannig of key operations of a general algorithm (such as Less_than for count()) are often referred to as policy objects.

6.3.3

count(list, [&](const string& a){ return a<s;});

[&](const string& a){ return a < s;} is called a lambda expression. It generates a function object exaclty like Less_than<int>{x}. The [&] is a capture list specifying that all local names used in the lambda body will be accessed through references. [&x] only capture x by refernce, [=x] only capture x by copy. Capture nothing is [], and capture all by reference is [&], whereas capture all by copy is [=]

template<typename C, typename Oper>
void for_all(C& c, Oper op)
    // requires Sequence<C> && Callable<OPer, Value_type<C>>
{
    for (auto& x: c) op(x);
}
void user(){
    vector<unique_ptr<Shape>> v;
    while (cin) v.push_back(read_shape(cin));
    for_all(v, [](unique_ptr<Shape>& ps){ ps->draw();});
    for_all(v, [](unique_ptr<Shape>& ps){ ps->rotate(45); });
}

Here passing unique_ptr<Shape>& to a lambda so that for_all() doesn't have to care exactly how the objects are stored. In particular, those for_all() calls do not affect the lifetime of the Shapes passed and the bodies of the lambdas use the argument just as if they have been plain-old pointers

For a lambda defineid within a member function, [this] captures the current object by reference so that we can refer class members. If we want a copy of the current object, we say [*this]

generic lambda: when needed, we can constrain the parameter with a concept. (see the example below Pointer_to_class, which can be defined to require * and ->)

template<class S>
void rotate_and_draw(vector<S>& v, int r){
    // here `r` stays in the local scope inside the function.
    // here `auto` is a generic lambda.
    for_all(v, [](auto& s){ s->rotate(r); s->draw(); });
}

for_each(v, [](Pointer_to_class auto* s){ s->rotate(r); s->draw(); });

Using a lambda, we can turn any statement into an expression. For example, in initialization, instead of speading statements around, we can group them into a single lambda function

// use lambda to turn statements into an expression:
vector<int> v = [&]{
    switch (m){
        case zero:
            return vector<int>(n);
        case seq:
            return vector<int>{p, q};
        case cpy:
            return arg
    }
}();  // define a lambda and immediately invoke it

7.3.3.3 Finally from 3rd edition

Destructors offer a general and implicit mechanism for cleaning up after use of an object (RAII), but what if we need to do some cleanup that is not associated with a single object, or with an object that does not have a destructor? We can define a function, finally that takes an action to be executed on the exit from the scope.

void old_style(int n)
{
    void* p = malloc(n * sizeof(int));
    auto act = fnially([&]{free(p); });
}

This is better than manually handle free(p) later. The finally() function is trivial

template<class F>
[[nodiscard]] auto finally(F f)
{
    return Final_action{f};
}

template<class F>
struct Final_action{
    explicit Final_action(F f): act(f) {}
    ~Final_action() { act(); }
    F act;
};

Here the attribute [[nodiscard]] is used to ensure that users do not forget to copy a generated Final_action into the scope for which its action is intended. There is a finally() in the Core GuideLine Support Library(the GSL) and a proposal for a more elaborate scope_exit mechanism for the standard library.

6.4

To define good templates, ew need some supporting language facilities

  • Values depende on a type: variable templates
  • Aliases for types and templates: alias templates
  • A Compile-time selection mechanism: if constexpr
  • A Compile-time mechanism to inquire about properties of types and expressiones: requires-expression

6.4.1

variable templates: when we use a type, we often want constants and values of that type. This is of course also the case when we use a class template: when we define a C<T>, we often want constants and variables of type T and other types depending on T.

template<class T>
constexpr T viscosity = 0.4;  // constant under T

template<class T>
constexpr space_vector<T> external_acceleration = {T{}, T{-9.8}, T{}}; // value under T

auto vis2 = 2 * viscosity<dboule>;
auto acc = external_acceleration<float>;

template<typename T, typename T2>
constexpr bool Assignable = is_assignable<T&, T2>::value; // this will becomes concept definitions.

template<typename T>
void testing{
    static_assert(Assignable<T&, double>, "can't assign");
}

Curiously enough, most variable templates seem to be constants. But then, so are many variables. Terminology hasn't kept up with our notion of immutability.

6.4.2

it is very common for a parameterized type to provide an alias for types related to their template arguments

template<typename T>
class Vector {
    using value_type=T;
};
In fact, every standard library container provides value_type as the name of its value type. This allows us to

template<typename C>
using Value_type = typename C::value_type

template<typename Container>
void algo(Container& c){
    Vector<Value_type<Container>> vec;  // this extracts the value type of the container via using alias.
}

// can also be written as
template<typename Container>
void algo(Container& c){
    Vector<typename Container::iterator> vec;  // this extracts the value type of the container via using alias.
}

The typename is needed in Vector<typename Container::iterator> to inform the compiler that Container's iterator is supposed to be a type and not a value of some type.

This mechanism can also be used for partial binding

template<typename Key, typename Value>
class Map {

};
template<typename Value>
using String_map = Map<string, Value>;

String_map<int> m;  // m is a Map<string, int>

6.4.3

Consider writing an operation that can use one of two operations slow_and_safe(T) or simple_and_fast(T). The traditional solution is to write a pair of overloaded functions and slect the most appropriate based on a trait, such as the standard-library is_pod.

template<typename T>
void update(T& target){
    //...
    if constexpr(is_pod<T>::value)
        simple_and_fast(target)
    else
        slow_and_safe(target);
}

The is_pod<T> is a type trait that tells us whether a type can be trivailly copied.

Importantly, an if constexpr is not a text-manipulation mechanism and cannot be used to break the usual rules of grammar, type, and scope. This is not the #ifdefine.

6.5

  • A virtual function member cannot be a template member functions;
// this is ERROR
class Bar{
public:
    // this is not doable.
    template<typename T>  
    virtual T work(T value){ return value;}
}

// this is OK
template<typename T>
class Bar {
public:
    virtual T get(){ return value_; }
    virtual ~Bar(){};
private:
    T value_;
};
  • Templates offer compile-type "duck typing"
  • There is no separate compilation of templates: #include template definitions in every translation unit that uses them

TODO

  • 6.2.3. deduction guide: how does that work and resolve the value_type?
  • 6.3.3. the object life time, and capture/argument/local var/