A bit of background for the unified call proposal

Save to:
Instapaper Pocket Readability

C++ provides two calling syntaxes, x.f(y) and f(x,y). This has bothered me for a long time. I discussed the problem in the context of multimethods in D&E in 1994, referring back to a proposal by Doug Lea from 1991, and again in 2007. Each of the syntaxes has its virtues, but they force design decisions on people, especially library designers. When you write a library, which syntax do you use for operations on objects passed to you by your users? The STL insists on the traditional functional syntax, f(x,y), whereas many OO libraries insist on the dot notation, x.f(y). In turn, libraries force these decisions on their users.


Interestingly, I solved the problems for operators: x+y doesn’t care whether you provide operator+(T,T) or T::operator+(T). The problem is getting more noticeable as we get more generic libraries. Note begin(c) and c.begin() for range-for loops and in general code. Why do we/someone have to write both? If c.begin() exists, begin(c) should find it, just as x+y finds the right implementation. Such call-syntax adapters are pure overhead and different people’s adapters can clash.


In early 2014, Herb Sutter and I each independently decided to propose a unified syntax. Herb suggested unification based on allowing x.f(y) to find a non-member function, giving preference to the x.f(y) syntax, whereas my ideal was that x.f(y) and f(x,y) should mean exactly the same. After a quick discussion, we joined forces. Based on real input from code and users, I reluctantly agreed that for compatibility reasons, x.f(y) and f(x,y) could not mean exactly the same. The only feasible way forward was to do a traditional lookup based on the syntax used, and then try the other syntax if the first one failed. Stability – backwards compatibility – is an important feature, overruling my desire for perfection.


To my surprise, many people came out strongly against x.f(y) finding f(x,y) – even if member functions were preferred over free-standing functions by the lookup rules. I received email accusing me of “selling out to the OO crowd” and people whose experience and opinion I respect insisted that x.f(y) finding f(x,y) would seriously compromise their ability to design stable interfaces. I think those fears are greatly exaggerated, but I could be wrong. Also, I prefer the dot syntax in some cases; for example, I find x.f(y).g(z) more readable than g(f(x,y),z). However, there was no chance of acceptance of a proposal that included x.f(y) finding f(x,y). Maybe modules will eventually help here. Furthermore, David Vandevoorde pointed out that because of the two-phase lookup rules having x.f(y) find f(x,y) would complicate the task for compiler writers and possibly be expensive in compile time.


So, we are left with a simple proposal to allow f(x,y) to find x.f(y) where f(x,y) wouldn’t work today. This solves the problem for library writers and their users along the lines of the STL:

  • Library designers don’t have to force users to use one preferred syntax or to duplicate the implementation to handle both.
  • It allows us to write simple concepts for new libraries.
  • We no longer have to write call-syntax adapters.
  • We can in many cases add functionality to an abstraction without modifying a class
  • We no longer have to bother the users with the distinction between member functions and helper functions.

The proposal was approved by the Evolution Working Group. Faisal Vali did an experimental implementation in Clang. Now we just have to hope that we can agree on exact wording in time for C++17. I consider it a small proposal that will significantly simplify the way we design and use libraries.

Add a Comment

You must sign in or register to add a comment.

Comments (12)

3 0

data_abstraction_matters said on Feb 15, 2016 06:17 AM:

Please, please, please reconsider.

You must explain to the detractors that they must reconsider allowing x.f(y) to resolve to f(x, y). This is not a selling out to OO, but in fact the opposite! Allowing x.f(y) to resolve as such enables us to finally get _away_ from OOP by using an alternative style called 'Data Abstraction Style'. I have written up an example of this style here - https://github.com/bryanedds/das

I have also written an entire C++ core library in said style here - https://github.com/bryanedds/ax

In PLT terms, data abstraction is the dual of OOP. In fact, I use it significantly in F# as a way to do pure functional programming where others just fall back into OOP - https://vimeo.com/128464151

Data Abstraction Style with resolution of free-standing functions to dot syntax gives us the best of both worlds - the increased modularity and extensibility of free-standing functions as well as the nice tooling and API explore-abily of the dot intellisense.

Additionally, there are precedence for this functional is less OOP-y language like Rust and D.

Finally, this syntax is important just to allow extension methods without a more specialized syntax that won't likely appear anyways.

Please pass along this information to the people holding out on allowing x.f(y) resolve to f(x, y) - it is not selling out to OOP - it's an elegant path to finally move beyond it. People must be made to understand this before making their final conclusions!
0 0

xan said on Feb 15, 2016 07:02 AM:

Would it help if the library writer opted in with the "operator" keyword, sort of like for real operators?

T operator f(U x, V y);

allows

f(x, y);
x.f(y);
0 0

dobkeratops said on Feb 15, 2016 07:43 AM:

This is extremely disappointing.

As a compromise for those who oppose 'full UFCS', why not propose that UFCS-able free functions require an explicit 'this' pointer e.g.
void foo(Bar*); // not available for UFCS. foo(x)
void foo(Bar* this){} // available for UFCS as x->foo();

This would prevent any surprising behaviour in existing source bases, and it would be easy for a specific project to discourage its' use if the maintainers don't like it (as it makes it easy to search for declarations).
(it would also make it easier to refactor code back & forth, allowing people to improve the maintainability of existing sourcebases.)

The full version of UFCS is far more useful: as the other poster comments, it's not about OOP, it's about allowing the superior discoverability of dot-autocomplete(without cluttering up classes with excessive dependancies) , and less nesting for chaining.

As it stands we're going to be stuck with this conflicting draw, hence bouncing between the two styles, for many years to come.

With full UFCS all ambiguity is eliminated and we'd be able to use a.foo(b) everywhere - its' easier to read and write,especially for complex expressions - and we'd be able to refactor code based on changing demands (moving more code out of classes, for superior decoupling.
0 0

Bjarne Stroustrup said on Feb 15, 2016 09:57 AM:

It is not me you have to convince, and please remember that opponents of x.f(y) -> f(x.y) are neither stupid nor inexperienced. It will take a long time to address their genuine concerns. For now, we have "half a loaf" which will be most helpful in many areas. That is, if the committee accepts the proposal - as looks probable - we have that.

I am against explicit annotations for this ("opt in"). They mess up code and puts the burden of knowing details of the implementation back onto the users.
0 0

thomas_n said on Feb 15, 2016 01:28 PM:

Maybe I'm too inexperienced or not smart enough to see it on my own, so maybe you could shed some light on what those "genuine concerns" are (in more detail)?
4 0

byuu said on Feb 15, 2016 01:28 PM:

Please add my voice to those requesting reconsideration for f(x,y)->x.f(y)

I don't believe it to be a "half loaf" situation: x.f(y)->f(x,y) does not allow me to extend classes, and is thus of little use to me as a user of classes. This is more akin to breadcrumbs.

I realize that this very extensibility is exactly what some are opposed to in the first place, but I believe their objections to be unfounded: the data is still encapsulated via private members, which can only be accessed via friend declarations from the class author. Further, the functions would not be virtual, so there are no changes to the vtable, and thus, no changes whatsoever to the class itself: it is simply a syntactic transformation.

C++ is wrought with features, and is why it is so chiefly loved among its most ardent supporters: powerful in the right hands, and forbidden by company-specific coding conventions as desired. UFCS could easily join them, even as a compiler flag if the name lookup is truly such a burden.

Let's take a string class for example. Say it contains string::reverse(), but lacks string::lowercase(). I have no choice as a user but to write string& lowercase(string&). This leads to horrible composing, or turning C++ into C/Lisp.

Current style:

split("\n", lowercase(string.reverse().rtrim("#"))).strip(" ");

C/Lisp (proposed) style with only x.f(y)->f(x,y):

strip(split("\n, lowercase(reverse(rtrim(string, "#")))), " ");

What we could have with true f(x,y)->x.f(y):

string.reverse().rtrim("#").lowercase().split("\n").strip(" ");

Only the last version is plainly clear and readable. I know you will tell me to split the statement into multiple lines, but that's not really the point.

Nobody is happy with the first example. Without this, you end up in one of two camps: people writing minimal classes and functional/Lisp-style code as in the middle example; or people writing kitchen sink classes that are a bloated mess, to try and attain syntax akin to the third example. The end result is that we have dozens of incompatible string classes from different libraries, instead of everyone building upon std::string.

And subclassing only works within one's own codebase, and fails as soon as someone else's codebase introduces its own subclass. Yes, overload conflicts could potentially arise with UFCS; but that's already the case today with f(x,y). It's vastly more manageable than subclassing.

I understand many on the C++ committee are reserved; but the D programming language already supports f(x,y)->x.f(y), and the sky has assuredly not fallen.

If UFCS is truly not possible for C++17, is there any possibility you could please consider extension methods, if nothing else? The limitation that plain functions cannot be added to a class after its initial definition is completely arbitrary and quite detrimental to code consistency.

Sorry for the long-winded post, but thank you in advance for your consideration!
0 0

Bjarne Stroustrup said on Feb 15, 2016 01:41 PM:

(1) For reasons, see the "Unified Call Concerns" paper
(2) This is not "just crumbs": when using the f(x) syntax you can use members and helper functions without having to remember which is which; you just cannot do that with the x.f() syntax.
3 0

ibob said on Feb 15, 2016 02:30 PM:

I, too, would like to join the "x.f(y) should resolve to f(x, y)" crowd.

I understand the concerns that this might break newly added functionality in the *future of the future*, but this can be mitigated by a warning (which in turn would create warnings in existing code, indeed).

While I see no point in adding a special keyword to the extension methods (as the suggested "operator") I do see one to adding an explicit annotation to extensible classes (say "extensible class Foo"). This should render all concerns mute.

As others have pointed out, I too see little to no benefit in f(x, y) resolving to x.f(y). Yes that would make us not worry about remembering which is which, but so does the compiler. After all f(x, y) is a compilation error if there is no f(x, y).

I have never seen code that would benefit from f(x, y) resolving to x.f(y), and on the contrary: I have seen (and written) lots of code that would be much more aesthetically pleasing if the opposite was true. It's just natural to how the human brain processes text and information.

Are there even any languages that resolve f(x, y) to x.f(y)? There are lots that do the opposite. Most modern object-oriented languages allow extension methods. C# and D and Rust do. Languages like Lua or Nim don't even have "methods". In Nim x.f(y) is just syntactic sugar for f(x, y) (in Lua x:f(y) is syntactic sugar for x.f(x, y)). There is a reason language designers have chosen this and not the former.

Adding the suggested feature alone is yet another step in ostracizing C++ and putting it into the "unlike every other" bucket.
3 0

ChuckAllison said on Feb 16, 2016 01:49 PM:

FWIW, D has had UFCS for many years and chose your initial scheme. Works well. I like the pipeline style of programming it supports. Maybe some data from the D community would be helpful?
2 0

byuu said on Feb 16, 2016 09:05 PM:

> when using the f(x) syntax you can use members and helper functions without having to remember which is which; you just cannot do that with the x.f() syntax.

Yes, and you end up with my case two: strip(split("\n, lowercase(reverse(rtrim(string, "#")))), " ");

The order of operations is evaluated from right-to-left (rtrim is called first, strip last), and there is heavy displacement between arguments (all of the inner function calls obscure the first parameter to strip.) This begins to resemble functional languages, which to me at least is not an advantage.

Right now, a user or library writer *could* write wrappers for x.f(y)->f(x,y). They could even be compiler-generated. Yes, it would be a burden on source code size, compilation times, and potentially binary size.

But the reverse is not true: once a library writer closes their class definition, it's no longer possible to add new x.f(y) wrappers or functions. And as my third example shows, x.f(y) leads to a substantial increase in code readability: left-to-right evaluation, and the function arguments aren't obscured.

At any rate, I understand if your hands are tied. I'll keep my fingers crossed and hope f(x,y)->x.f(y) can make it into a future revision of the language.

Thanks again for your time, as well for your hard work on this excellent programming language.
2 0

Bjarne Stroustrup said on Feb 17, 2016 05:39 AM:

Thanks Chuck, but it is not me you have to convince. I still think that my original proposal or something close to it would be the best. I also still think that what we are likely to get (f(x,y) can find x.f(y)) solves real problems and is far better than nothing.
0 0

Michael Bevin said on Feb 18, 2016 09:53 AM:

Hi, very disappointing that we're only (if-at-all) getting the solution in one direction (in the near-medium-term) by the looks of it .... seems to be the nature with all such new things that people have a lot of resistance, which is not necessarily always warranted - hard for me to imagine what genuine concerns there are that couldn't be assuaged one way or another ...... so hopefully they can be assuaged in the not tooo distant future so we can get both sides of this.

In any case, I really trust you and Herb and everyone are doing your best there Bjarne .... good luck continuing your work to evolve C++, hopefully for a long time still - I was really impressed with your list of main features you posted that you were suggesting for C++17, really spot on.