-
Notifications
You must be signed in to change notification settings - Fork 251
Design note: UFCS
Q: Why does UFCS use fallback semantics (prefer a member function)? Doesn't that mean that adding a member function later could silently change behavior of existing call sites?
Many aspects of this were raised and discussed in WG 21 when we looked at UFCS proposals over the past decade, and here's a partial brain dump of some highlights.
First, note that name lookup fallback semantics, which can cause name hiding, isn't a new thing. We have some relevant experience already: Anytime you use fallback semantics for name lookup, such as preferring one scope over another, this leads to name hiding. This happens today when a derived name hides a base name, or a name in a nested class or namespace hides one in an outer namespace... so name hiding is not necessarily inherently scary, and in those cases I think the experience is that it's been working fine because of the principle that the more-specialized thing should be in control of its interface and therefore have the priority in saying what the name means for its type.
For UFCS, name hiding will happen if UFCS prefers one scope over another (e.g., if it prefers a member if available). This can be a feature rather than a bug, but I agree there are potential dangers in silent changes in behavior. But again, the same thing can already happen if a derived class adds a name (not just a function), or a nested namespace/class adds a name. I think the priority principle still applies -- the more-specialized thing should be in control of its interface, and for UFCS that is the type author of this object.
Here are five major alternative semantics we could choose for UFCS, in rough order of likelihood of unwanted silent changes in behavior:
A) Prefer nonmember function. Adding a new nonmember function could hide a member function, even one that is a better match, and silently change behavior. Probably really bad, because now we're changing what the type's own interface means in different contexts. The type author should have control over what their type means. This is the version that got the most resistance in WG 21 (and I now think rightly so).
B) Prefer member function. Adding a new member function could hide a nonmember function, even one that is a better match, and silently change behavior. This is much less bad than A, and preserves the principle that the type author should have control over what their type means. (In this alternative, the implementation could warn when a nonmember is a better match, but I suspect that would be noisy and people would want to turn it off.)
C) Overload and select the best match. Adding a new member or nonmember function could still silently change behavior (so this does not eliminate potential accidental surprises), but if it does it's because you're getting a better match which is at least something.
D) Declare the call ambiguous if either set is nonempty, and require explicit disambiguation (e.g., scope resolution). This seems to at least partly defeat the purpose of UFCS in many use cases though.
E) Slice the Gordian knot: Don't allow both nonmember and member functions in the language, pick one kind and force everything to be that. This can work but I feel it would go way beyond the charter of Cpp2 to be a cleaner C++, and limit to only changes that solve known problems in quantifiable ways while still being C++. It seems like it would be a fundamental underlying change to C++'s model and a foreign concept, so IMO it's on the table for Cpp2 but has to clear a high bar for being compellingly the right way to go because every change like this adds to risk of incompatibility.
Right now, I think B and C are likely the best choices, and for now I'm going with B to keep the type's author in control of their type's interface.