36

Currently you cannot use static_assert to verify parameters of a constexpr function, even if all calls to it are indeed constexpr. That makes sense because the compiler still has to create a non-constexpr instantiation of this function in case some other module will try to call it. Sadly, this is the case even if the function is static or in an anonymous namespace.

C++20 however, will introduce a new keyword consteval which is like constexpr but it doesn't allow calling a function in a non-constexpr way. In this case, the compiler can know for sure that the function parameters will always be known at compile time. Therefore, in theory it should be possible to validate them with static_assert.

The question is: Does the standard allow it?


Example:

#include <iostream>

consteval char operator""_bchar(const char text[], const size_t length)
{
    static_assert(length == 8, "Binary char has to have 8 digits!"); // <-- This is currently not possible.
    uint8_t byte = 0;
    for (size_t i = 0; i != length; ++i)
    {
        byte <<= 1;
        byte |= text[i] == '1' ? 0b00000001 : 0b00000000;
    }
    return byte;
}

int main()
{
    std::cout << "01000001"_bchar << std::endl;
    return 0;
}

I'm asking because I'm going to write some user-defined literals (more complicated than the example). I have an option to use compiler extensions to deal with the validation or wait a little for the compiler update and write fully standard-compliant code.

1

4 Answers 4

28

Will consteval allow to use static_assert on function arguments?

No. Function arguments have never been, and will continue to not be, usable as constant expressions.

There is a difference between something being constant evaluated and being usable as a constant-expression. consteval ensures that we're in a constant evaluation context, but it does not also cause everything to become constant-expressions.

In order to allow function arguments to be usable as constant expressions, you would need to make everything implicitly a template:

template <int> struct X { };

consteval auto foo(int i) {
    static_assert(i > 10); // in order to allow this...
    return X<i>{};         // ... you'd have to allow this too
}

And now foo(20) and foo(30) return different types. That's a template.


Important background reading for understanding why this is a fundamental and inherent limitation can be found in Andrew Sutton's Translation and evaluation: A mental model for compile-time metaprogramming:

Having a mental model of compile-time evaluation that physically separates it from the process of translation has been extremely helpful for me. In particular, it has helped me understand what is not possible (e.g., instantiating a template during evaluation). This helps prune the design space for otherwise large and complex language features. Hopefully, others will find this note helpful as well.


With static_assert specifically though, you can add a workaround just to cause a compilation failure. That's just adding anything at all that can't be used during constant evaluation. Like:

#define CONSTEVAL_STATIC_ASSERT(c, msg) do { if (!(c)) throw msg; } while(false)

as in:

consteval char operator""_bchar(const char text[], const size_t length)
{
    CONSTEVAL_STATIC_ASSERT(length == 8, "Binary char has to have 8 digits!");
    // ...
}
Sign up to request clarification or add additional context in comments.

24 Comments

Your CONSTEVAL_STATIC_ASSERT does not fail at compile-time. It throws the exception at run-time. I use g++ (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0.
There is a difference between something being constant evaluated and being usable as a constant-expression. What's the difference?
@NO_NAME It's already X<i> that's the problem. And yes, allowing the former necessarily means allowing the latter.
Hmm, I suppose that consteval functions are already required to have their code be present on call time. So what's the problem of implicitly making them templates?
This is just one more reason why C++ is unnecssarily complicated. If C++ would follow the model: "If it is constant, one can use it as a constant expression" much things would be much easier: No need for constexpr, consteval, static const, static_assert, ...
|
5

I agree w/ commenters above - it's not possible to use static_assert() w/ function arguments, but, it's still possible to trigger compile error in consteval function on argument condition. I.e. to get the same effect static_assert is designed for.

consteval char operator""_bchar(const char text[], size_t length)
{
    //static_assert(length == 8, "Binary char has to have 8 digits!");
    length /= (length == 8); // Binary char has to have 8 digits!
}

The trick is (length != 8) triggers division by zero which is not constant expression.

Compile error would look like (gcc-11):

test.cpp: In function ‘int main()’:
test.cpp:110:18:   in ‘constexpr’ expansion of ‘operator""_bchar(((const char*)"12345"), 5)’
test.cpp:104:12: error: ‘(5 / 0)’ is not a constant expression
   104 |     length /= (length == 8); // Binary char has to have 8 digits!
       |     ~~~~~~~^~~~~~~~~~~~~~~~

!!!WARNING,WARNING,WARNING!!!: it works in consteval functions ONLY. If used in constexpr functons, your program will be killed w/ division by zero error. Use assert() instead or throw exception.

2 Comments

I doesn't answer my question but still, nice trick.
constexpr functions that are constant-evaluated also work.
2

I have modified technique from @Barry's answer to work in all consteval, constexpr and regular functions.

#include <assert.h>
#include <type_traits>

#define constexpr_assert(expression) do{if(std::is_constant_evaluated()){if(!(expression))throw 1;}else{assert(!!(expression));}}while(0)

When support for C++23 will be better, it will be possible to use if consteval to improve it a bit:

#include <assert.h>

#define constexpr_assert(expression) do{if consteval{if(!(expression))throw 1;}else{assert(!!(expression));}}while(0)

Comments

0
  • The solution using throw is restricted to systems where exceptions are generally enabled. Else the compiler/linker will complain even if the branch is not evaluated.
  • The solution using division by zero may always issue a warning, even if the branch is not evaluated. Else an error.

Here another workaround for asserting a condition for consteval function parameters:

#include <cstdlib>

/**
 * @brief A poor-mans static assertion
 *
 * Will create a compile error in case condition is not true.
 *
 * @param condition will be tested for true
 */
consteval void consteval_assert(const bool condition)
{
    if (!condition)
    {
        // compilation of this branch will only be attempted if condition is false
        std::abort(); // this will not compile, as function requires constant expressions
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.