9

In C++11 constexpr functions, a second statement such as an assert() is not possible. A static_assert() is fine, but wouldn't work if the function is called as 'normal' function. The comma operator could come to help wrto. the assert(), but is ugly and some tools spit warnings about it.

Consider such 'getter' which is perfectly constexpr-able beside the assertion. But I would like to keep some kind of assertion for runtime and compile time, but cannot just overload depending on the 'constexpr' context.

template<int Size>
struct Array {
  int m_vals[Size];
  constexpr const int& getElement( int idx ) const
  {
    ASSERT( idx < Size ); // a no-go for constexpr funcs in c++11
    // not possible, even in constexpr calls as being pointed out, but what I would like:
    static_assert( idx < Size, "out-of-bounds" );
    return m_vals[idx];
  }
};

Side conditions: C++11, no heap, no exceptions, no compiler specifics.

Note as commenters pointed out (thanks!), static_assert on the argument is not possible (but would be nice). The compiler gave me a different error on out-of-bounds access in that situation.

Borph
  • 842
  • 1
  • 6
  • 17
  • 4
    Related: [How to use static_assert for constexpr function arguments in C++?](https://stackoverflow.com/q/26072709/580083). – Daniel Langr Jan 23 '20 at 13:49
  • "*but is ugly*" Well, the comma operator may be ugly, but it does the job in C++11. The fancy solution would be to switch to C++14. :) – Acorn Jan 23 '20 at 13:55
  • "*some tools spit warnings about it*" Which tools and which warnings? – Acorn Jan 23 '20 at 13:55
  • Comma operator used to be my solution, but a) is warned about by static code analysis like qacpp (coding guidelines) and b) was leading to a weird syntax error at a downstream project (didn't understand, suspect a custom assert macro). Well I just try to avoid it now, but agree that it did the job. – Borph Jan 23 '20 at 14:02
  • The `static_assert` is not ok here for constant expression calls either. `idx < Size` is never a constant expression and can never be used as such in a `static_assert`. On the other hand the `assert` is ok in a `constexpr` context since resolution of [LWG issue 2234](http://cplusplus.github.io/LWG/lwg-defects.html#2234). – walnut Jan 23 '20 at 14:08
  • @walnut True, overlooked that, was mainly focused on the `assert`. For `static_assert` [this](https://stackoverflow.com/a/37320913) could work. – Borph Jan 23 '20 at 14:17
  • @walnut Also, the project uses an own `ASSERT()` macro instead of standard `assert()`. – Borph Jan 23 '20 at 14:21
  • 2
    @Borph No, you cannot use `static_assert` dependent on `idx` at all. You can only diagnose a wrong value of `idx` if the function is used in a context that requires a constant expression, by forcing the evaluation of a construct that makes it not a constant expression. Outside of such context, you can never check the value at compile-time. – walnut Jan 23 '20 at 14:24
  • @walnut I checked: I originally omited the `static_assert`, because out-of-bounds access gave me anyways a _"read of dereferenced one-past-the-end pointer is not allowed in a constant expression"_ error. – Borph Jan 23 '20 at 14:38
  • got to be c++17 or newer –  Jan 23 '20 at 17:51

3 Answers3

3

Something like

void assert_impl() { assert(false); } // Replace body with own implementation

#ifdef NDEBUG // Replace with own conditional
#define my_assert(condition) ((void)0)
#else
#define my_assert(condition) ((condition) ? (void()) : (assert_impl(), void()))
#endif

template<int Size>
struct Array {
  int m_vals[Size];
  constexpr const int& getElement( int idx ) const
  {
    return my_assert(idx < Size), m_vals[idx];
  }
};

It will give a compile time error on assertion failure if used in a context requiring a constant expression (because it will call a non-constexpr function).

Otherwise it will fail at runtime with a call to assert (or your analogue).

This is the best that you can do as far as I know. There is no way to use the value of idx to force a check at compile-time outside of context requiring constant expressions.

The comma operator syntax is not nice, but C++11 constexpr functions are very limited.

Of course, as you already noted, undefined behavior will be diagnosed anyway if the function is used in a context requiring a constant expression.

If you know that assert (or your analogue) doesn't expand to anything that is forbidden in a constant expression if the condition evaluates to true but does so if it evaluates to false, then you can use it directly instead of my_assert and skip the indirection that I build in my code.

walnut
  • 21,629
  • 4
  • 23
  • 59
  • 1
    Given the downvotes, could someone please explain to me where my answer is wrong? – walnut Jan 23 '20 at 16:28
  • This and @ecatmur solution are similar, I just can choose one answer. Yours is straight-forward. One remark though: why `(void)0` in the `NDEBUG` case and `void()` in the other? Or is it really the same? – Borph Jan 24 '20 at 12:34
  • @Borph `(void)0` is a no-op, it compiles to nothing (which is what you want when `NDEBUG` is defined), while you need the `void()` so that the second and third operands of the conditional operator have the same type, `void`. – Bob__ Jan 24 '20 at 13:58
  • @Borph I think `(void)0` would be fine in all cases as well. I just replaced it in the first case, because `void()` can also be parsed as type of a function without parameters and without return type depending on the context. It cannot be parsed that way in the subexpressions in the second case. – walnut Jan 24 '20 at 15:13
3

static_assert cannot be used here. The argument to a constexpr function is not permitted in a constant-expression. Therefore, there is no solution to your problem under the given constraints.

We can, however, solve the problem by bending two constraints

  1. not using static_assert (use other methods to produce a compile-time diagnostic instead), and

  2. disregard that the comma operator "is ugly and some tools spit warnings about it." (Showing its ugliness is an unfortunate consequence of the strict requirements of C++11 constexpr functions)

Then, we can use a normal assert:

template <int Size>
struct Array {
  int m_vals[Size];
  constexpr const int& getElement(int idx) const
  {
    return assert(idx < Size), m_vals[idx];
  }
};

In a constant evaluation context, this will emit a compiler error like error: call to non-'constexpr' function 'void __assert_fail(const char*, const char*, unsigned int, const char*)'.

L. F.
  • 19,445
  • 8
  • 48
  • 82
3

Better than a comma expression, you can use a ternary conditional. The first operand is your assertion predicate, the second operand is your success expression, and since the third operand can be any expression - even one not usable in a C++11 constant context - you can use a lambda to invoke your library's ASSERT facility:

#define ASSERT_EXPR(pred, success)    \
    ((pred) ?                         \
     (success) :                      \
     [&]() -> decltype((success))     \
     {                                \
         ASSERT(false && (pred));     \
         struct nxg { nxg() {} } nxg; \
         return (success);            \
     }())

Explanation of the body of the lambda:

  • ASSERT(false && (pred)) is to ensure that your assertion machinery is invoked with an appropriate expression (for stringification).
  • struct nxg { nxg() {} } nxg is for future-safety, to ensure that if you compile in C++17 or above with NDEBUG the lambda is still non-constexpr and so the assertion is enforced in const-evaluation context.
  • return (success) is there for two reasons: to ensure that the second and third operands have the same type, and so that if your library respects NDEBUG the success expression is returned regardless of pred. (pred will be evaluated, but you'd hope that assertion predicates should be cheap to evaluate and have no side effects.)

Example of use:

template<int Size>
struct Array {
  int m_vals[Size];
  constexpr int getElement( int idx ) const
  {
    return ASSERT_EXPR(idx < Size, m_vals[idx]);
  }
};

constexpr int I = Array<2>{1, 2}.getElement(1); // OK
constexpr int J = Array<2>{1, 2}.getElement(3); // fails
ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • Ah! @walnut and an uninitialized variable might be allowed later on if it isn't accessed. I'll try to find a better guard. Thanks! – ecatmur Jan 23 '20 at 16:53
  • You are right that `pred` evaluation should be cheap, but they aren't always. So as a general `ASSERT_EXPR` macro I wouldn't recommend. I sometimes do expensive calls in an assert myself (e.g. to check invariants). – Borph Jan 24 '20 at 12:13
  • @ecatmur why is `struct nxg { nxg() {} } nxg` needed? What would go wrong in C++17? It allows more in `constexpr` functions, do you mean the `assert` would be `constexpr` then, too? – Borph Jan 24 '20 at 12:39
  • 1
    @Borph I'm assuming that even if you turn on `NDEBUG` to disable runtime asserts, you still want the compile-time asserts to be checked. Making the body of the failure-case lambda non-`constexpr` is a way to ensure this - but it does have the cost of evaluating and discarding the predicate at runtime in `NDEBUG`. Otherwise you could define the macro under `NDEBUG` to just `return (success);`. – ecatmur Jan 24 '20 at 17:19