Louis Abraham's Home Page

Checking properties of C++ template types

01 Jun 2023

When working with C++, templates offer immense power, letting you write code that’s both generic and reusable. However, they can be a bit of a double-edged sword if the necessary properties of template parameters aren’t well-defined or properly verified. In this article, we’ll shed light on how to inspect some properties of C++ types and ensure that our template data structure doesn’t assume more about the parameters K and V than what’s required.

A quick look at our example data structure

Let’s take a look at our templated data structure:

template <typename K, typename V>
class MyMap
{

private:
    std::map<K, V> m_map;

public:
    MyMap() {}
    void insert(K key, V value)
    {
        m_map.insert(std::make_pair(key, value));
    }
    void erase(K key)
    {
        m_map.erase(key);
    }
    V &operator[](K key)
    {
        return m_map[key];
    }
};

This class simply wraps around a std::map, providing us with the flexibility to store any types K and V as key-value pairs. But what exactly are the requirements for K and V?

Checking type properties with type_traits

One approach to verify if a type possesses a certain property is to leverage the type_traits library in C++. This library offers a collection of type traits that can be used to manipulate and inspect type properties at compile time.

Let’s say we want to ensure that K and V are both copy constructible. We can employ std::is_copy_constructible to verify this:

static_assert(std::is_copy_constructible<K>::value, "K must be copy constructible");
static_assert(std::is_copy_constructible<V>::value, "V must be copy constructible");

This is useful if we know what properties we want. However it will not ensure that our implementation doesn’t assume any additional properties on K and V than required.

Ensuring no extra properties are required with mock types

To verify that your data structure does not demand any additional properties on K and V, you can compile tests using mock types. Let’s consider the following mock types:

class IntK
{
private:
    int val;

public:
    // Custom constructor to enforce no default constructor
    IntK(int v) : val(v) {}

    bool operator<(const IntK &other) const
    {
        return val < other.val;
    }
};

class CharV
{
private:
    char val;

public:
    // Custom constructor, prevents from creating a default constructor
    CharV(char v) : val(v) {}

    // Default constructor because it is required by std::map::operator[]
    CharV() : val('a') {}
};

These mock types, IntK and CharV, meet the bare minimum requirements to be used as K and V in our MyMap class. They consciously define custom constructors, compelling us to define a default constructor only if needed.

With these mock types, we can compile a test:

#include <map>
#include <cassert>
#include <string>

template <typename K, typename V>
class MyMap
{
    static_assert(std::is_copy_constructible<K>::value, "K must be copy constructible");
    static_assert(std::is_copy_constructible<V>::value, "V must be copy constructible");

private:
    std::map<K, V> m_map;

public:
    MyMap() {}
    void insert(K key, V value)
    {
        m_map.insert(std::make_pair(key, value));
    }
    void erase(K key)
    {
        m_map.erase(key);
    }
    V &operator[](K key)
    {
        return m_map[key];
    }
};

int main()
{
    MyMap<int, char> m1;
    MyMap<int, std::string> m2;
    MyMap<std::string, char> m3;
    MyMap<std::string, std::string> m4;
    MyMap<IntK, CharV> m5;
    m5.insert(IntK(1), CharV());
    auto v = m5[1];
    m5.erase(1);
    return 0;
}

If this test runs successfully without any compilation errors, it’s an encouraging indication that MyMap does not presume any additional properties on K and V. Note that we had to actually call methods from MyMap class in the test to ensure that the compiler checks those functions.

Nevertheless, bear in mind that this method isn’t infallible. It requires diligent test writing with mock types, and there’s a chance that some required properties might be overlooked. However, it’s an excellent starting point to ensure your implementation doesn’t assume any additional properties on K and V than required.

To look at a practical example where I used this technique, check out my implementation of an interval map.