Baduit

A young French developper who really likes (modern) C++

About me
24 October 2021

What if I told you, you don't have to run your unit tests?

by Baduit

Intro

Don’t burn me at the stake (yet), I’m not saying that you should not write unit tests, they are important, I’m just saying that in some case, you never have to run them. Let me explain why.

The anecdote

Few weeks ago I had to write a function to concatenate 2 std::array into one, the function is quite simple and look like this :

1
2
3
4
5
6
7
8
9
10
11
12
#include <type_traits>
#include <array>
#include <algorithm>

template <typename T, std::size_t aSize, std::size_t bSize>
constexpr auto concat_array(const std::array<T, aSize>& a, const std::array<T, bSize>& b)
{
	std::array<T, aSize + bSize> result_array;
	std::ranges::copy(a, result_array.begin());
	std::ranges::copy(b, result_array.begin() + aSize);
	return result_array;
}

It just takes two std::array of the same type, create a new std::array with whose size is that of the two other std::array added together, then it copies the content of the first one, then the content of the second one.

I have written my function, now it’s time to write the tests, some could argue that I should have written them before and use tdd, but I didn’t and that’s not the point of this article at all. For this article simplicity’s sake, I will only show my first test and it looked like this :

1
2
3
4
5
6
7
8
// In reality I use Doctest instead of just some assert, but it is simpler to show it this way
int basic_test()
{
    const std::array<int, 3> a = { 1, 2, 3 };
    const std::array<int, 2> b = { 1, 2 };
    const auto res = concat_array(a, b);
    assert((res == std::array<int, 5>{ 1, 2, 3, 1, 2 }));
}

My test pass, I’m happy and was ready to go on with my life write more tests, but suddenly I thought : “My function is marked constexpr, this means that it can be computed during compilation time, so instead of making an assert, I can make a static_assert”. So, I did that :

1
2
3
4
constexpr std::array<int, 3> a = { 1, 2, 3 };
constexpr std::array<int, 2> b = { 1, 2 };
constexpr auto res = concat_array(a, b);
static_assert(res == std::array<int, 5>{ 1, 2, 3, 1, 2 });

It compiled without any error, it meant that my test passed, and I didn’t even have to run my test, I just need to use my compiler and it runs the test for me. It works for all code that can run during compilation (constexpr functions, consteval functions, template stuffs, etc).

Constexpr as much as possible

I hear you say that not all code can run during compilation, well now with C++ 20 you can do a lot of stuff (even more when C++23 will be here). I mean, you can for example allocate memory with new, yes, you can use std::vector and std::string in your constexpr function. You even can have a constexpr virtual method.

Let’s have create a function adding all digit contained in an ascii string:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <vector>
#include <string_view>
#include <numeric>

// std::isdigit is not constexpr
constexpr bool is_digit(char c)
{
    return c >= '0' && c <= '9';
}

constexpr unsigned int accumulate_string_digits(std::string_view str)
{
    std::vector<unsigned int> digits;
    for (auto c: str)
    {
        if (is_digit(c))
            digits.push_back(c - 48);
    }
    return std::accumulate(digits.begin(), digits.end(), 0);
}

We could have easily done this without creating a std::vector but then it would not fit my example.

And now the tests:

1
2
3
4
5
static_assert(accumulate_string_digits("") == 0);
static_assert(accumulate_string_digits("1") == 1);
static_assert(accumulate_string_digits("12345") == 15);
static_assert(accumulate_string_digits("1a23c45c") == 15);
static_assert(accumulate_string_digits("Hello, World!") == 0);

It works ! Unfornately for now only with a recent version of msvc (the compiler of Microsoft shipped with Visual Studio), Clang and Gcc did not implement constexpr std::vector yet.

Conclusion

When I said that you don’t have to run your unit tests I twisted a bit the truth, you can just sometimes let the compiler run them for you during compilation time. Also a lot of code can’t be constexpr, even with C++20 (or C++23 in the future) so this does not apply to all your code, but when it is possible, it is a powerful tool!

From now on that’s what I do when I have a very recent compiler available. I keep these tests in a different file with my classic runtime unit tests to not increase the compilation time too much.

Sources

tags: C++