user8044236
user8044236

Reputation:

Static assert that passes if expression is not known at compile time

I want to implement my_static_assert that slightly differs from c++17 single-parametric static_assert: if condition inside my_static_assert is not known at compile time, it should pass.

The second my_static_assert in the following example should pass but if I would use static_assert it will fail.

#include <iostream>

int x, y;
constexpr int f1() { return 0; }
constexpr int f2() { return 0; }
int f3() { return x; }
int f4() { return y; }
constexpr int sum(int a, int b) { return a + b; }

int main() {
    std::cin >> x >> y;

    // it should fail 
    my_static_assert(sum(sum(f1(), f2()), sum(f1(), f1())) != 0);

    // it should pass
    my_static_assert(sum(sum(f1(), f2()), sum(f4(), sum(f3(), f1()))) != 0);
}

If you want to know why this question arised:

I am building expressions using leaf functions f1,f2,f3,f4 and operations on the expression nodes: sum,mul,div,sub. Leafs that are known at compile time contain value that is always 0.

I am trying to check that my expressions contain at least one element that is not known at compile time.

Upvotes: 11

Views: 251

Answers (1)

If you are willing to commit to a compiler with GNU extensions, it's possible. So be warned.

It takes two overloads and a helper macro:

template<std::size_t N>
constexpr void assert_helpr(int(&)[N]) = delete;

void assert_helpr(...) {}

#define my_static_assert(...) do {               \
    __extension__ int _tmp [(__VA_ARGS__) + 1];  \
    assert_helpr(_tmp);                          \
} while(0)

It works as follows:

  1. The macro grabs your token soup and treats it like an integral expression. +1 is to avoid pathological zero cases.
  2. If the expression is a constant expression, we get a standard array. Otherwise we get a VLA.
  3. Now we call one of two overloaded functions. Overload resolution will always choose a c var-arg function last. If what we got was a regular array, the first, deleted overload is chosen, and the assertion fails.
  4. If we got a VLA, the second overload is chosen and the assertion passes.

I haven't tested it thoroughly, but it seems to work on both Clang and GCC. See it live.


@Artyer was kind enough to share a solution based on this approach on godbolt.

Here's a reduction of that code to this problem:

template<std::size_t N>
constexpr std::false_type assert_helpr(int(&)[N]);
constexpr std::true_type  assert_helpr(...);

#define my_static_assert(...) do {                               \
    __extension__ int _tmp[(__VA_ARGS__) + 1];                   \
    static_assert(decltype(assert_helpr(_tmp)){}, #__VA_ARGS__); \
} while(0)

Same use of overloading, except this time we grab an actual result type from the call via decltype, and proceed to create a true compile time Boolean constant out of it.

This allows direct use of static_assert, and as a nice to have feature, we can pass it the stringified token soup to get an indication in the error of the expression that failed.

Upvotes: 7

Related Questions