Will consteval allow using static_assert on function arguments?
Asked Answered
N

3

30

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.

Niccolo answered 26/7, 2019 at 20:45 Comment(0)
S
23

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!");
    // ...
}
Saurischian answered 26/7, 2019 at 20:47 Comment(20)
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.Niccolo
Nevermind, it works if I try to assign it to a constexpr constant.Niccolo
There is a difference between something being constant evaluated and being usable as a constant-expression. What's the difference?Niccolo
The fact you would have to allow X<i>{}; doesn't mean you would have to also allow return X<i>{};. I don't see a problem here.Niccolo
@NO_NAME It's already X<i> that's the problem. And yes, allowing the former necessarily means allowing the latter.Saurischian
The document about mental model really helped me to understand boundaries between translation and evaluation. I know why X<i> will not be possible. Although, I still don't think it prevents static_assert from being used on consteval function parameters. It could be solved not by allowing them to be used as constant expressions but rather by loosening restrictions on static_assert so it can be used on constant evaluated values. This is possible even withing current language boundaries, as you've shown yourself.Niccolo
@Saurischian It would be a problem if it was in a normal function. Or even in a constexpr-function outside a block restricted to compile-time evaluation.Whitecollar
@Whitecollar What would be a problem?Saurischian
Taking advantage of the fact that the value of the argument is known at compile-time.Whitecollar
@Whitecollar Sure, but the question is specifically about consteval where at least it would appear that those concerns don't apply.Saurischian
@Saurischian As I said. Consider adding that it is an arbitrary limitation to the answer.Whitecollar
@Whitecollar What is an arbitrary limitation? I do not think anything here is arbitrary.Saurischian
That an immediate function cannot use its arguments as non-type template arguments is an arbitrary implementation, as it is restricted to compile-time anyway.Whitecollar
@Whitecollar It's not arbitrary at all.Saurischian
@Whitecollar It's completely consistent with the way everything else in the language works and it's completely consistent with the model for how constant evaluation works. Read Andrew's short paper that I linked.Saurischian
Andrew separates code into two buckets: Compiletime (translation) and runtime (evaluation). Normal functions run at runtime, constexpr both, and consteval only at compiletime. So, using runtime limitations for consteval is very restricting and arbitrary.Whitecollar
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?Barratry
Nice answer, but I think it is not entirely correct. In your example you claim "... you'd have to allow this too" but that is not true. It would be perfectly fine(but inconsistent, and maybe hard to teach) that static_assert can see values of arguments, but that those arguments can not be used as template arguments.Grosso
What's the problem with foo() being template <int j> X<j> foo(); with some new deduction guide for functions foo(int i) -> foo<i>()? Deduction guides for functions would be nice anyway.Jaclin
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, ...Toffic
U
2

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.

Unmoving answered 5/9, 2022 at 12:33 Comment(3)
I doesn't answer my question but still, nice trick.Niccolo
agreed- nice trick! But addressing an example of a wider question is not the same as addressing the wider question. Either come up with a generalization of the trick that works for all cases of the question (good luck?), or delete your answer, or at least acknowledge in the answer that it doesn't answer the full question and only addresses the example.Magnificat
constexpr functions that are constant-evaluated also work.Hyohyoid
I
1

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)
Inhospitable answered 31/5, 2023 at 5:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.