24

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.

Boann
  • 48,794
  • 16
  • 117
  • 146
Piotr Siupa
  • 3,929
  • 2
  • 29
  • 65

3 Answers3

17

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!");
    // ...
}
Barry
  • 286,269
  • 29
  • 621
  • 977
  • 1
    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`. – Piotr Siupa Jul 26 '19 at 20:58
  • 1
    Nevermind, it works if I try to assign it to a constexpr constant. – Piotr Siupa Jul 26 '19 at 21:05
  • 3
    `There is a difference between something being constant evaluated and being usable as a constant-expression.` What's the difference? – Piotr Siupa Jul 26 '19 at 21:09
  • The fact you would have to allow `X{};` doesn't mean you would have to also allow `return X{};`. I don't see a problem here. – Piotr Siupa Jul 26 '19 at 21:16
  • 1
    @NO_NAME It's already `X` that's the problem. And yes, allowing the former necessarily means allowing the latter. – Barry Jul 26 '19 at 21:36
  • 1
    The document about mental model really helped me to understand boundaries between translation and evaluation. I know why `X` 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. – Piotr Siupa Jul 26 '19 at 21:51
  • @Barry 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. – Deduplicator Jul 27 '19 at 12:07
  • @Deduplicator What would be a problem? – Barry Jul 27 '19 at 13:21
  • Taking advantage of the fact that the value of the argument is known at compile-time. – Deduplicator Jul 27 '19 at 13:30
  • @Deduplicator Sure, but the question is specifically about `consteval` where at least it would appear that those concerns don't apply. – Barry Jul 27 '19 at 15:55
  • @Barry As I said. Consider adding that it is an arbitrary limitation to the answer. – Deduplicator Jul 27 '19 at 23:16
  • @Deduplicator What is an arbitrary limitation? I do not think anything here is arbitrary. – Barry Jul 28 '19 at 11:17
  • 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. – Deduplicator Jul 28 '19 at 11:22
  • @Deduplicator It's not arbitrary at all. – Barry Jul 28 '19 at 13:52
  • @Deduplicator 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. – Barry Jul 28 '19 at 14:31
  • 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. – Deduplicator Jul 28 '19 at 15:14
  • 3
    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? – Johannes Schaub - litb Aug 04 '19 at 12:16
  • 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. – NoSenseEtAl Nov 05 '20 at 14:06
  • What's the problem with `foo()` being `template X foo();` with some new deduction guide for functions `foo(int i) -> foo()`? Deduction guides for functions would be nice anyway. – Goswin von Brederlow Jun 04 '22 at 03:00
  • 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, ... – tommsch Jul 31 '23 at 09:49
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.

  • I doesn't answer my question but still, nice trick. – Piotr Siupa Sep 05 '22 at 14:31
  • 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. – starball Sep 05 '22 at 22:02
  • constexpr functions that are constant-evaluated also work. – Yiğit Feb 23 '23 at 09:14
0

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)
Somnium
  • 1,059
  • 1
  • 9
  • 34