Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to dispatch between assert() and static_assert(), dependend if in constexpr context?

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.

like image 534
Borph Avatar asked Jan 23 '20 13:01

Borph


3 Answers

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.

like image 117
walnut Avatar answered Nov 15 '22 04:11

walnut


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*)'.

like image 38
L. F. Avatar answered Nov 15 '22 03:11

L. F.


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
like image 39
ecatmur Avatar answered Nov 15 '22 02:11

ecatmur