Generic classes in Modern C++

In this article we’re going to continue the theme of a class-designer’s toolkit, looking at adding generics (template functionality) to C++ classes. Templates have been around since C++98, and almost all C++ programmers will have used them: std::vector<int> has the type int as a parameter, itself being a specialization of a generic class. Any user-defined type can be used as a type parameter, having equal status to the built-in types.

Generic classes

Classes which are preceded by a the template keyword are automatically turned into generic classes. These classes can have any number of template parameters, we’re going to concentrate on classes having just one, this being a template type parameter. The class definition starts of as follows:

template<typename T>
class Named {
    T value;
    std::string name;
public:
// rest of class definition ...

The unspecified (generic) type T can be used within the class body in place of a user-defined or built-in type name (in this case with the member variable value), and other non-template member variables can be used (as shown with name). Strictly speaking, an instance of class Named should be defined as Named<int> etc., but in Modern C++ the type detection facilities allow a plain Named in many cases. In older code the use of class instead of typename may sometimes be found; the meaning is identical as the two are interchangeable in this context.

Here is the full class definition in an example program:

#include <iostream>
#include <string>
#include <string_view>

template<typename T>
class Named {
    T value;
    std::string name;
public:
    Named(const T& value, std::string_view name)
        : value{value}, name{name} {}
    const T& get() const { return value; }
    std::string_view get_name() const { return name; }
};

template<typename T>
std::ostream& operator<<(std::ostream& os, const Named<T>& n) {
    return os << n.get_name() << ':' << n.get();
}

int main() {
    Named i(42, "answer");
    Named p(3.14, "pi");
    std::cout << i << '\n' << p << '\n';
}

Lines 10-11 are the constructor whose parameter value sets the type of the template value for the class, with no further work required on the part of the programmer. Lines 12-13 are getters used to obtain the values of the member variables, which following good C++ practice are declared const. The use of std::string_view is becoming more common in C++, as it allows const char* and std::string values to be passed without needing a std::string object to be created.

Lines 16-19 allow outputting of our Named class to streams. It is a generic function whose type is deduced from the type of parameter n‘s own template type parameter. Being a friend of Named is not necessary as public getters are used. Lines 21-25 demonstrate use of the Named class, the output being:

answer:42
pi:3.14

Generic Members

Even if a class is not a generic class, it can still have member functions which are themselves generic. (Combining both is also a possibility.) The use of the template keyword comes within the body of the class, immediately before the generic function(s). For special member functions, such as the constructor, a mixture of non-template specializations and generic version(s) can be used:

class AnythingString {
    std::string str;
public:
    AnythingString() {}
    AnythingString(std::string_view value) {
        str = value;
    }
    template<typename T>
    AnythingString(const T& value) {
        std::ostringstream oss;
        oss << value;
        str = oss.str();
    }
// rest of class definition ...

Three constructors are defined above, the default constructor, a non-template constructor accepting a std::string_view, and a generic constructor which converts its parameter into a std::string to store in the member variable str.

Here is the full class definition in an example program:

#include <iostream>
#include <string>
#include <string_view>
#include <sstream>

using namespace std::literals;

class AnythingString {
    std::string str;
public:
    AnythingString() {}
    AnythingString(std::string_view value) {
        str = value;
        std::cerr << "string_view specialization called\n";
    }
    template<typename T>
    AnythingString(const T& value) {
        std::ostringstream oss;
        oss << value;
        str = oss.str();
        std::cerr << "generic conversion called\n";
    }
    template<typename T>
    AnythingString& operator=(const T& value) {
        str = AnythingString(value).get();
        return *this;
    }
    template<typename T>
    AnythingString& operator+=(const T& value) {
        str += AnythingString(value).get();
        return *this;
    }
    std::string_view get() const { return str; }
};

std::ostream& operator<<(std::ostream& os, const AnythingString& s) {
    return os << s.get();
}

int main() {
    AnythingString s = 1.23;
    s += ' ';
    s += 45;
    s += " anything"sv;
    std::cout << s << '\n';
}

Lines 23-27 are the generic definition of the copy-assignment operator, which creates a temporary AnythingString from its parameter and uses the std::string element thus created. In the same way, lines 28-32 are the generic definition of operator plus-assign, however this time the std::string element of the temporary AnythingString is appended.

Lines 36-38 are the definition of the stream output function, which does not need to be generic as the class AnythingString is not itself generic. Finally, lines 40-46 test the class with some different types as input, the output being:

generic conversion called
generic conversion called
generic conversion called
string_view specialization called
1.23 45 anything

Note that even though the explicit keyword was not used with the constructor taking a std::string_view, passing a std::string or const char* uses the (slower) generic conversion in preference. Thus, two more specializations should be written in order to cater for these types as input to the class constructor.

Non-type template parameters

We may wish to provide a compile-time constant as a template parameter to a class definition. Often, this will be a size_t, but could in practice be any built-in type (or a user-defined type with a constexpr constructor). An advantage of this approach is that with the memory requirements of a class having been explicitly specified, heap allocations can be avoided (this is often important for embedded systems).

The syntax is not complicated, we simply provide the type in place of the keyword typename as we have seen above:

template<size_t N>
class RandomRange {
    std::array<int,N> range;
// rest of class definition ...

We can reuse the non-type template parameter (here called N) later in the class definition. Here a private member called range is a std::array of type int and size N. We will fill range with N random numbers, each of which will appear exactly once. A definition of an instance of this class could be RandomRange<4>, for which we could predict would use 16 bytes as its storage class.

Here is the full class definition in an example program:

#include <array>
#include <random>
#include <numeric>
#include <algorithm>
#include <iostream>

template<size_t N>
class RandomRange {
    std::array<int,N> range;
public:
    template<typename T>
    RandomRange(T& engine, int start = 0) {
        std::iota(range.begin(), range.end(), start);
        std::shuffle(range.begin(), range.end(), engine);
    }
    const std::array<int,N>& operator()() {
        return range;
    }
};

int main() {
    std::default_random_engine dre;
    RandomRange<9> r(dre, 1);
    for (const auto n : r()) {
        std::cout << "- " << n << '\n';
    }
}

Lines 1-5 are the headers, <random> for std::default_random_engine, <numeric> for std::iota and <algorithm> for std::shuffle. Lines 11-15 are a generic constructor taking a template type parameter T which is only required within the constructor itself when initializing the class. In lines 16-18 we allow access to the range member using operator() as a “getter”.

In the main program, line 23 defines and initializes the instance of the RandomRange class with a size of nine. This is set to commence at one and use the random number generator from line 22. Lines 24-26 simply output the sequence already generated, no further random number generation is used here.

That concludes our look at generic facilities in Modern C++ which can be applied to classes. We have seen how template type parameters and template non-type parameters can be used with classes and/or member functions. You should now be confident about how to use the generic features of Modern C++ when designing your own class types.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s