Logo

Blog


The difference between static_assert and C++20's requires

Over this year, I gave various presentations and classes about C++20's Concepts. In today's post, I like to discuss the difference between a static_assert and a Concept, or better, requires.

I previously wrote about C++20's Concepts. For reference, these are my previous posts about C++20 Concepts:

This post is motivated by my talk "C++20 Templates: The next level - Concepts and more".

There I start with the task of writing a function Add which adds an arbitrary amount of values together. One restriction of Add is that the function should work only with values of the same type.

A possible C++17 solution

My solution in C++17 is the following:

1
2
3
4
5
6
template<typename... Args>
std::enable_if_t<are_same_v<Args...>, first_arg_t<Args...>>
Add(Args&&... args) noexcept
{
  return (... + args);
}

This solution is based on two helpers, are_same_v, which checks whether all types in a parameter pack are of the same type. The second helper is first_arg_t which essentially grabs the first parameter of a parameter pack. Since all types are the same, this is what are_same_v checks, the first type is equal to all others. Below you find the helpers for completeness.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
template<typename T, typename... Ts>
constexpr inline bool are_same_v = std::conjunction_v<std::is_same<T, Ts>...>;

template<typename T, typename...>
struct first_arg
{
  using type = T;
};

template<typename... Args>
using first_arg_t = typename first_arg<Args...>::type;

A possible C++20 solution

Now, using C++20, my solution is the following:

1
2
3
4
5
6
7
template<typename... Args>
A Requires-clause using are_same_v to ensure all Args are of the same type.
requires are_same_v<Args...> 
auto Add(Args&&... args) noexcept
{
  return (... + args);
}

As you can see, I only need the are_same_v helper.

A solution using static_assert

All right, this is just to get you on the same page. I'm aware that there are several other possible solutions out there. What I dislike about the C++17 approach is the enable_if_t - way too complicated. For the full picture, feel free to watch my talk. Today I like to focus on an alternative C++17 implementation without enable_if:

1
2
3
4
5
6
template<typename... Args>
auto Add(Args&&... args) noexcept
{
  static_assert(are_same_v<Args...>);
  return (... + args);
}

That solution looks a little less scary. Much like the C++20 version, it only requires are_same_v as a helper. I could also use the optional message of the static_assert to generate a, hopefully, meaningful message for users.

Comparing static_assert to requires

While this C++17 solution looks good, there is a huge difference between it and the C++20 approach: static_assert is hidden inside Add. We are looking at a very small example here, only two lines of code in the body, something you most likely do not have that often in your real-world code. The deeper the static_assert is hidden, the worse it is. This static_assert models a requirement for Add. As a user, I want to know such a requirement upfront. Regardless of how nice you formulated the optional message, I will not be thrilled if that static_assert fires. This approach also makes it impossible to provide an overload to Add, which would treat different types.

The C++20 solution states the requirement clearly in its function signature as my initial C++17 version does. While sometimes the initial C++17 version looks too scary and may feel too complicated to write, C++20 gives us an easy way to express our intent. Beyond that, C++20 allows us to express the difference between a requirement and an assertion.

Express the difference between a requirement and an assertion

In C++20, use concepts or requires-clause as early as possible. Replace class / typename with a concept if possible. Use a requires-clause as a fallback. That way the requirements are stated clearly for users without a need to read the function body and spot limitations there.

Use static_assert for assertions that should not occur on a usual basis. Something that may depend on the system the program is compiled for or similar things that are less related to the template type.

Andreas