Skip to content

Design note: UFCS

Herb Sutter edited this page Oct 5, 2022 · 4 revisions

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?

A: Yes, and the principle is that the type author should be in control of their type.

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 some of the major alternative semantics we could choose for UFCS, in rough order of likelihood of unwanted silent changes in behavior. Note that this list is not exhaustive!

  • 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 both sets are 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.

Note that there are other options, notably to enable nonmember function authors to explicitly opt into being callable using member call syntax, C# "extension methods" style. I'm not attracted to this style for several reasons:

  • It is a variation of the above options, not an alternative to them, which is why I didn't put this in the above list: It doesn't directly address the problem of which function to prefer if the type author does provide a function of the same name too, so this actually has to be used together with (not instead of) an option like A, B, C, D, or similar.
  • It typically requires the function to know in advance that it should be callable with (only) member call syntax. So it's not about UFCS at all, really... it's more about a way to write more member-like functions rather than unifying functions, which is more appropriate in languages that only have member functions (in other words, this is also fits better when used with option E above).
  • It typically requires the function to extend a specific nominated type, which doesn't work well for composability of libraries. For example, I may want to use two types from two independently authored libraries that have similar interfaces, and use the same additional function to extend both without writing the function twice.
  • It would essentially restrict UFCS to only new code. I think it is important to also help unify the use of existing code and libraries.