C++ Basic templates usage – Part 1

coralkashri

Coral Kashri

Posted on August 15, 2020

C++ Basic templates usage – Part 1

This is the second article in the series of meta programming tutorials. Here we'll see a very basic usage of templates in C++. Due to the amount of content that fits into the basic usage of templates, I separated this article into 2 parts. In this part I'll talk about the usage in functions & classes, and about how the compiler handle them.

Next post on series: Basic templates usage – Part 2

Generic Functions

Assume you have the following function:

int sum(int num1, int num2) { return num1 + num2;}
Enter fullscreen mode Exit fullscreen mode

Now you might want to apply the exact same functionality for float & double:

float sum(float num1, float num2) {
    return num1 + num2;
}

double sum(double num1, double num2) {
    return num1 + num2;
}
Enter fullscreen mode Exit fullscreen mode

The code just got 3 times expanded. This is where the templates come to our help. Templates give us the option to accept any type, and to apply the same algorithms for all of them (in future posts I’ll show how to make the compiler choose between different algorithms for different types in the same function, but for now- same algorithm different types). Let’s have a look on templates in action:

template <typename T>
T sum(T num1, T num2) {
    return num1 + num2;
}

int main() {
    int n1, n2;
    float n3, n4;
    double n5, n6;
    std::string a, b;

    n1 = 1; n2 = 2;
    n3 = 3.5f; n4 = 4.3f;
    n5 = 5.2; n6 = 6.1;
    a = "Hello "; b = "World";
    std::cout << sum<int>(n1, n2) << std::endl; // Prints 3 
    std::cout << sum<float>(n3, n4) << std::endl; // Prints 7.8
    std::cout << sum<double>(n5, n6) << std::endl; // Prints 11.3
    std::cout << sum<std::string>(a, b) << std::endl; // Prints "Hello World". Note: From the function's name it's clearly that this isn't the expected usage, but the compiler doesn't know it. We'll see in future posts how to handle such cases.

    return EXIT_SUCCESS;
}
Enter fullscreen mode Exit fullscreen mode

How Does It Work?

For every new call type to the template function, the compiler generates a new function specialization with the new types (since the types are known at compile time). For example, in our case the compiled code will look something like this:

template <typename T>
T sum(T num1, T num2) {
    return num1 + num2;
}

// Integer specialization
template<>
int sum<int>(int num1, int num2) {
    return num1 + num2;
}

// Float specialization
template<>
float sum<float>(float num1, float num2) {
    return num1 + num2;
}

// Double specialization
template<>
double sum<double>(double num1, double num2) {
    return num1 + num2;
}

// std::string specialization
template<>
std::string sum<std::string>(std::string num1, std::string num2) {
    return num1 + num2;
}

// ... main ...
Enter fullscreen mode Exit fullscreen mode

Function Specializations Deduction

In simple cases, when the compiler is able to automatically deduce the function template types just from the passed parameters, we don’t have to explicitly specify the types. For example, we could call sum this way:

int main() {
    /* ... Variable Declarations ... */

    std::cout << sum(n1, n2) << std::endl; // Prints 3 
    std::cout << sum(n3, n4) << std::endl; // Prints 7.8 
    std::cout << sum(n5, n6) << std::endl; // Prints 11.3 
    std::cout << sum(a, b) << std::endl; // Prints "Hello World".

    return EXIT_SUCCESS;}
Enter fullscreen mode Exit fullscreen mode

Generic Classes

The basic templates usage in classes is similar to the functions one. Assume you have a class that handles an integer

class my_class {
public:
    explicit my_class(int val) : m_val(val) {}
    void print_val() const { std::cout << m_val << std::endl; }
    [[nodiscard]] int get() const { return m_val; }
    void set(int val) { m_val = val; }

    int m_val;
};
Enter fullscreen mode Exit fullscreen mode

Now, to make this function also handle a float, double, std::string, and char, you’ll have to create 4 more separate classes, with 4 different names.

Why can’t you just create 4 different classes with the same name, like we did with the functions?

unlike functions, there is no such a thing “class overloading”, for a simple reason: At compile time, the compiler don’t know which class you refer to, and which class you want to create. The error you’ll get for trying define the same class name twice or more: “Redefinition of 'class_name'”.

To archive a single class that handle a single variable of different type every time you have several options:

  1. Use templates.
  2. Use std::variant (which is also implemented using template).

I'll show here the solution using templates, because std::variant is not in the scope of this series (I might talk about it in a future independent post).

template <typename T>
class my_class {
public:
    explicit my_class(T val) : m_val(val) {}
    void print_val() const { std::cout << m_val << std::endl; }
    [[nodiscard]] T get() const { return m_val; }
    void set(T val) { m_val = val; }

    T m_val;
};

int main() {
    my_class<int> mc_i(5);
    my_class<double> mc_d(3.6);

    return EXIT_SUCCESS;
}
Enter fullscreen mode Exit fullscreen mode

Class Specializations Behind The Scenes

Same as in the functions, for every new type usage of this class, the compiler will generate a complete class specialization. In our case:

template <typename T>
class my_class {
public:
    explicit my_class(T val) : m_val(val) {}
    void print_val() const { std::cout << m_val << std::endl; }
    [[nodiscard]] T get() const { return m_val; }
    void set(T val) { m_val = val; }

    T m_val;
};

// Integer specialization
template<>
class my_class<int> {
public:
    explicit my_class(int val) : m_val(val) {}
    void print_val() const { std::cout << m\_val << std::endl; }
    [[nodiscard]] int get() const { return m_val; }
    void set(int val) { m_val = val; }

    int m_val;
};

// Double specialization
template<>
class my_class<double> {
public:
    explicit my_class(double val) : m_val(val) {}
    void print_val() const { std::cout << m_val << std::endl; }
    [[nodiscard]] double get() const { return m_val; }
    void set(double val) { m_val = val; }

    double m_val;
};
Enter fullscreen mode Exit fullscreen mode

This way the compiler can make the decision which my_class specialization you refer to in your code.

Class Specializations Deduction

Since the class’s CTOR signature is different for every specialization, you don’t have to explicitly mention the class type:

int main() {
    my_class mc_i(5); // Integer specialization
    my_class mc_l(5l); // Long specialization
    my_class mc_d(5.0); // Double specialization
    my_class mc_f(5.f); // Float specialization

    return EXIT_SUCCESS;
}
Enter fullscreen mode Exit fullscreen mode

However, to use a CTOR that doesn’t have a unique signature for an specialization, you’ll still have to explicitly mention the type:

// my_class::my_class() {}

int main() {
    // my_class mc_i; // Deduction failure: "No viable constructor or deduction guide for deduction of template arguments of 'my_class'"
    my_class<int> mc_i; // Integer specialization

    return EXIT_SUCCESS;}
Enter fullscreen mode Exit fullscreen mode

Conclusion

This is all for the first part of basic templates usage. You are more than welcome to share your thoughts about templates and the instantiations. Personally, it took me long time to agree with the way the compiler copy the class for every different type of usage, back in the days it just seemed to me like a huge waste of code memory. Today I think it’s better than slow down performances in order to deduce the types at run time.

This post originally published on my personal blog: C++ Senioreas.

💖 💪 🙅 🚩
coralkashri
Coral Kashri

Posted on August 15, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related