Designing Traits and Policy Classes

Traits and policy classes are often used with C++ generics, but their role and purpose is often something of an enigma even to experienced C++ programmers. The definitions of these two terms overlap to some extent, and could be summarized as follows:

  • Traits represent natural additional properties of a template parameter.
  • Policies represent configurable behavior for generic functions and types.

In this article the example code will show use of both of these two, with type information obtained through a (specialized) traits class and functionality obtained from a (generic) policy class.

The motivating example is to allow the following code to compile and run:

auto sum = accum({'a', 'b', 'c', 'd', 'e'});  // sum = 495

Of course, we could use std::accumulate() over a vector<char> with an initial value of type int to avoid overflow, but for demonstration purposes we’re going to “roll our own”. This begins with creation of an AccumulateTypeTraits class to provide a type for the result based upon the type of the element(s). The requirements for this traits class are as follows:

  • For unsigned integral element types, provide unsigned long long
  • For signed integral element types, provide long long
  • For floating-point element types, use the same type for the result
  • Disallow all other types

The resulting code as a C++ header file looks like this:

#include <type_traits>

template <typename T, typename U = void>
struct AccumulateTypeTraits {
    using Type = U;
};

template <typename T>
struct AccumulateTypeTraits<T, typename std::enable_if_t<std::is_integral_v<T> && std::is_unsigned_v<T>>> {
    using Type = unsigned long long;
};

template <typename T>
struct AccumulateTypeTraits<T, typename std::enable_if_t<std::is_integral_v<T> && std::is_signed_v<T>>> {
    using Type = long long;
};

template <typename T>
struct AccumulateTypeTraits<T, typename std::enable_if_t<std::is_floating_point_v<T>>> {
    using Type = T;
};

Note that the <type_traits> header is required, and that use of abc_v<T> is a shorthand for abc<T>::value, which you may see in older code. The general case will fail when attempting to create a variable of type void, and as usual the specializations follow (in no particular order).

The client code looks like this:

#include "Traits.hpp"
#include <iostream>
#include <initializer_list>

template<typename T, typename R = AccumulateTypeTraits<T>::Type>
auto accum(const std::initializer_list<T>& arr) {
    R r{};
    auto elem = arr.begin(), end = arr.end();
    while (elem != end) {
        r += *elem++;
    }
    return r;
}

int main() {
    std::cout << "accum({1, 2, 3, 4, 5}) = " << accum({1, 2, 3, 4, 5}) << '\n';
    std::cout << "accum({'a', 'b', 'c', 'd', 'e'}) = " << accum({'a', 'b', 'c', 'd', 'e'}) << '\n';
    std::cout << "accum({1.1, 2.2, 3.3, 4.4, 5.5}) = " << accum({1.1, 2.2, 3.3, 4.4, 5.5}) << '\n';
}

And produces the output:

accum({1, 2, 3, 4, 5}) = 15
accum({'a', 'b', 'c', 'd', 'e'}) = 495
accum({1.1, 2.2, 3.3, 4.4, 5.5}) = 16.5

Now suppose we want to generalize our code so that other accumulation functions such as multiplication can be used. We can still use the same traits class, but need to design policy classes which provide both an initializer value (zero for addition, unity for multiplication) and an operation function. The outline of a C++ header file defining two different classes, SummationPolicy and MultiplicationPolicy, each with a static init member and a static operation() function looks like this:

struct SummationPolicy {
    inline const static int init{};

    template<typename T,typename U>
    inline static void operation(T& s, const U& v) {
        s += v;
    }
};

struct MultiplicationPolicy {
    inline const static int init{ 1 };

    template<typename T,typename U>
    inline static void operation(T& s, const U& v) {
        s *= v;
    }
};

This can be then used by the modified client code as follows:

#include "Traits.hpp"
#include "Policy.hpp"
#include <iostream>
#include <initializer_list>

template<typename P = SummationPolicy, typename T = void, typename R = AccumulateTypeTraits<T>::Type>
auto accum(const std::initializer_list<T>& arr) {
    R r{ P::init };
    auto elem = arr.begin(), end = arr.end();
    while (elem != end) {
        P::operation(r, *elem++);
    }
    return r;
}

int main() {
    std::cout << "accum({1, 2, 3, 4, 5}) = " << accum({1, 2, 3, 4, 5}) << '\n';
    std::cout << "accum({'a', 'b', 'c', 'd', 'e'}) = " << accum({'a', 'b', 'c', 'd', 'e'}) << '\n';
    std::cout << "accum({1.1, 2.2, 3.3, 4.4, 5.5}) = " << accum({1.1, 2.2, 3.3, 4.4, 5.5}) << '\n';
    std::cout << "accum<MultiplicationPolicy>({1, 2, 3, 4, 5}) = " << accum<MultiplicationPolicy>({1, 2, 3, 4, 5}) << '\n';
    std::cout << "accum<MultiplicationPolicy>({'a', 'b', 'c', 'd', 'e'}) = " << accum<MultiplicationPolicy>({'a', 'b', 'c', 'd', 'e'}) << '\n';
    std::cout << "accum<MultiplicationPolicy>({1.1, 2.2, 3.3, 4.4, 5.5}) = " << accum<MultiplicationPolicy>({1.1, 2.2, 3.3, 4.4, 5.5}) << '\n';
}

The output from running this program is:

accum({1, 2, 3, 4, 5}) = 15
accum({'a', 'b', 'c', 'd', 'e'}) = 495
accum({1.1, 2.2, 3.3, 4.4, 5.5}) = 16.5
accum<MultiplicationPolicy>({1, 2, 3, 4, 5}) = 120
accum<MultiplicationPolicy>({'a', 'b', 'c', 'd', 'e'}) = 9505049400
accum<MultiplicationPolicy>({1.1, 2.2, 3.3, 4.4, 5.5}) = 193.261

Making the template parameters work correctly can sometimes be tricky, but produces the result of clean calling conventions even with code which is extremely general. That’s it for this article, hopefully it will have given some ideas of how to implement traits and policy classes in your own code. Source code is available for download on GitHub.

Leave a comment