Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
CPlusPlusNotesForProfessionals.pdf
Скачиваний:
58
Добавлен:
20.05.2023
Размер:
5.11 Mб
Скачать

Chapter 77: Templates

Classes, functions, and (since C++14) variables can be templated. A template is a piece of code with some free parameters that will become a concrete class, function, or variable when all parameters are specified. Parameters can be types, values, or themselves templates. A well-known template is std::vector, which becomes a concrete container type when the element type is specified, e.g., std::vector<int>.

Section 77.1: Basic Class Template

The basic idea of a class template is that the template parameter gets substituted by a type at compile time. The result is that the same class can be reused for multiple types. The user specifies which type will be used when a variable of the class is declared. Three examples of this are shown in main():

#include <iostream> using std::cout;

template <typename T> class Number { public:

void setNum(T n); T plus1() const;

private: T num;

};

template <typename T>

void Number<T>::setNum(T n) { num = n;

}

template <typename T>

T Number<T>::plus1() const { return num + 1;

}

//A simple class to hold one number of any type

//Sets the class field to the given number

//returns class field's "follower"

//Class field

//Set the class field to the given number

//returns class field's "follower"

int main() {

 

 

 

Number<int> anInt;

// Test with an integer (int replaces T in the class)

anInt.setNum(1);

 

 

 

cout << "My integer + 1 is " << anInt.plus1() << "\n";

// Prints

2

Number<double> aDouble;

// Test with a double

 

 

aDouble.setNum(3.1415926535897);

 

 

cout << "My double + 1 is " << aDouble.plus1() << "\n";

// Prints

4.14159

Number<float> aFloat;

// Test with a float

 

 

aFloat.setNum(1.4);

 

 

 

cout << "My float + 1 is " << aFloat.plus1() << "\n";

// Prints

2.4

return 0; // Successful completion

}

Section 77.2: Function Templates

Templating can also be applied to functions (as well as the more traditional structures) with the same e ect.

//'T' stands for the unknown type

//Both of our arguments will be of the same type.

GoalKicker.com – C++ Notes for Professionals

413

template<typename T>

void printSum(T add1, T add2)

{

std::cout << (add1 + add2) << std::endl;

}

This can then be used in the same way as structure templates.

printSum<int>(4, 5); printSum<float>(4.5f, 8.9f);

In both these case the template argument is used to replace the types of the parameters; the result works just like a normal C++ function (if the parameters don't match the template type the compiler applies the standard conversions).

One additional property of template functions (unlike template classes) is that the compiler can infer the template parameters based on the parameters passed to the function.

printSum(4, 5); // Both parameters are int.

//This allows the compiler deduce that the type

//T is also int.

printSum(5.0, 4); // In this case the parameters are two different types.

//The compiler is unable to deduce the type of T

//because there are contradictions. As a result

//this is a compile time error.

This feature allows us to simplify code when we combine template structures and functions. There is a common pattern in the standard library that allows us to make template structure X using a helper function make_X().

//The make_X pattern looks like this.

//1) A template structure with 1 or more template types. template<typename T1, typename T2>

struct MyPair

{

T1

first;

T2

second;

};

//2) A make function that has a parameter type for

//each template parameter in the template structure. template<typename T1, typename T2>

MyPair<T1, T2> make_MyPair(T1 t1, T2 t2)

{

return MyPair<T1, T2>{t1, t2};

}

How does this help?

auto

val1

=

MyPair<int, float>{5, 8.7};

//

Create

object

explicitly defining the types

auto

val2

=

make_MyPair(5, 8.7);

//

Create

object

using the types of the parameters.

//In this code both val1 and val2 are the same

//type.

Note: This is not designed to shorten the code. This is designed to make the code more robust. It allows the types to be changed by changing the code in a single place rather than in multiple locations.

GoalKicker.com – C++ Notes for Professionals

414

Section 77.3: Variadic template data structures

Version ≥ C++14

It is often useful to define classes or structures that have a variable number and type of data members which are defined at compile time. The canonical example is std::tuple, but sometimes is it is necessary to define your own custom structures. Here is an example that defines the structure using compounding (rather than inheritance as with std::tuple. Start with the general (empty) definition, which also serves as the base-case for recrusion termination in the later specialisation:

template<typename ... T> struct DataStructure {};

This already allows us to define an empty structure, DataStructure<> data, albeit that isn't very useful yet.

Next comes the recursive case specialisation:

template<typename T, typename ... Rest> struct DataStructure<T, Rest ...>

{

DataStructure(const T& first, const Rest& ... rest) : first(first)

, rest(rest...)

{}

T first;

DataStructure<Rest ... > rest;

};

This is now su cient for us to create arbitrary data structures, like DataStructure<int, float, std::string> data(1, 2.1, "hello").

So what's going on? First, note that this is a specialisation whose requirement is that at least one variadic template parameter (namely T above) exists, whilst not caring about the specific makeup of the pack Rest. Knowing that T exists allows the definition of its data member, first. The rest of the data is recursively packaged as DataStructure<Rest ... > rest. The constructor initiates both of those members, including a recursive constructor call to the rest member.

To understand this better, we can work through an example: suppose you have a declaration DataStructure<int, float> data. The declaration first matches against the specialisation, yielding a structure with int first and DataStructure<float> rest data members. The rest definition again matches this specialisation, creating its own float first and DataStructure<> rest members. Finally this last rest matches against the base-case defintion, producing an empty structure.

You can visualise this as follows:

DataStructure<int, float> -> int first

-> DataStructure<float> rest -> float first

-> DataStructure<> rest -> (empty)

Now we have the data structure, but its not terribly useful yet as we cannot easily access the individual data elements (for example to access the last member of DataStructure<int, float, std::string> data we would have to use data.rest.rest.first, which is not exactly user-friendly). So we add a get method to it (only needed

GoalKicker.com – C++ Notes for Professionals

415

in the specialisation as the base-case structure has no data to get):

template<typename T, typename ... Rest> struct DataStructure<T, Rest ...>

{

...

template<size_t idx> auto get()

{

return GetHelper<idx, DataStructure<T,Rest...>>::get(*this);

}

...

};

As you can see this get member function is itself templated - this time on the index of the member that is needed (so usage can be things like data.get<1>(), similar to std::tuple). The actual work is done by a static function in a helper class, GetHelper. The reason we can't define the required functionality directly in DataStructure's get is because (as we will shortly see) we would need to specialise on idx - but it isn't possible to specialise a template member function without specialising the containing class template. Note also the use of a C++14-style auto here makes our lives significantly simpler as otherwise we would need quite a complicated expression for the return type.

So on to the helper class. This time we will need an empty forward declaration and two specialisations. First the declaration:

template<size_t idx, typename T> struct GetHelper;

Now the base-case (when idx==0). In this case we just return the first member:

template<typename T, typename ... Rest>

struct GetHelper<0, DataStructure<T, Rest ... >>

{

static T get(DataStructure<T, Rest...>& data)

{

return data.first;

}

};

In the recursive case, we decrement idx and invoke the GetHelper for the rest member:

template<size_t idx, typename T, typename ... Rest> struct GetHelper<idx, DataStructure<T, Rest ... >>

{

static auto get(DataStructure<T, Rest...>& data)

{

return GetHelper<idx-1, DataStructure<Rest ...>>::get(data.rest);

}

};

To work through an example, suppose we have DataStructure<int, float> data and we need data.get<1>(). This invokes GetHelper<1, DataStructure<int, float>>::get(data) (the 2nd specialisation), which in turn invokes GetHelper<0, DataStructure<float>>::get(data.rest), which finally returns (by the 1st specialisation as now idx is 0) data.rest.first.

So that's it! Here is the whole functioning code, with some example use in the main function:

GoalKicker.com – C++ Notes for Professionals

416