Chapter 3
IN THIS CHAPTER
Defining template and template library creation
Understanding the elements of good template design
Developing basic math, structure, and class templates
Using template specialization to your advantage
C++ has been around for many years. Because of its longevity, C++ templates abound. In fact, it may seem that there is a template for every practical purpose. However, the templates that are available to the developer community through standardized and third-party resources usually reflect generalized needs. The individual company you work for (or you as a developer) may have specialized needs that a generalized template can’t address.
The trick to creating a customized tool is to think the process through, just as you would for any application you create. The fact that you’ll use this customized tool to create multiple applications means that you must apply a higher standard to its design and the code it contains than you would for one-time applications. A mistake in a customized tool can spell errors in every application you create using it, so this code must work well.
This chapter addresses the thought process behind templates first and then shows some typical template examples. The examples help demonstrate ways in which you can use templates to create better applications that require less code because the templates you create meet your needs more completely than any generalized template can. After you see the template examples, you discover the techniques used to place a number of templates in a library. Finally, you discover how to use the template library to create applications.
The examples in this chapter discuss significant template creation and use details. However, they’re designed to work with a broad range of C++ versions simply because templates are most useful when they support more than the latest version. However, C++ 17 and 20 do provide some interesting additional features, such as type deduction (see the “Understanding the Role of auto” section in Book 3, Chapter 1 for details) and you can read about them in the article at https://dzone.com/articles/c-template-story-so-farc11-to-c20
.
The first step in creating a template is deciding whether your idea will generate a useful template. Most developers have thousands of creative thoughts that translate into ideas during their careers; however, only a few of these ideas are exceptionally useful. By determining whether the template you want to create is a good idea at the outset, you waste less time on bad ideas and have more time to create that truly useful idea. Before you begin creating a new template, consider the following questions:
Book 5, Chapter 5 offers some insights into basic template creation techniques. However, that introductory chapter doesn’t address what makes for a good template. The template you create has to look professional and work as expected. The first decision you have to make is what kind of template to create. You can choose among these three types:
The second decision you have to make is how to weight design factors when creating the template. C++ offers myriad ways to accomplish any given task. For example, you have multiple ways to create a function. However, one method is normally superior to the others simply because it offers some particular benefit. Consider these requirements when choosing the kind of template to create:
The third decision you must make is how inclusive to make the template. In some cases, you want to create a template that can handle a range of situations. However, a template can quickly become unwieldy and difficult to manage. A good template is balanced; it includes the elements you need, but nothing beyond.
With a math template, you usually need access to a wealth of calculations but may use only one or two of those calculations at a time. For example, when calculating your mortgage, you don’t need to know the amortization calculation. However, you might need the amortization calculation the next week when thinking about a retirement plan. In short, the calculations all have a purpose, and you need them all, but you don’t need them all at the same time. Because of the way you use math templates, they work best as a series of function templates. The MathTemplate
example, in Listing 3-1, shows how to create the series of functions.
LISTING 3-1: Defining a Series of Function Templates
#include <iostream>
#include <cmath>
using namespace std;
template<typename T> T Area(T height, T length) {
return height * length;
}
const double PI = 4.0*atan(1.0);
template<typename T> T CircleArea(T radius) {
double result;
result = PI * radius * radius;
// This version truncates the value.
return (T)result;
}
template<typename T> T TriangleArea(T base, T height) {
double result;
result = base * height * 0.5;
return (T)result;
}
int main() {
cout << "4 X 4 Areas:" << endl;
cout << "Square: " << Area<int>(4, 4) << endl;
cout << "Circle: " << CircleArea<int>(2) << endl;
cout << "Triangle: " << TriangleArea<int>(4, 4) << endl;
cout << "Using a value of pi of: " << PI << endl;
return 0;
}
The calculations could consist of any math calculation. The point of the example is that using functions makes each of the calculations discrete, easy to use, and easy to manage. When you run this example, you see the following output:
4 X 4 Areas:
Square: 16
Circle: 12
Triangle: 8
Using a value of pi of: 3.14159
Note that CircleArea<int>(2)
uses half the value of the other calculations as input. That’s because you calculate the area of a circle using the equation π × r2. If you want to see other area and volume equations, check out the website at http://www.aquatext.com/calcs/calculat.htm
.
radius = radius / 2;
result = PI * radius * radius;
Dividing the input by 2, essentially changing the diameter to a radius, means that you could call the equation using the same number as all the other area calculations: CircleArea<int>(4)
. Whichever approach you choose, you need to document how the template works so that other developers know how to use it.
You should also note that the circle and triangle calculations perform a bit of type coercion to ensure that the user gets the expected results back by modifying the return
statement to read return (T)result;
. The type conversions are needed to keep your templates from generating warning messages. It’s important to note that the approach used in the example truncates the result when the template returns an int
.
Structure templates have many interesting uses, such as creating a data repository that doesn’t depend on a particular type. The StructureTemplate
example, shown in Listing 3-2, shows one such use.
LISTING 3-2: Creating a Template from a Structure
#include <iostream>
using namespace std;
template<typename T> struct Volume {
T height;
T width;
T length;
Volume() {
height = 0;
width = 0;
length = 0;
}
T getvolume() {
return height * width * length;
}
T getvolume(T H, T W, T L) {
height = H;
width = W;
length = L;
return height * width * length;
}
};
int main() {
Volume<int> first;
cout << "First volume: " << first.getvolume() << endl;
first.height = 2;
first.width = 3;
first.length = 4;
cout << "First volume: " << first.getvolume() << endl;
Volume<double> second;
cout << "Second volume: "
<< second.getvolume(2.1, 3.2, 4.3) << endl;
cout << "Height: " << second.height << endl;
cout << "Width: " << second.width << endl;
cout << "Length: " << second.length << endl;
return 0;
}
In this case, the structure contains height, width, and length data values that the code can use to determine volume. The structure includes a constructor to initialize the values, so even if someone calls getvolume()
without initializing the structure, nothing bad will happen. The structure allows independent access of each of the data values. You can set or get them as needed.
The getvolume()
function is overloaded. You can call it with or without input values. The code in main()
tests the structure thoroughly. Here’s what you see as output from this example:
First volume: 0
First volume: 24
Second volume: 28.896
Height: 2.1
Width: 3.2
Length: 4.3
template<typename T>
typedef map<string, T> MyDef;
When you try to compile this code in Code::Blocks, you see the following error:
error: template declaration of 'typedef'
However, you can define a typedef
within a structure template. The StructureTemplate2
example code, in Listing 3-3, shows a variation of the example found in Listing 6-4 of Book 5, Chapter 6.
LISTING 3-3: Using a Structure to Define a typedef
#include <iostream>
#include <map>
using namespace std;
template<typename T> struct MyDef {
typedef map<string, T> Type;
};
int main() {
MyDef<string>::Type marriages;
marriages["Tom"] = "Suzy";
marriages["Harry"] = "Harriet";
cout << marriages["Tom"] << endl;
cout << marriages["Harry"] << endl;
return 0;
}
This example overcomes the C++ limitations by placing the typedef
within the struct
, MyDef
. The same structure can hold any number of typedef
entries.
Suzy
Harriet
Class templates perform the heavy lifting of the template types. You use a class template to define objects of nearly any size. Classes are larger and more complex than the other techniques demonstrated in the chapter so far. In most cases, you use classes to represent complex objects or to perform tasks ill suited for function or structure templates.
The example shows a specialized queue implementation. It includes many of the features of a standard queue and then adds a few features to meet special development needs. Queues and other containers tend to contain complex code, but you also need to use them with a variety of data types, making a class template the perfect implementation. The ClassTemplate
example, shown in Listing 3-4, shows the code for this example.
LISTING 3-4: Creating a Specialized Queue
#include <iostream>
#include <vector>
using namespace std;
template<typename T> class MyQueue {
protected:
vector<T> data;
public:
void Add(T const &input);
void Remove();
void PrintString();
void PrintInt();
bool IsEmpty();
};
template<typename T> void MyQueue<T>::Add(T const &input){
data.push_back(input);
}
template<typename T> void MyQueue<T>::Remove() {
data.erase(data.begin());
}
template<typename T> void MyQueue<T>::PrintString() {
vector<string>::iterator PrintIt = data.begin();
while (PrintIt != data.end()) {
cout << *PrintIt << endl;
PrintIt++;
}
}
template<typename T> void MyQueue<T>::PrintInt() {
vector<int>::iterator PrintIt = data.begin();
while (PrintIt != data.end()) {
cout << *PrintIt << endl;
PrintIt++;
}
}
template<typename T> bool MyQueue<T>::IsEmpty() {
return data.begin() == data.end();
}
int main() {
MyQueue<string> StringQueue;
cout << StringQueue.IsEmpty() << endl;
StringQueue.Add("Hello");
StringQueue.Add("Goodbye");
cout << "Printing strings: " << endl;
StringQueue.PrintString();
cout << StringQueue.IsEmpty() << endl;
StringQueue.Remove();
cout << "Printing strings: " << endl;
StringQueue.PrintString();
StringQueue.Remove();
cout << StringQueue.IsEmpty() << endl;
MyQueue<int> IntQueue;
IntQueue.Add(1);
IntQueue.Add(2);
cout << "Printing ints: " << endl;
IntQueue.PrintInt();
return 0;
}
The example starts with the class MyQueue
. Note that data
is a vector
, not a queue
as you might expect. A queue
is an adapter — as such, it doesn’t provide support for many of the features found in containers, such as vector
. One of these features is the use of iterators
.
MyQueue
includes the capability to add, remove, and print elements. In addition, you can check whether a queue is empty or full. You have already seen the code for these tasks in other parts of the book.
You might wonder about the code used for printing. The example includes separate methods for printing strings and integers, which might seem counterintuitive. After all, why not simply declare the iterator as follows so that it accepts any data type:
vector<T>::iterator PrintIt = data.begin();
The problem is that the iterator requires a specific data type. Consequently, you must declare it as shown previously in Listing 3-4. Otherwise you get this unhelpful error message:
error: expected ';' before 'PrintIt'
At some point, you want to test this new class using steps similar to those found in main()
. The test checks whether the queue actually does detect the empty and filled states, determines how adding and removing elements works, and checks whether the print routines work. Here is the output from this example:
1
Printing strings:
Hello
Goodbye
0
Printing strings:
Goodbye
1
Printing ints:
1
2
Some templates don’t go together quite as easily as you might expect because they express a concept that doesn’t translate the same way for every data type. For example, when you use stringify
to turn a data type into its string representation, the technique differs based on data type. When using stringify
on an int
, you might use the following template (as shown in the StringifyInt
example):
#include <iostream>
#include <sstream>
using namespace std;
template<typename T>
inline string stringify(const T& input) {
ostringstream output;
output << input;
return output.str();
}
int main() {
// This call works as expected.
cout << stringify<int>(42) << endl;
// This call truncates.
cout << stringify<double>(45.6789012345) << endl;
return 0;
}
The stringify()
function accepts any data type and simply uses an ostringstream
to convert input
to a string
. This approach works fine for the first call in main()
, which is an int
. However, when the code uses it for a double
, the result is truncated, as shown here:
42
45.6789
#include <iostream>
#include <sstream>
#include <iomanip>
#include <limits>
using namespace std;
template<typename T>
inline string stringify(const T& input) {
ostringstream output;
output << input;
return output.str();
}
template <>
inline string stringify<double> (const double& input) {
ostringstream output;
const int sigdigits = numeric_limits<double>::digits10;
output << setprecision(sigdigits) << input;
return output.str();
}
int main() {
cout << stringify<int>(42) << endl;
cout << stringify<double>(45.6789012345) << endl;
return 0;
}
When you run this example, you see the expected result because the double
form of the template uses setprecision
to modify the ostringstream
value. As a result, you see the following output:
42
45.6789012345
You won’t normally create a template and stick it in your application project file. The previous examples in this chapter put everything together for ease of explanation, but in the real world, templates usually reside in a library. Code::Blocks provides several kinds of library projects. This chapter looks at the static library
— a library that is added into the application. Templates always reside in static libraries.
Creating a library project is only a little different than creating a console application. The following steps describe how to create a library project:
Choose File ⇒ New ⇒ Project.
You see the New From Template dialog box, shown in Figure 3-1.
FIGURE 3-1: Provide a description of your project for Code::Blocks.
Highlight the Static Library icon on the Projects tab and then click Go.
You see the Welcome page of the Static Library wizard.
Click Next.
You see a list of project-related information fields, as shown in Figure 3-2. These questions define project basics, such as the project name.
FIGURE 3-2: Provide a description of your static library for Code::Blocks.
Type a name for your project in the Project Title field.
The example uses MathLibrary
as the project title. Notice that the wizard automatically starts creating an entry for you in the Project Filename field.
Click Next.
You see the compiler settings, shown in Figure 3-3. This example uses the default compiler settings. However, it’s important to remember that you can choose a different compiler, modify the locations of the debug and release versions of the project, and make other changes as needed. Code::Blocks provides the same level of customization for libraries as it does for applications.
Change any required compiler settings and click Finish.
The wizard creates the application for you. It then displays the Code::Blocks IDE with the project loaded. This template creates a main.c
file rather than a main.cpp
file. Note that the Static Library project main.c
file includes some sample code to get you started. You could compile this library and test it now.
FIGURE 3-3: Change the compiler settings to meet your project needs.
The static library starts with a standard C file. To make this library work well with templates, you need to delete the C file, add a C++ file, and add a header file. The following steps describe how to perform this process:
Right-click main.c
in the Projects tab of the Management window and choose Remove File From Project from the context menu that appears.
Code::Blocks removes the file from the project tree.
Choose File ⇒ New ⇒ File.
You see the New from Template dialog box, shown in Figure 3-4.
Highlight the C/C++ Header icon and click Go.
You see the Welcome page of the C/C++ Header wizard.
Click Next.
The wizard asks you to provide the header configuration information (see Figure 3-5).
In the Filename with Full Path field, type MathLibrary.h, click the ellipsis (…) button, and then click Save.
Code::Blocks adds the complete project path to the filename you chose. Notice that Code::Blocks also supplies an entry for the Header Guard Word field. This word ensures that the header isn’t added more than once to a project.
Click All and then click Finish.
The C/C++ Source wizard adds the file to your project. You’re ready to begin creating a template library.
FIGURE 3-4: Add new files using the New from Template dialog box.
FIGURE 3-5: Define the header requirements.
At this point, you have what amounts to a blank header file in a static library project. Your static library could conflict with other libraries, so it’s important to add a namespace to your code. The example uses MyNamespace
, but normally you’d use something related to you as a person or your company, such as MyCompanyInc
. The MathLibrary
heading, in Listing 3-5, shows what you need to create the library used for this example.
LISTING 3-5: Creating a Static Library
#ifndef MATHLIBRARY_H_INCLUDED
#define MATHLIBRARY_H_INCLUDED
#include <iostream>
#include <cmath>
using namespace std;
namespace MyNamespace {
template<typename T> T Area(T height, T length) {
return height * length;
}
const double PI = 4.0*atan(1.0);
template<typename T> T CircleArea(T radius) {
double result;
result = PI * radius * radius;
// This version truncates the value.
return (T)result;
}
template<typename T> T TriangleArea(T base, T height) {
double result;
result = base * height * 0.5;
return (T)result;
}
}
#endif // MATHLIBRARY_H_INCLUDED
As you can see, this is a portable form of the math library discussed in the “Creating a Basic Math Template” section, earlier in this chapter. Of course, the library form has changes. You have the usual #define
statements and the use of a namespace to encapsulate all the code. Notice that the namespace comes after all the declarations.
You have a shiny new template library. It’s time to test it. The MathLibraryTest
console application uses MathLibrary
to display some area information. The output is the same as in the “Creating a Basic Math Template” section, earlier in this chapter. Listing 3-6 shows the test code used for this example.
LISTING 3-6: Testing the Static Library
#include <iostream>
#include "..\MathLibrary\MathLibrary.h"
using namespace std;
using namespace MyNamespace;
int main() {
cout << "4 X 4 Areas:" << endl;
cout << "Square: " << Area<int>(4, 4) << endl;
cout << "Circle: " << CircleArea<int>(2) << endl;
cout << "Triangle: " << TriangleArea<int>(4, 4) << endl;
cout << "Using a value of pi of: " << PI << endl;
return 0;
}
When you use your own libraries, you need to tell the compiler where to find them. Because you likely created the example library in the same folder as the test application, you can use the simple path shown in Listing 3-6.
Because the library relies on a namespace, you must also include using namespace MyNamespace;
in the example code. Otherwise, you’ll spend hours trying to figure out why the compiler can’t locate the templates in your library. You access and use the template library much as you did before.