Document #: | P2818R0 |
Date: | 2023-03-15 |
Project: | Programming Language C++ |
Audience: |
EWG |
Reply-to: |
Gašper Ažman <gasper.azman@gmail.com> |
this
not on the first
argument?friend
thing
out-of-line?This paper introduces a unification of hidden friends and explicit-object member functions to allow a limited, but hopefully uncontroversial Uniform Call Syntax for them.
Unlike the previous proposals on this topic, this one avoids pretty much all controversy.
Why we might want to have UFCS in the language has been covered extensively already, and Barry Revzin classified all approaches and issues in [Revz].
The short summary boils down to a few points:
While the above list may be a bit short, at least the first point is so incredibly important that the list of papers on the subject is staggering.
Note: the CS: and OR: labels refer to semantic options in the above-linked article. Please read it, it’s short.
The post very helpfully lists much prior art by many of WG21’s esteemed members: Glassborow, Sutter, Stroustrup, Coe, Orr, and Maurer; specifically [N1585], [N4165], [N4174], [N4474], [P0079R0], [P0131R0], [P0251R0], [P0301R0] and [P0301R1].
With regards to the taxonomy proposd in [Revz], this paper is sortish in the CS:FreeFindsMember category, but with CS:MemberFindsFree and CS:ExtensionMethods left as a possible future extensions, as they aren’t mutually exclusive.
This paper proposes OR:OneRound, but with ambiguities being impossible (ill-formed) due to the way this is done.
There have been thoughts around
operator..FQN()
, Kirk Shoop and
Ville Voutilainen seem to have a reasonable grasp of how far that
got.
The pipeline operator in its various incarnations also is roughly related - the people at the head of that one seem to be Barry Revzin, Colby Pike [P2011R1] and Isabella Muerte [P1282R0].
We propose that marking an explicit-object member function as a
friend
(to parrot inline friend
function declarations, specifically hidden friends) would also make it
callable via free-function argument-dependent-lookup.
Example:
struct S {
friend int f(this S) {
return 42;
}
};
int g() {
{}.f(); // OK, returns 42
S(S{}); // OK, same
f(f)(S{}); // Error; f can only be found by ADL
}
// note: forward declaration
int f(S); // Error, int f(S) conflicts with S::f(S)
int f(S) {}; // Error, int f(S) conflicts with S::f(S)
int f(int); // OK
That’s pretty much it.
While friend
communicates
exactly what this actually does, it only does so if one knows about
hidden friends.
We could use a context-sensitive keyword here, like
adl
, or associated(type-id-or-class-template-id, ...)
,
as Lewis Baker’s paper [D2823R0] proposes,
and just say that these functions are callable via ADL for the classes
mentioned in the parentheses using this free-function notation. We still
need this paper’s wording to get that accomplished.
The main issue with associated(type-id-or-class-template-id, ...)
is that type-ids cannot be dependent, as that would be equivalent to
adding the overload to every namespace.
friend
does not introduce
this complexity, but the additional power might be useful enough to
choose as preferred syntax for the semantics this paper proposes.
(very slightly paraphrased)
In general, concerns arise from the existing guarantee that member functions simply don’t mix with ADL. Using a member call syntax means ADL doesn’t happen, but declaring a function a member also makes it not dance with ADL overloads in an overload set. The first guarantee is paramount, the second is Really Nice To Have.
Granted, the proposal still allows having the second guarantee. Don’t
mark your functions adl
,
done.
There is only one function in the first place.
The friend
syntax signals the
behavior exactly. The declaration of the member function is
also injected into the type’s “hidden” namespace as a hidden
friend after notionally removing the keyword
this
from the argument list.
This is OK, because explicit-object member functions have free-function type, and their bodies behave as if they were free functions, so we’re not lying. We’re doing exactly what it looks like.
You opt-in to UFCS on a per-declaration basis. This matters because UFCS is primarily about enabling generic code to use a given type, and gives precise control about both the free-function and member-function interfaces of a given class. When both interfaces should provide a given signature, this is the only proposal that lets you just do that and only that, without impacting other parts of either overload set.
It just merges two things we already have - hidden friends, and explicit-object member functions. No need to remember which comes first or how a given function is defined - both syntaxes always dispatch to the only implementation.
It does not propose, but does not preclude, future extensions into, well, extension methods. See the Future Extensions chapter.
While the author of this proposal is of a mild opinion that Extension Methods (CS:ExtensionMethods) would not carry their weight in C++, this paper is specifically neutral on this topic and reserves the only plausible syntax for them:
// Disclaimer: NOT PROPOSED IN THIS PAPER
struct E {};
int h(this E) { return 42; } // look ma, I'm not a member of E
int main() {
(E{}); // ok, obviously, since h is declared outside of E
h{}.h(); // also OK, h found by ADL and specifies where to put `this`.
E}
There is one caveat - in this case, if
E
declares an
h(this E)
, it would conflict at
declaration time, since this proposal already specifies that
behavior.
(very slightly paraphrased)
We can do a double-requirement. The extension needs to be declared with an explicit object parameter and the class needs to say that it allows such extending. Then we’re good with the extension too.
Consider the ranged for
loop
and the magic it does with looking for
begin
and
end
.
If we had had this facility from the beginning, we probably
wouldn’t have invented that magic, but just added
friend
to
begin()
and
end()
member functions of all
standard containers instead, so that the free-function version was
accessible in all contexts.
std::ranges
might have had a
substantially different design if [P0847R7] and this facility had been
available. Note that
std::ranges::begin
looks at free
function and members to decide which one to call.
friend
to signal exactly what
friend
does in this context,
like a context-sensitive keyword
associated(type-id, ...)
?Because this is a novel direction that might actually fit the language and pass.
No, but given that it uses a syntax that is ill-formed in C++23, and that it only inserts an alias to the same function that otherwise works exactly like a hidden friend, I really don’t have implementation concerns. Any compiler that implements [P0847R7] will have zero issues implementing this paper.
No. I don’t need them, and injecting functions into the space of member functions that is explicitly given to the class designer is wrong unless properly scoped. I don’t know how to properly scope it. If you do, the only reasonable syntax is above.
this
not on the first
argument?Not yet. I might bring that paper if this one passes, but separately.
friend
thing out-of-line?It’s still an explicit-object member function, so normally:
struct S {
friend int f(this S);
};
int S::f(this S) { return 42; }
No, friends
can’t be private,
and so this can’t either.
Fortunately, making it private has no reasonable use - if you’re in the class implementation where you can use private methods, you’re not in a generic context where you’d have to use free-function syntax.
No. Most of the time, you won’t want to make your explicit-object member functions also public. This has to do with satisfying concept requirements, not getting closer to general UFCS.
Any time you decay something to a reference in a forwarding function, the compiler has to ODR-use the move-constructor.
This matters for immovable types:
struct immovable {
() = default;
immovable(immovable&&) = delete;
immovable};
void takes_immovable(immovable x) {}
auto forwarding_function(auto&& x) {
return takes_immovable(std::forward<decltype(x)>(x));
}
auto h() {
(immovable{}); // OK, direct construction
takes_immovable
// Error, use of deleted function immovable(immovable&&)
(immovable{});
forwarding_function}
In general, forwarding functions don’t work for immovable types, and
std::execution
and related
things will generate a lot of them.
The work-around is to use lazy construction such as with
in_place
(see below) but that
generates a lot of bloat the optimizer has to remove and is far from
free in terms of syntax.
This especially matters for construction of aggregates that have immovable members, since they don’t (and can’t) have user-declared constructors, although this problem isn’t tackled by this paper.
With this paper, we declare true aliases to member functions, so the following will work:
struct Logger {
// OK, log(logger, immovable{}) and logger.log(immovable{}) both work
friend void log(this Logger&, auto... args);
friend void log_free(Logger& self, auto&&...args) {
// does not work with immovable types
.log(std::forward<decltype(args)>(args)...);
self}
};
in_place
template <typename F>
struct in_place {
F_ f;
operator decltype(std::move(f)()) && {
return std::move(f)();
}
};
template <typename F>
(F) -> in_place<F>;
in_place
// usage
(in_place([]{return immovable{};})); // works, but breaks overload resolution in case of templates
forwarding_function// void takes_immovable(std::same_as<immovable> auto x) is broken because it doesn't force a conversion