Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Implementing Generic Class Behavior
Define member functions for class templates both inside and outside the class.
Class Templates with Member Functions
In the Function templates lesson, we looked at function templates:
template <typename T> // this is the template parameter declaration
T maximum(T x, T y) // this is the function template definition for maximum<T>
{
return (x > y) ? x : y;
}
With a function template, we can define type template parameters (e.g., typename T) and then use them as the type of our function parameters (T x, T y).
In the Class templates lesson, we covered class templates, which allow us to use type template parameters for the type of our data members:
#include <iostream>
template <typename T>
struct Box
{
T value{};
T capacity{};
};
// Here's a deduction guide for our Box (required in C++17 or older)
// Box objects initialized with arguments of type T and T should deduce to Box<T>
template <typename T>
Box(T, T) -> Box<T>;
int main()
{
Box<int> b1{ 10, 100 }; // instantiates Box<int> and creates object b1
std::cout << b1.value << ' ' << b1.capacity << '\n';
Box<double> b2{ 5.5, 50.0 }; // instantiates Box<double> and creates object b2
std::cout << b2.value << ' ' << b2.capacity << '\n';
Box<double> b3{ 7.5, 75.0 }; // creates object b3 using prior definition for Box<double>
std::cout << b3.value << ' ' << b3.capacity << '\n';
return 0;
}
Related content
We discuss deduction guides in the Class template argument deduction (CTAD) and deduction guides lesson.
In this lesson, we'll combine elements of both function templates and class templates as we take a closer look at class templates that have member functions.
Type template parameters in member functions
Type template parameters defined as part of a class template parameter declaration can be used both as the type of data members and as the type of member function parameters.
In the following example, we rewrite the above Box class template, converting it from a struct to a class:
#include <ios> // for std::boolalpha
#include <iostream>
template <typename T>
class Box
{
private:
T m_value{};
T m_capacity{};
public:
// When we define a member function inside the class definition,
// the template parameter declaration belonging to the class applies
Box(const T& value, const T& capacity)
: m_value{ value }
, m_capacity{ capacity }
{
}
bool isFull(const Box<T>& other);
};
// When we define a member function outside the class definition,
// we need to resupply a template parameter declaration
template <typename T>
bool Box<T>::isFull(const Box<T>& other)
{
return m_value == other.m_value && m_capacity == other.m_capacity;
}
int main()
{
Box box1{ 10, 100 }; // uses CTAD to infer type Box<int>
std::cout << std::boolalpha << "isFull(10, 100): " << box1.isFull( Box{10, 100} ) << '\n';
std::cout << std::boolalpha << "isFull(10, 50): " << box1.isFull( Box{10, 50} ) << '\n';
return 0;
}
The above should be fairly straightforward, but there are a few things worth noting.
First, because our class has private members, it is not an aggregate and therefore can't use aggregate initialization. Instead, we have to initialize our class objects using a constructor.
Since our class data members are of type T, we make the parameters of our constructor type const T&, so the user can supply initialization values of the same type. Because T might be expensive to copy, it's safer to pass by const reference than by value.
Note that when we define a member function inside the class template definition, we don't need to provide a template parameter declaration for the member function. Such member functions implicitly use the class template parameter declaration.
Second, we don't need deduction guides for CTAD to work with non-aggregate classes. A matching constructor provides the compiler with the information it needs to deduce the template parameters from the initializers.
Third, let's look more closely at the case where we define a member function for a class template outside of the class template definition:
template <typename T>
bool Box<T>::isFull(const Box<T>& other)
{
return m_value == other.m_value && m_capacity == other.m_capacity;
}
Since this member function definition is separate from the class template definition, we need to resupply a template parameter declaration (template <typename T>) so the compiler knows what T is.
Also, when we define a member function outside of the class, we need to qualify the member function name with the fully templated name of the class template (Box<T>::isFull, not Box::isFull).
Injected class names
In a prior lesson, we noted that the name of a constructor must match the name of the class. But in our class template for Box<T> above, we named our constructor Box, not Box<T>. Somehow this still works, even though the names don't match.
Within the scope of a class, the unqualified name of the class is called an injected class name. In a class template, the injected class name serves as shorthand for the fully templated name.
Because Box is the injected class name of Box<T>, within the scope of our Box<T> class template, any use of Box will be treated as if we had written Box<T> instead. Therefore, although we named the constructor Box, the compiler treats it as if we had written Box<T> instead. The names now match!
This means we can also define our isFull() member function like this:
template <typename T>
bool Box<T>::isFull(const Box& other) // note the parameter has type Box, not Box<T>
{
return m_value == other.m_value && m_capacity == other.m_capacity;
}
Because this is a definition for a member function of Box<T>, we're in the scope of the Box<T> class template. Therefore, any use of Box is shorthand for Box<T>!
In the Class template argument deduction (CTAD) and deduction guides lesson, we noted that CTAD doesn't work with function parameters (as it is argument deduction, not parameter deduction). However, using an injected class name as a function parameter is okay, as it is shorthand for the fully templated name, not a use of CTAD.
Where to define member functions for class templates outside the class
With member functions for class templates, the compiler needs to see both the class definition (to ensure the member function template is declared as part of the class) and the template member function definition (to know how to instantiate the template). Therefore, we typically want to define both a class and its member function templates in the same location.
When a member function template is defined inside the class definition, the template member function definition is part of the class definition, so anywhere the class definition can be seen, the template member function definition can also be seen. This makes things easy (at the cost of cluttering our class definition).
When a member function template is defined outside the class definition, it should generally be defined immediately below the class definition. That way, anywhere the class definition can be seen, the member function template definitions immediately below will also be seen.
In the typical case where a class is defined in a header file, this means any member function templates defined outside the class should also be defined in the same header file, below the class definition.
In the Function template instantiation lesson, we noted that functions implicitly instantiated from templates are implicitly inline. This includes both non-member and member function templates. Therefore, there is no issue including member function templates defined in header files into multiple code files, as the functions instantiated from those templates will be implicitly inline (and the linker will de-duplicate them).
Any member function templates defined outside the class definition should be defined just below the class definition (in the same file).
Summary
Class template member functions: Member functions of class templates can use the template type parameters defined in the class template parameter declaration. These parameters can be used for both data members and function parameters.
Defining member functions inside the class: When defined inside the class template definition, member functions implicitly use the class template parameter declaration and don't need their own template parameter declaration.
Defining member functions outside the class: When defined outside the class template definition, member functions need their own template parameter declaration and must qualify the function name with the fully templated class name (e.g., Box<T>::functionName).
Injected class names: Within the scope of a class template, the unqualified class name serves as shorthand for the fully templated name. This allows writing Box instead of Box<T> within the class scope, including for constructors and function parameters.
Location of definitions: Member function templates defined outside the class should be placed immediately below the class definition in the same file (typically a header file). This ensures the compiler can see both the class definition and the function template definitions together.
Implicit inline: Functions instantiated from member function templates are implicitly inline, allowing them to be safely included in header files without violating the one definition rule.
Understanding how to work with class template member functions is essential for creating flexible, reusable code. The ability to define functions both inside and outside the class provides flexibility in organizing code while maintaining the benefits of templates.
Implementing Generic Class Behavior - Quiz
Test your understanding of the lesson.
Practice Exercises
Generic Stack Container
Create a class template with member functions defined both inside and outside the class. Implement a simple stack data structure.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!