Reputation: 1975
Given the following code:
struct A {
constexpr static auto e() { return 0; }
void f(int V1 = e()) { int V2 = e(); }
};
I've got the following clang error:
error: function 'e' with deduced return type cannot be used before it is defined
void f(int V1 = e()) { int V2 = e(); }
~~^~~
My understanding is that both function-body and default argument are complete-class contexts in which the class and its member definitions, in those contexts, are considered completely defined.
Within the function body, I've used the function A::e
as an initializer for variable V2
. There's no error at this point which makes me sure that, within the function body, the function A::e
is considered completely defined.
Within the function parameter list, I've used A::e
as a default argument for parameter V1
. By definition, a default argument is considered a complete-class context. At this point, I'm getting an error stating that A::e
is used before it's defined. But per my understanding, A::e
is considered defined in any complete-class context.
This is somehow weird for me because function bodies and default arguments are complete-class contexts. I really don't see any difference between both because in both contexts, the member function X::e
is fully-defined.
I'm not sure that [dcl.spec.auto]/11 is what causes the error here. It says:
If the name of an entity with an undeduced placeholder type appears in an expression, the program is ill-formed [..]
If [dcl.spec.auto]/11
is what causes the error, the initialization of variable V2
shall also be ill-formed because if, that's the case, the name e
of the initializer e()
is a name of an entity with an undeduced placeholder type.
So, my question is: what is the wording of the standard that prohibits e()
to be a default argument for V1
and, at the same time, allows e()
to be an initializer for V2
?
Upvotes: 11
Views: 1560
Reputation: 119154
There is no obvious reason why, as the OP suggests, being in a complete-class context ought to guarantee that you can use a function with a deduced return type.
The OP writes:
My understanding is that both function-body and default argument are complete-class contexts in which the class and its member definitions, in those contexts, are considered completely defined.
According to [class.mem.general]/7, in a complete-class context, the class is considered a complete type. It says nothing about whether all function bodies are "visible" and thus, return type deduction has been completed. (I am placing this term in scare quotes because I am not using it in a standard way and will not attempt to give a rigorous definition.)
It's true that in the body of a member function of a class, we can call other functions that may have been declared later in the class, and this is made possible by a combination of [class.mem.general]/7 and the name lookup rules ([basic.lookup.unqual]/8). However, this has nothing to do with the body of the callee being "visible". Its definition may even be in another TU, after all.
Being in a complete-class context is clearly not sufficient when there is mutual recursion:
struct B {
static auto e() { return decltype(f())(0); }
static auto f() { return decltype(e())(0); }
};
It's pretty obvious that no compiler will accept the above code. So there must be something more to it than the idea that when you are in a complete-class context, the return types of other member functions that have deduced return types are considered to be known.
In fact, we don't even need mutual recursion. All compilers reject the following:
struct C {
void f() { int V2 = e(); }
static auto e() { return 0; }
};
The foregoing examples suggest that compilers interpret functions defined inside class definitions as follows: hoist the declarations of all class member functions above all definitions (as if all definitions were out-of-line) but maintain the relative ordering between them. That is, the above code is interpreted approximately as if we had a one-pass compiler and the following code:
struct C {
void f();
static auto e();
};
void C::f() { int V2 = e(); }
auto C::e() { return 0; }
When default arguments are involved, where are they defined? If the transformation is as follows, the code ought to be acceptable:
struct A {
constexpr static auto e();
void f(int V1);
};
constexpr auto A::e() { return 0; }
void A::f(int V1 = e()) { int V2 = e(); } // OK
But if a compiler uses the strategy of putting all default arguments first before all function bodies, then we again have a problem:
struct A {
constexpr static auto e();
void f(int V1);
};
void A::f(int V1 = e()); // ill-formed
constexpr auto A::e() { return 0; }
void A::f(int V1) { int V2 = e(); }
The fact that compilers don't accept the OP's code suggests that this latter interpretation corresponds with what is happening internally in those compilers. Why might the Big 4 have all made the choice to put all default arguments first before all function bodies? Well, in C++98, that's a convenient choice. A function body may contain a call to another member function, and that call may use the value of the latter's default argument. But the reverse wasn't true: in C++98, although a default argument's initializer might involve a call to another member function, the body of the latter function is not required to have been seen yet (again, it could even be defined in another TU).
I can hear you saying "hang on, hang on, this is a 'language-lawyer' question! Show me the wording! Where in the standard is this hoisting behaviour specified?"
It isn't. Because in C++98, there was no need to. The standard merely said that the class is regarded as complete inside function bodies (including ctor-initializers) and default arguments ([class.mem]/2) and let compilers figure out what to do. In C++98, the OP's mental model of "this is a complete-class context, so I have full access to the information about the types of all the members" held.
The introduction of constexpr
and auto
in later standards resulted in a wording gap. As the example of B
above shows, the OP's mental model can no longer hold, and instead the well-formedness of the program seems to depend on considerations of whether default arguments precede function bodies or vice versa, with the standard yielding no guidance!
However, there is a solution to this problem. It begins with the observation that all compilers accept the templated version:
template <class T>
struct D {
constexpr static auto e() { return 0; }
void f(int V1 = e()) { }
};
int main() {
D<void>{}.f();
}
Why is it so? It's because members of a class template specialization (NB: D<void>
is technically a "specialization" in the terminology of the standard, even though the class template D
has not been "specialized" for void
) are individually subject to instantiation (which, in general, does not occur at the same time as the instantiation of the class definition itself). When f()
is called, and only then, is D<void>::f
's default argument instantiated, since it is needed at that point (C++20 [temp.inst]/3, [temp.inst]/13). The instantiation of that default argument triggers the instantiation of the definition of D::e
(C++20 [temp.inst]/4). Everything works out when this code is templated, because the standard requires templated entities to be instantiated only when needed, and this occurs regardless of declaration order; the compiler has to resolve the dependencies, so to speak.
This inconsistency is the subject of CWG 2335. The example there is not quite analogous to the OP's example, because static data member initializers are not complete-class contexts. However, the note at the end is very wide-ranging in its implications:
The consensus of CWG was to treat templates and classes the same by "instantiating" delayed-parse regions when they are needed instead of at the end of the class.
What is a "delayed-parse region" in a non-template class? Presumably, it just means a complete-class context. The insight here is that in order to answer questions such as whether OP's code should work, we need to apply the concept of instantiation to non-templates. A complete-class context, such as a function body or a default argument, should be instantiated only when needed. That means that when this core issue is eventually resolved, OP's code will (presumably) become well-formed by analogy to the templated version.
Upvotes: 7