Search code examples
c++templatesc++20c++-conceptstype-constraints

C++20 requires expression does not catch static_assert


I was really excited when I first heard about C++20 constraints and concepts, and so far I've been having a lot of fun testing them out. Recently, I wanted to see if it's possible to use C++20 concepts to test the constraints of classes or functions. For example:

template <int N>
requires (N > 0)
class MyArray { ... };

template <int N>
concept my_array_compiles = requires {
  typename MyArray<N>;
};

my_array_compiles<1>;  // true
my_array_compiles<0>;  // false

At first I didn't have any issues, but I encountered a case where static_assert in a dependent function prevents compilation, even though it appears in a requires expression. Here is an example that illustrates this:

template <bool b>
requires b
struct TestA {
  void foo() {}
};

template <bool b>
struct TestB {
  static_assert(b);
  void foo() {}
};

template <template<bool> class T, bool b>
concept can_foo = requires (T<b> test) {
  test.foo();
};

can_foo<TestA, true>;   // true
can_foo<TestA, false>;  // false
can_foo<TestB, true>;   // true
// can_foo<TestB, false>; does not compile

TestA and TestB should work similarly for most use cases (although I found that TestB<false> can be used as a type as long as it isn't instantiated or dereferenced). However, my expectation was that a failed static_assert within a requires expression would cause it to evaluate to false instead. This is especially important for using library code that still uses static_assert. For example, std::tuple_element:

template <class T>
concept has_element_0 = requires {
    typename tuple_element_t<0, T>;
};

has_element_0<tuple<int>>;  // true
// has_element_0<tuple<>>; does not compile

When I pass in an empty tuple to the above concept, I get the error static_assert failed due to requirement '0UL < sizeof...(_Types)' "tuple_element index out of range". I've tested this on g++ 10.3.0 and clang 12.0.5. I was able to work around this issue by providing a wrapper that uses constraints, but it somewhat defeats the purpose since I am essentially preventing the compiler from seeing the static_assert by enforcing the same condition at a higher level.

template <size_t I, class T>
requires (I >= 0) && (I < tuple_size_v<T>)
using Type = tuple_element_t<I, T>;

template <class T>
concept has_element_0 = requires {
    typename Type<0, T>;
};

has_element_0<tuple<int>>;  // true
has_element_0<tuple<>>;     // false

And it doesn't always work depending on how std::tuple_element is used:

template <size_t I, class T>
requires (I >= 0) && (I < tuple_size_v<T>)
tuple_element_t<I, T> myGet(const T& tup) {
    return get<I>(tup);
}

template <class T>
concept has_element_0 = requires (T tup) {
    myGet<0>(tup);
};

has_element_0<tuple<int>>;  // true
// has_element_0<tuple<>>; does not compile

So ultimately my questions are: is this expected behavior that requires expressions don't take static_assert into account? If so, what was the reason for that design? And finally, is there a better way to accomplish my goal on classes with static_assert without using the above workaround?

Thanks for reading.


Solution

  • Yes, nothing in the content of the stuff you interact with is checked. Just the immediate context of the declaration.

    In some cases with decltype the non immediate context of some constructs is checked, but any errors remain hard.

    This was done (way back) to reduce the requirements on compilers. Only in what is known as "immediate context" do the compilers need to be able to cleanly back out when they see an error and continue compiling.

    Static assert is never suitable for this purpose. Static assert, if hit, ends the compilation.