Does the default argument for a function parameter is considered as an initializer for that parameter?

Viewed 79

Suppose I have function declarations like these:

static const int R = 0;
static const int I = 0;

void f(const int& r = R);
void g(int i = I);

Per [dcl.fct.default]/1:

If an initializer-clause is specified in a parameter-declaration this initializer-clause is used as a default argument [..]

and per the grammar constructs, an initializer can comprise an initializer-clause. Right?

So I concluded that R is an initializer for the parameter r, and I is also an initializer for the parameter i.

Now per [const.expr]/2:

A variable or temporary object o is constant-initialized if

  • (2.1) either it has an initializer [..] and
  • (2.2) the full-expression of its initialization is a constant expression [..]

So both parameters have an initializer and also the full-expression of their initializations is a constant expression.

So, Are both parameters r and i considered constant-initialized?

1 Answers

No, because an initializer-clause is not necessarily part of an initializer. The grammar item in question here is this version of the parameter-declaration:

attribute-specifier-seqopt thisopt decl-specifier-seq declarator = initializer-clause

Applying just the right amount of hair-splitting, this means that the form of a parameter declaration with a default argument is similar to an initialization, but it is still subtly different. The most obvious difference being that the default argument is ignored if an actual argument is used. Less obvious is that a parameter cannot actually be constant-initialized, even if its default argument is a constant expression. This remains true even if the function is only evaluated at compile time. Here is some code that shows the subtle differences in meaning:

#include <random>

// R may be evaluated at compile time and must be constant-initialized
static constexpr int R = 0;

// f may be evaluated at compile time
constexpr int f(const int& r = R) { return r + 42; }

// I must be constant- *or* zero-initialized, even if only evaluated at runtime
constinit const int I = f();

// g is an "immediate function": it may not be evaluated at runtime
consteval int g(int i = I) { return i - 42; }

int main() {
    // A variable that may not appear in constant expressions,
    // because it is not constant-initialized
    const int not_const = std::rand();

    int x1 = f();                       // OK, constant initialization
    int x2 = f(not_const);              // OK, evaluated at runtime
    constexpr int x3 = f();             // OK
    // constexpr int x4 = f(not_const); // error, must be constant-initialized

    int y1 = g();                       // OK
    // int y2 = g(not_const);           // error
    constexpr int y3 = g();             // OK
    // constexpr int y4 = g(not_const); // error
}

As you can see here, this generates only one runtime call to f(int const&) and none to g(int). Even though the parameters for the other calls are effectively compile-time constants, you can neither use nor specify them as such:

constexpr int f(const int& r = R) {
    // constexpr int x = r; // error, not a constant expression
    return r + 42;
}

consteval int g(int i = I) {
    // constexpr int y = i; // still not a constant expression!
    return i - 42; 
}

// Also not allowed:
// consteval void h(constexpr int x = 0, constinit int y = 1); 
Related