Live data from Hacker News

A bit of background for the unified C++ call proposal

isocpp.org

31–40 of 46 posts

Re: A bit of background for the unified C++ call proposal

#31
Dear Bjarne,

Please, please, please reconsider not enabling x.f(y) resolve to f(x, y).

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 D and Rust - http://www.drdobbs.com/cpp/uniform-function-call-syntax/2327... https://github.com/rust-lang/rust/issues/16293

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 decision!

Re: A bit of background for the unified C++ call proposal

#32
post #5

>> 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 the concern is that a class "owns" its methods either directly or through inheritance. Therefor, the interface is expressed by this namespace and it can't be "invaded" or modified. From that…

I'm going to have to pose an educated disagreement.

According to the classic C++ article, "What's in a class?" by Herb Sutter (http://www.gotw.ca/publications/mill02.htm) -

The Interface Principle suggests -

For a class X, all functions, including free functions, that both (a) "mention" X, and (b) are "supplied with" X are logically part of X, because they form part of the interface of X.

So yes, f(x) is part of x's interface in C++, and has been considered to be so for a long time.

Re: A bit of background for the unified C++ call proposal

#33
What a shocker. Allowing f(x,y) to find x.f(y) is nearly useless; and allowing x.f(y) to find f(x,y) is scrapped. Even though D can do the latter just fine.

I can already write all my class functions as f(x,y) functions, if I want my code to look like C.

The whole point of UFCS is to work around the obnoxious fact that classes are closed after their initial definitions(*). Only the latter can help with that. So when I use a string library that has a reverse function, I can say string.reverse(), but when it then is missing a lowercase function, my only option is to create lowercase(string) myself. And now my usage of the string class is a mixture of global function versus member function calls. No consistency at all. And when I start trying to chain them, it turs into a complete disaster, eg:

    split("\n", lowercase(string.reverse().rtrim("#"))).strip();
    //vs
    string.reverse().rtrim("#").lowercase().split("\n").strip();
And this is why library writers end up trying to pack the kitchen sink into their classes. Or you end up with things like std::string that, out of the box, are basically just glorified memcpy classes (no trim, no transform, no string replace, no tokenization, no parsing [eg string->int], etc etc.); and thus you end up with countless people making their own string classes anyway.

UFCS could have even allowed extending built-in types in extremely useful ways. I could have declared "BitRef int::bits(int lo, int hi)", and allowed for eg "int x; if(x.bit(7)) x.bits(2,3) = 2;" instead of "if(x & 0x80) { x |= 0x08; x &= ~0x04; }" -- that may look a bit weird, but anyone who's done a ton of bit-twiddling will certainly appreciate the potential there for cleaner code.

I really hope D can get the GC completely out of the standard library and get a command-line switch to disable it. Then I can seriously consider switching languages.

(*) we could actually solve this problem with external class functions, but I'm sure they're even less willing to go for that. But imagine if you had:

    class Integer {
      ...
      int square() const { return x * x; }
      int self() const { return x; }
    private:
      int x;
    };
    
    extension int Integer::cube() const { return self() * square(); }
    
    int main() {
      Integer x = 5;
      print("{0}, {1}\n", x.square(), x.cube());
      //prints 25,125
    }
The idea would be that your extension can't access the private/protected state (unless the function is declared a friend of the class.)

You still have the ability to extend classes with useful functions as needed, and the encapsulation principles aren't violated: no internal state can be accessed, the functions can't be virtual so you won't need to modify the vtable definition, etc.

Re: A bit of background for the unified C++ call proposal

#34
post #8

Ahh I'm sad. I really wanted x.f(y) to find f(x,y). Then std::algorithm and ranges would combine to make sweet linq-like filter/map operations.

That's how the D language does it! Functional programming in D is a joy in part because of this feature.

Re: A bit of background for the unified C++ call proposal

#35
I like C#'s approach where a static function can be called as a method by adding the "this" keyword to the first parameter in its declaration. This makes it opt-in, which is annoying for legacy code. All collections got a fantastic map/filter/fold system bolted on through this mechanism, and as Bjarne noted the dot notation is great for chaining calls.

So in C#, to declare an fn as an "external method" you say:

    static void MyFunc(this MyClass myObj, int someParam){doStuff();}
Which can be called as

   var foo = new MyClass();
   foo.MyFunc(5);
The downside is that method not found errors become much more complicated and it's not obvious to the user whether a method is coming from a different package or not.

Re: A bit of background for the unified C++ call proposal

#37
One of the silly things about member functions is the effect that they have on argument ordering when ordering is not supposed to matter.

For example, consider "operator +", which can have in-object or function implementations. The ridiculous thing is that, for any two types in an expression like "+", you can't tell from observation if it will compile! Is it only available as a member function? If so, then any primitive left-hand argument in this supposed-to-be-commutative expression must be swapped to the right side, or wrapped in a pointless object. The same problem can exist for non-operator functions.

At least now with "auto" and other implicit typing (and even tuples and std::get), there is a chance to write single functions that work around messes such as these.

Re: A bit of background for the unified C++ call proposal

#38
post #30
post #25

Earlier quoted context omitted.

Under the covers, `x.f(y)` is actually something like `_ZN1X1fEi(&x, y)`, where the mangled name indicates the class of X and the type of y. The implicit 'this' pointer is actually explicit in the implementation. Similarly, a call to `f(&x, y)` would actually be a call to `_Z1fP1Xi(&x, y)`, with similar reasoning. The author is proposing that writing f(x, y) Which would normally only bind to the second mangled functi…

This is also why functions can work on a null object and not crash, as long as they do not access instance data. Though at that stage they should be static or external.

Yes, except for calls to virtual functions, which actually become

  (&x)->vtbl[N](&x, y)

Re: A bit of background for the unified C++ call proposal

#39
post #30
post #25

Earlier quoted context omitted.

Under the covers, `x.f(y)` is actually something like `_ZN1X1fEi(&x, y)`, where the mangled name indicates the class of X and the type of y. The implicit 'this' pointer is actually explicit in the implementation. Similarly, a call to `f(&x, y)` would actually be a call to `_Z1fP1Xi(&x, y)`, with similar reasoning. The author is proposing that writing f(x, y) Which would normally only bind to the second mangled functi…

This is also why functions can work on a null object and not crash, as long as they do not access instance data. Though at that stage they should be static or external.

AFAIK, this is still undefined behavior, so "can work" means "you got lucky this time, your next compiler upgrade may turn this into a crash, security vulnerability, or utterly confounding Heisenbug".

Re: A bit of background for the unified C++ call proposal

#40

One of the silly things about member functions is the effect that they have on argument ordering when ordering is not supposed to matter. For example, consider "operator +", which can have in-object or function implementations. The ridiculous thing is that, for any two types in an expression like "+", you can't tell from observation if it will compile! Is it only available as a member function? If so, then any primit…

  > One of the silly things about member functions is the 
  > effect that they have on argument ordering when ordering 
  > is not supposed to matter.
This isn't a problem with member functions, free functions have this problem as well. And not just in C++, commutativity just isn't a first-class concept in any programming language that I've ever seen.
Post reply on HN