ISO/IEC JTC1 SC22 WG21
N3444=12-0134
Richard Smith
2012-09-21

Relaxing syntactic constraints on constexpr functions

Background

This paper suggests the relaxation of a number of syntactic restrictions for constexpr function definitions. Prior to N3268, the body of a constexpr function was required to be of the form

{ return expression; }

N3268 loosened up the rules to allow (7.1.5/3):

However, the syntactic constraints are still extremely restrictive, and the lack of expressiveness is a common complaint directed at the constexpr feature.

Problem

Consider this simple integer pow function:

// Compute a to the power of n
int pow(int a, int n) {
  if (n < 0)
    throw std::range_error("negative exponent for integer power");
  if (n == 0)
    return 1;
  int sqrt = pow(a, n/2);
  int result = sqrt * sqrt;
  if (n % 2)
    return result * a;
  return result;
}

For this function, and many others like it, it is sometimes desirable to allow compile time evaluation (for use in array bounds or enumerators). However, under the current language rules, we cannot mark this function as constexpr without completely rewriting it:

constexpr int pow_helper(int a, int n, int sqrt) {
  return sqrt * sqrt * ((n % 2) ? a : 1);
}
// Compute a to the power of n
constexpr int pow(int a, int n) {
  return (n < 0) ? throw std::range_error("negative exponent for integer power") :
         (n == 0) ? 1 : pow_helper(a, n, pow(a, n/2));
}

Such rewrites can be performed mechanically by the programmer to remove the uses of local variable declarations and if statements from functions, and thus allow them to be marked constexpr. However, this makes the resulting code harder to read and understand, and forces an awkward coding style on the programmer. There is no reason to require the programmer to go to this effort.

Note that a helper function is required in the rewrite, to avoid computing sqrt multiple times. This is necessary even for implementations which aggressively cache constexpr evaluations, in order to give good performance if the code is used in a context which is not evaluated at translation time.

Solution

Allow arbitrary code in constexpr function definitions, with three exceptions:

Specific changes

This paper proposes applying the following changes. These choices are largely independent. All references to constexpr functions here also apply to constexpr constructors, unless otherwise indicated.

Local variable declarations

Declarations of variables with automatic storage duration will be permitted within constexpr function definitions, so long as they have literal type and, if a constructor is called to perform their initialization, that constructor is a constexpr constructor. These can currently be simulated using a helper function that binds the variables to parameters, and the behavior of local variables would match the behavior of such parameters. In particular, there is no requirement that the variables be const, but any attempt to modify them would cause constant evaluation to fail (so the call will be deferred to runtime).

Static variable declarations

Declarations of variables with static and thread storage duration will be permitted within constexpr functions, with some restrictions. Firstly, such a variable cannot have dynamic initialization. If it did, the initial value of the variable could depend on the order in which the implementation chose to evaluate constexpr function calls:

constexpr int first_val(int n) {
  static int value = n; // error: not constant initialization
  return value;
}
const int N = first_val(5);
int arr[first_val(10)];

Secondly, such a variable cannot have a non-trivial destructor. This allows an implementation to evaluate a constexpr function call at will, without any concern about whether such evaluation causes a side-effect at program termination.

In all other respects, such static or thread_local variables can be used within constexpr functions in the same ways that they could be used if they were declared outside the function. In particular, they do not need to be constexpr nor have a literal type if their value is not used:

constexpr mutex &get_mutex(bool which) {
  static mutex m1, m2; // non-const, non-literal, ok
  if (which)
    return m1;
  else
    return m2;
}

Type definitions

Type definitions, including enum and class definitions, will be permitted without restriction within constexpr functions. There appear to be no implementation or practical reasons which warrant their restriction, and they can be useful for tracking local state of a constexpr function, or as a comparator or similar object to be used as an argument to a suitable constexpr algorithm.

constexpr algo &min_by_cost(algo *begin, algo *end) {
  struct comparator {
    constexpr bool operator()(const algo &a, const algo &b) const {
      return a.cost() < b.cost();
    }
  };
  return min_by(begin, end, comparator());
}

constexpr algo &algo_for_this_platform = min_by_cost(algos, algos + num_algos);

This example would also benefit from lambdas being permitted in constexpr functions, but that is not proposed in this paper.

if statements and multiple return statements

if-statements will be permitted, as a more natural syntactic alternative to the existing support for ? : expressions. The rule that a constexpr function must have exactly one return statement will be relaxed to requiring at least one return statement, so that each branch of an if-statement can return a value. For constexpr constructors, any number of return statements will be permitted.

If a control flow path is taken through a constexpr function which does not reach a return statement or a throw expression, the behavior of the program is undefined, so constant evaluation fails.

Compound statements

compound-statements will be permitted, in order to allow if-statements to control complex computations involving variable declarations.

Expression statements

expression-statements will be permitted. Since no side-effects can occur inside a constant expression, the only effect of an expression-statement during function invocation substitution can be to render the expression non-constant. However, that effect can be desirable in some cases. For instance:

template<typename T, size_t N>
constexpr typename array<T, N>::const_reference array<T, N>::at(size_type n) const {
  if (n >= N)
    throw std::out_of_range("array::at");
  return elems[n];
}

constexpr array<int, 3> arr = { 0, 1, 2 };
enum E { e = arr.at(5) }; // error at compile time
int f() { return arr.at(5); } // exception at runtime

An arbitrary expression-statement is permitted, in order to allow calls to functions performing checks and to allow assert-like constructs. void also becomes a literal type, so that constexpr functions which exist only to perform such checks may return void.

#define ASSERT(expr) \
  (void)((expr) || assert_failed(#expr, __LINE__, __FILE__))
void assert_failed(...); // not constexpr
struct S {
  std::array a<int, 100>;
  size_t i;

  constexpr void check_invariants() const {
    ASSERT(i < a.size());
    ASSERT(a[i] == 0);
  }
  S(std::array<int, 100> a_, size_t i_) : a(a_), i(i_) {
    check_invariants();
  }
};

Semantics

In each case, the construct behaves identically within function invocation substitution as it would if the function were evaluated at runtime. The values of local variables are substituted into expressions prior to their evaluation, in the same manner that function argument values are substituted for references to the function parameters. If any evaluated expression is not a core constant expression after substitution, the function call is also not a core constant expression.

Implementation

An implementation of an earlier revision of this proposal is available here.