g++ doesn't compile constexpr function with as

2019-01-22 23:38发布

template<typename T> constexpr inline 
T getClamped(const T& mValue, const T& mMin, const T& mMax) 
{ 
     assert(mMin < mMax); // remove this line to successfully compile
     return mValue < mMin ? mMin : (mValue > mMax ? mMax : mValue); 
}

error: body of constexpr function 'constexpr T getClamped(const T&, const T&, const T&) [with T = long unsigned int]' not a return-statement

Using g++ 4.8.1. clang++ 3.4 doesn't complain.

Who is right here? Any way I can make g++ compile the code without using macros?

4条回答
一夜七次
2楼-- · 2019-01-22 23:46

g++ is right. Per the standard, a non-static assert is not permitted in a constexpr statement.

... its function-body shall be a compound-statement that contains only:
  null statements,
  static_assert-declarations,
  typedef declarations and alias-declarations that do not define classes or enumerations,
  using-declarations,
  using-directives,
  and exactly one return statement.
      -- 7.1.5/3

查看更多
Melony?
3楼-- · 2019-01-22 23:55

GCC is right. However, there is a relatively simple workaround:

#include "assert.h"

inline void assert_helper( bool test ) {
  assert(test);
}
inline constexpr bool constexpr_assert( bool test ) {
  return test?true:(assert_helper(test),false);
}

template<typename T> constexpr
inline T getClamped(const T& mValue, const T& mMin, const T& mMax)
{
  return constexpr_assert(mMin < mMax), (mValue < mMin ? mMin : (mValue > mMax ? mMax : mValue));
}

where we abuse the comma operator, twice.

The first time because we want to have an assert that, when true, can be called from a constexpr function. The second, so we can chain two functions into a single constexpr function.

As a side benefit, if the constexpr_assert expression cannot be verified to be true at compile time, then the getClamped function is not constexpr.

The assert_helper exists because the contents of assert are implementation defined when NDEBUG is true, so we cannot embed it into an expression (it could be a statement, not an expression). It also guarantees that a failed constexpr_assert fails to be constexpr even if assert is constexpr (say, when NDEBUG is false).

A downside to all of this is that your assert fires not at the line where the problem occurs, but 2 calls deeper.

查看更多
劫难
4楼-- · 2019-01-23 00:03

constexpr calculates in compile time. Non static assert in run-time.

查看更多
beautiful°
5楼-- · 2019-01-23 00:04

As of C++14, this is no longer an issue; g++ with the -std=c++14 flag compiles and runs your code just fine.

There are three drawbacks:

  • As noted in your question, this does not work in C++11.
  • The assert will, of course, never by triggered at compile time. Even adding a static_assert with the same condition won't work, since mMin and mMax are not considered constant expressions.
  • Moreover, because the assert isn't triggered at compile time, but the function is constexpr, if the condition is false but the expression is evaluated at compile time (e.g. constexpr auto foo = getClamped(1,2,0);), the assert will never fire--meaning that the incorrect function arguments will not be caught.

In a comment, user oliora links to an interesting blog post by Eric Niebler that describes multiple approaches that work in C++11 and can be triggered while compiling or at runtime as appropriate.

In short, the strategies are:

  • throw an exception; to make it uncatchable (i.e. more like an assert), mark the constexpr function nothrow
    • Niebler does not call this out in his post, but the throw expression must be wrapped in some kind of larger logical expression that is only evaluated if the condition being asserted is false, such as a ternary expression (which is what Niebler uses in his example). A standalone if (condition) throw <exception>; statement will not be permitted, even in C++14.
    • Niebler also fails to note that, unlike assert, this approach does not depend on NDEBUG; release builds will trigger failures and crash.
  • Throw a custom expression type whose constructor invokes std::quick_exit. This eliminates the need for nothrow.
    • Again, this won't be compiled out for release builds (unless you wrap the quick_exit call in ifdef's).
  • Wrap an actual assert inside a lambda, which is passed to a struct that takes an arbitrary callable (as a template parameter) and invokes it and then calls std::quick_exit, and then throw that struct. This one seems like severe overkill, but of course it generates a true assertion-failure message at runtime, which is nice.
    • This is the only approach that will not cause a release build to crash.
    • oliora provides a variation of this approach without the throw and the quick_exit. This seems much cleaner and saner.
查看更多
登录 后发表回答