December 5, 2020·4 min read·engineeringctypesmacrosstandards

typeof standardization has to decide which qualifiers survive

N2619 proposes obtaining a type from an expression. Qualifier preservation and variably modified types make the specification more involved than the familiar extension spelling suggests.

Obtaining an expression's type sounds straightforward until the result carries a qualifier the receiving code does not want. A generic temporary may need an assignable value, while another declaration may need to preserve exactly what the original expression describes. JeanHeyd Meneide's N2619, whose revision history dates this revision December 5, treats that distinction as part of standardizing typeof for C.

The WG14 document log records the paper in December. Its subject is a portable way to obtain a type, drawing on existing compiler extensions. The proposal remains under discussion, including choices about keyword spelling.

I value C's explicit types, but generic source sometimes needs to name a type it can already describe through an expression. Requiring that source to repeat a concrete type can duplicate information that the compiler already knows. A standard facility would be useful if it defines which information the operation preserves.

The result is a type

A type-producing operation differs from evaluating an expression to obtain its value. A declaration can use the resulting type to introduce another object. The new object still needs its own initialization and lifetime. Obtaining its type does not copy the original object.

The proposal's conservative spelling option uses _Typeof. The following illustrates a type-preserving declaration under that proposed option, not portable code available in standard C today.

const int limit = 12;
_Typeof(limit) other_limit = 20;

Under the proposal's preserving semantics, the second object retains the qualification of the first. The initializer supplies its value independently. That separation is useful because a type relationship and a value relationship are different program requirements.

A generic macro can need both relationships. It may want a temporary whose type follows an operand and whose value comes from evaluating that operand. The source needs to describe both operations correctly. A type facility helps with the declaration, while evaluation and sequencing still require their own design.

This is why typeof alone is not a complete macro-programming system. It answers a specific type question. It does not automatically establish that a macro evaluates its arguments once or groups its substituted tokens correctly.

Preserving qualification can be necessary or inconvenient

N2619 proposes preserving qualifiers in the type-producing operation and a separate operation for removing them. The text discusses const, volatile, and _Atomic, including disagreements in existing practice. Those details affect what a declaration produced by generic code permits.

For a declaration intended to reproduce the original type, retaining qualification is natural. Discarding it would lose part of the description. For a local temporary intended to hold an ordinary working value, retaining all qualification can impose a different set of restrictions.

These uses cannot be distinguished by the word type alone. The author of the generic code needs to choose whether the goal is faithful reproduction or a deliberately modified type. Making that choice explicit is more informative than letting it depend on an implementation's extension behavior.

The distinction also affects the explanation a library can give its users. A macro that preserves a qualifier should say so when that changes how its result may be used. A macro that removes qualification needs a reason related to the object it creates or the value it returns.

A useful example should therefore identify the intended destination as well as the source expression. The reader can then see why preserving or removing qualification serves that declaration. The spelling alone cannot explain the choice.

Type preservation does not transfer ownership. A pointer type can describe which operations are permitted through a pointer without establishing who releases the referenced object. Generic facilities still need lifetime contracts even when their type relationships become more precise.

Unevaluated has a boundary

The proposal usually obtains a type without evaluating its operand, but it treats variably modified types separately. Its wording and discussion make those cases relevant to side effects. This follows an area where familiar size-related syntax already needs a more careful account.

For a programmer, the practical consequence is to avoid treating every expression placed inside a type query as inert. If a generic facility admits variably modified types, its explanation has to account for the applicable evaluation rules. The ordinary case cannot serve as a universal promise.

That matters most when the operand contains work whose execution changes program state. A type query's purpose may be descriptive, but the language still needs rules about how it obtains runtime-dependent type information. Those rules determine whether source expressions participate in execution.

A library can choose to constrain its supported inputs to simplify that account. Such a constraint should be part of the facility's contract. Otherwise, a caller can reasonably infer a broader promise from examples that happen to use only simple scalar values.

Sharing a name across languages has consequences

N2619 also explains why importing C++'s decltype spelling would be problematic. In C++, parentheses around an expression can affect the result through reference-related rules. C macros commonly add parentheses to control token substitution and precedence.

The concern is therefore more specific than two languages having different type systems. A shared header could make punctuation added for macro safety participate in a different type rule. The same apparent query would then require a different explanation depending on the compilation language.

For code that crosses language boundaries, a familiar name can be misleading if the semantics diverge. A distinct spelling makes the need to understand that difference more visible. Conversely, a shared facility needs an agreement about behavior before the shared name provides much benefit.

C's interoperation role makes this part of the proposal especially interesting to me. A portable type query should reduce implementation-specific dependencies without adding a new ambiguity to headers shared with another language.

The remaining design work includes choosing a spelling and specifying how qualification and runtime-dependent types behave. Those decisions determine whether generic code can state a reliable contract. The useful result is a declaration whose relationship to its source expression can be explained precisely, including which properties of the type it keeps.