On the non-uniform semantics of return-type-requirements

Document number: P1452R1
Date: 2019-03-11
Project: ISO/IEC JTC 1/SC 22/WG 21/C++
Audience subgroup: Evolution
Revises: P1452R0
Reply-to: Hubert S.K. Tong <hubert.reinterpretcast@gmail.com>

Changelog

Changes from R0

Introduction

What we have as a return-type-requirement today is a misnomer in both the syntactic and semantic sense. The Concepts TS introduced two kinds of semantic constraints that syntactically took the form of a trailing-return-type: the deduction constraint and the implicit conversion constraint. In the time since, deduction constraints were first changed by the lack of constrained placeholders until the adoption of P1141R1 at the 2018 San Diego meeting (becoming syntactically different from a trailing-return-type), and then—with P1084R2—they ceased to deduce in the manner associated with return type deduction. The syntactic space occupied by deduction constraints in the Concepts TS is now occupied by something else altogether; however, the implicit conversion constraint lives on. Yet the two are both return-type-requirements, using the -> token.

The inconsistency between the uses of the -> token in the same context seems unfortunate; and the trailing-return-type form of return-type-requirement, with its current semantics, appears to be easily replaceable. Its presence in the language seems to be an unnecessary complication that may frustrate future extensions.

A possible future: Generalized placeholder deduction

One aspect of the Concepts TS that did not make the cut into C++2a is “generalized auto”. In particular, something like this:

void f(std::unique_ptr<Concept auto> p); //
    //                 ^^^ Constraint applied to the pointee type.

would mean the same as:

template <Concept T> void f(std::unique_ptr<T> p);

Let us then consider the application of “generalized auto” to return-type-requirements where an analogue would look like this:

requires {
  { f() } -> std::unique_ptr<Concept auto>; //
      // Constrain the pointee type.
}

and ask ourselves what the following should mean:

{ x } -> Concept auto;

-> Type versus -> Concept

The -> Type form of return-type-requirement behaves consistently with the behaviour of return E; for a function that is declared to return Type except that placeholder types are allowed for trailing-return-types in function declarations and not for return-type-requirements. That is, the semantics of the various compound-requirements in

requires { { E } -> Type; };
requires (void (&f)(Type)) { f(E); };
requires { [](Type) {}(E) }; };
requires { { E } -> ConvertibleTo<Type>; };

are roughly the same, with the first two being equivalent. The last form is less aware of the context in terms of access checking, null pointer conversion, and other cases where perfect forwarding is less-than-perfect. It also requires explicit conversion to be possible, and not only implicit conversion. Both the first and the last forms are used by the library in N4800, the pre-San Diego working draft (with five instances of the first form and nine of the last form). Note, however, that -> Same<Type> is more common at forty instances.

In contrast to -> Type, the -> Concept form of return-type-requirement most notably does not use a type after the -> token, but is instead one of the places where a concept name has a special meaning. It also deduces in a manner different from either of Concept auto or Concept decltype(auto) would for a function whose declared return type contains such a placeholder type, and it does not involve a check for convertibility.

-> Concept auto is (syntactically) -> Type

We now come back to the question from earlier:

{ x } -> Concept auto; // What should this mean?

Maybe (with decltype((x)) being int &):

{ x } -> Concept; // Concept<int &> is satisfied.

or, maybe:

auto f() -> Concept auto { return x; } // Concept<int>; no "&" (!)

Extending the current -> Type case to allow placeholder types would leave us with a problem where the extension of the current semantics would lead to a difference in the meaning of the first two compound-requirements in

requires {
  { E } -> Concept;
  { E } -> Concept auto;
  { [](Concept auto) {}(E) };
};

However, we note that the third compound-requirement already expresses a deduction constraint in the TS style (except that the TS wording causes access checking, etc. to be ignored). It would seem that extending from the current semantics of -> Type return-type-requirements is not very profitable.

Concept auto with a different precedent

Given:

void g(Concept auto);

Deduction does not only happen through deducing template arguments from a function call (akin to the current -> Type deduction). It may also happen through deducing template arguments from a function declaration, e.g.,

class A {
  static int x;
  friend void ::g(decltype((x))); // Would deduce int &.
};

This latter form of deduction (deducing from a type as opposed to deducing from an expression) is a possible interpretation of the change to -> Concept adopted through P1084. This can also be seen as the difference between requiring convertibility and requiring sameness. Through these characterizations, we can make the case that -> Type does not have to retain the same semantics as return type deduction.

Possibility for a more powerful -> Type (recap)

Today’s placeholder types are limited in context; however, the Concepts TS allowed deduction constraints like

-> std::vector<Boolean>

through “generalized auto”.

We note that this sort of “type pattern” is expressed in the language as a type. Thus, if we retain -> Type with its current semantics, we will not gain the benefit of applying P1084 to such cases if we were to adopt them into the language. That -> Concept and -> Concept auto would have different semantics would be highly unfortunate, as will having -> Type behave rather differently depending on whether a placeholder is present or not.

We also note that Concept auto already has the ability to take on semantics that behave more like Concept in -> Concept. That is, we are free (at this time) to make it such that -> Type has semantics matching that of -> Same<Type>, by far the most commonly used semantic in N4800, by applying deduction from a type as opposed to deduction from an expression. Benefits would be that -> Concept auto would work like -> Concept does in N4800, and that replacing a concrete type in a return-type-requirement with a concept that the type models would not cause the compound-requirement to reject previously accepted cases.

Proposal for C++20: remove -> Type

The -> Type form of return-type-requirement is underpowered and does not bring additional expressiveness to the language. It is on the wrong side of the split between ConvertibleTo and Same as used with compound-requirements in N4800, and it introduces complications for future extensions; therefore, it is proposed that the trailing-return-type form of return-type-requirement be removed. At the same time, it is proposed that trailing-type-requirement is a more appropriate name.

Acknowledgements

The author would like to thank Walter E. Brown, Casey Carter, Paul Preney, David Stone, and any others who have been missed for their feedback on the topic of this paper. The author would also like to thank JF Bastien, chair of the Evolution Working Group Incubator, for the opportunity to present on this topic during the February 2019 session at Kailua-Kona, Hawaii.