Reputation: 2072
C23 §6.2.1(7) contains some new wording related to underspecified declarations (highlighted in bold below):
Structure, union, and enumeration tags have scope that begins just after the appearance of the tag in a type specifier that declares the tag. Each enumeration constant has scope that begins just after the appearance of its defining enumerator in an enumerator list. An ordinary identifier that has an underspecified definition has scope that starts when the definition is completed; if the same ordinary identifier declares another entity with a scope that encloses the current block, that declaration is hidden as soon as the inner declarator is completed.[footnote 19] Any other identifier has scope that begins just after the completion of its declarator.
Footnote 19 says:
That means that the outer declaration is not visible for the initialiser.
An underspecified declaration is a declaration which contains no type specifiers or that is declared with constexpr
(C23 §6.7.1(12)).
I was wondering why the scope of an underspecified declaration begins after completion of its definition, rather than after completion of its declarator, as is the case for ordinary declarations that are not underspecified. According to N2952, this rule is required in order to avoid a "cycle in the type or value dependency" (see second bullet point of 3. Common syntax requirements, and the proposed solution in 5.2 Scope of identifiers).
Question 1 - What does the paper mean by "a cycle in type or value dependency"?
I think I understand the problem of a cycle in the type dependency: If an underspecified declaration is missing a type specifier, then its type must be inferred from an initialiser expression. If the initialiser contains references to the underspecified declaration, e.g. auto foo = foo * 2;
, then we end up with a cyclic type dependency, where the type of the underspecified declaration depends on the type of the initialiser expression, which in turn depends on the type of the underspecified declaration because of the references. This makes it impossible to infer the type of the underspecified declaration.
But what is the problem of a cycle in the value dependency? I'm guessing this has to with constexpr
declarations. In the case of ordinary declarations that are not underspecified, the scope of the identifier begins just after completion of the declarator, so we are free to use it in the initialiser, e.g. int bar = bar * 2;
. This is ok, because the initialiser expression, bar * 2
, is evaluated at runtime and the storage duration rules ensure that bar
exists in memory by the time the initialiser is evaluated. However, in the case of a constexpr
declaration, e.g. constexpr int baz = baz * 2;
the initialiser must be evaluated at compile-time. But baz
does not yet exist at compile-time, which makes it impossible for the compiler evaluate the initialiser.
Question 2 - Why are outer declarations hidden as soon as the declarator is complete?
According to the excerpt above, an underspecified declaration hides outer declarations as soon as its declarator is complete. This means that an underspecified declaration starts shadowing outer declarations before it comes into scope (at the completion of its definition). By contrast, non-underspecified ordinary declarations start shadowing outer declarations at the same time as they come into scope (at the completion of its declarator). Why don't underspecified declarations do the same?
Question 3 - In what sense are constexpr
declarations "underspecified"?
By definition, there are two types of underspecified declarations: 1) those that are missing a type specifier, and 2) those that are declared with constexpr
. This means that a constexpr
declaration that contains a type specifier is still "underspecified", but in what way? My guess is that the term "underspecified" was extended to include such declarations in order to reflect the fact that they have similar syntactic requirements to the first kind of underspecified declaration, i.e. those that are missing type information (similar to the way in which typedef
isn't really a storage class specifier, but is classed as such in the grammar for syntactic convenience). Is this correct?
Upvotes: 3
Views: 103
Reputation: 223795
Question 1 - What does the paper mean by "a cycle in type or value dependency"?
I think I understand the problem of a cycle in the type dependency: If an underspecified declaration is missing a type specifier, then its type must be inferred from an initialiser expression. If the initialiser contains references to the underspecified declaration, e.g.
auto foo = foo * 2;
, then we end up with a cyclic type dependency, where the type of the underspecified declaration depends on the type of the initialiser expression, which in turn depends on the type of the underspecified declaration because of the references. This makes it impossible to infer the type of the underspecified declaration.
Yes.
But what is the problem of a cycle in the value dependency? I'm guessing this has to with
constexpr
declarations.
Same thing. If auto foo = foo * 2;
were allowed to be used with foo
referring to the foo
being declared, it would be saying the initial value of foo
depends on the value of foo
. (You could distinguish those two values in some way, thus making it a non-cyclic dependency of foo
on some other value of foo
, but that does not solve the problem, so there is no point in it.)
Question 2 - Why are outer declarations hidden as soon as the declarator is complete?
According to the excerpt above, an underspecified declaration hides outer declarations as soon as its declarator is complete. This means that an underspecified declaration starts shadowing outer declarations before it comes into scope (at the completion of its definition). By contrast, non-underspecified ordinary declarations start shadowing outer declarations at the same time as they come into scope (at the completion of its declarator). Why don't underspecified declarations do the same?
You can say an underspecified identifier differs from a fully specified identifier because the former hides (not shadows) an outer identifier before it comes into scope while the latter hides an outer identifier just as it comes into scope. Or you could say they are the same because both hide an outer identifier at the end of the declarator.
If we did not hide outer identifier at the end of the declarator, we would have this situation:
int a = 3, b = 4;
{
auto a = a*2; // Allowed, outer a is visible until end of definition.
int b = b*2; // Not allowed, outer b is hidden at end of declarator.
}
Then you have an additional difference between underspecified and full specified declarations, and we generally like to avoid discrepancies like that. Further, the rule closes some opportunity for errors, as when somebody attempts to write an initialization that validly uses the identifier in a fully specified declaration but that would be wrong if we allowed underspecified declarations to refer to the outer identifier. For example, it is allowed, in a fully specified declaration, to refer in some ways to the object being declared. The first member of a structure might be initialized with the size of the structure:
struct foo { size_t s; char data[DATA_LENGTH]; };
struct foo S = { sizeof S };
That could be useful in a program that manages multiple types of structures in queues and other data structures, but a similar underspecified declaration could give the wrong size in auto S = (struct foo) { sizeof S };
since this would give the size of some outer S
rather than this one.
Upvotes: 0