Hacker News new | ask | show | jobs
by mattnewport 3152 days ago
C++ has a standard which contains extremely precise descriptions of how things work which is more than can be said for many languages. How many other languages are there that have three major compiler and standard library implementations with almost no common code yet which all manage to be largely compatible (able to compile the same code and agree on the meaning)?

Some of the complexity of C++ comes from it doing hard things, some is in part a consequence of heroic efforts at backwards compatibility. There are areas of the language that most people stay away from or use in very constrained ways like multiple inheritance that are unlikely to be deprecated for backwards compatibility reasons but in my many years of professional C++ development multiple inheritance has never caused practical difficulties for me precisely because everybody stays away from it except for pure abstract interfaces.

1 comments

>> but rather from the lack of simultaneously short and precise descriptions of how things work.

> C++ has a standard which contains extremely precise descriptions of how things work which is more than can be said for many languages.

(0) Most programming languages don't set the bar very high.

(1) You missed the “short” part. The C++ standard is already pretty long, and it isn't even written in a form that makes it possible to prove things about C++ programs by consulting the standard.

> How many other languages are there that have three major compiler and standard library implementations with almost no common code yet which all manage to be largely compatible (able to compile the same code and agree on the meaning)?

I can think of at least four: Standard ML, Common Lisp, C, Java.

> Some of the complexity of C++ comes from it doing hard things,

In unintelligent ways. For example:

(0) Macro, pardon my French, template expansion as a generic programming tool is super dumb. Better alternatives were known in the 70's.

(1) C++ classes are very poor abstract data types: You need arcane language design hacks like `friend` to abstract over two or more types at once.

(2) C++ classes are also very poor object constructors: The language has no type for “all objects that have methods foo, bar, qux” (à la OCaml or Go).

> some is in part a consequence of heroic efforts at backwards compatibility.

Most of it.

> (0) Macro, pardon my French, template expansion as a generic programming tool is super dumb. Better alternatives were known in the 70's.

And yet C++ has better support for generic programming than most mainstream languages and it continues to improve. What's a language you think does generic programming 'right'?

> (1) C++ classes are very poor abstract data types: You need arcane language design hacks like `friend` to abstract over two or more types at once.

I don't really understand this comment. C++ has good support for static / compile time ADTs (the STL is very much built around the idea) and also supports 'runtime' ADTs through interfaces. In neither case is friend needed to abstract over types.

> (2) C++ classes are also very poor object constructors: The language has no type for “all objects that have methods foo, bar, qux” (à la OCaml or Go).

Static polymorphism in C++ currently relies on duck typing but this is what the Concepts TS is addressing. A major benefit of that will be improved error messages and a better generic programming experience.

> What's a language you think does generic programming 'right'?

The most obvious examples are ML and its derivatives.

> C++ has good support for static / compile time ADTs (the STL is very much built around the idea) and also supports 'runtime' ADTs through interfaces. In neither case is friend needed to abstract over types.

The STL is built around ADTs in spite of the language's lack of support. If C++ had an actual notion of abstract types, then templates could be type-checked against ADT specifications (so-called “concepts”), rather than having to wait until instantiation time.

> In neither case is friend needed to abstract over types.

You can't encapsulate a single abstraction providing two or more ADTs without using `friend`. That's a fact.

> Static polymorphism in C++ currently relies on duck typing but this is what the Concepts TS is addressing.

This is a reply to the wrong thing. Concepts are ADT specifications, not object specifications.

> The most obvious examples are ML and its derivatives.

I'm reasonably familiar with F# but I know that's strayed quite far from its ML roots and its generics are constrained to some degree by what the CLI supports. For the kinds of generic programming that C++ does well it's not clear to me where the ML style is superior. Can you give some specifics?

> If C++ had an actual notion of abstract types, then templates could be type-checked against ADT specifications (so-called “concepts”), rather than having to wait until instantiation time.

Yep, that's why everyone wants Concepts to be standardized.

> You can't encapsulate a single abstraction providing two or more ADTs without using `friend`. That's a fact.

I'm still not clear what the issue is here. A C++ class can implement multiple interfaces for dynamic polymorphism without using friend and can implement more than one 'concept' for static polymorphism without using friend. Can you explain what you mean in more detail?

> This is a reply to the wrong thing. Concepts are ADT specifications, not object specifications.

Are you talking about something like Go interfaces? For my use cases static polymorphism is generally preferable to dynamic polymorphism and in those cases where dynamic polymorphism is desirable C++ style interfaces are often sufficient. When the need arises for something like a Go interface in C++ people usually use type erasure but implementing that currently requires a bit more boilerplate than would be ideal (though there are libraries that help). That's something that could be solved in the future by something like Herb Sutter's metaclass proposal. Perhaps I'm still not understanding your point though.

> Can you give some specifics?

Concepts are what ML and Haskell have been calling “signatures” and “type classes”, respectively, for ages. Unlike concepts, which only exist in the collective minds of C++ programmers, signatures and type classes are actual features of existing type systems, so, for instance, the type checker can make sure that you aren't trying to use an inexistent (member) function - without ever attempting to instantiate your generic code.

It is very unfortunate that F# got rid of this important feature.

> Yep, that's why everyone wants Concepts to be standardized.

What I'm telling you is other languages have had this very same feature for well over two decades.

> I'm still not clear what the issue is here.

Here's a thought exercise: Design an API for manipulating graphs, nodes and edges. The concrete representation of these three types must be hidden from the user by language-enforced mechanisms. Using `friend` is not allowed. Using the pimpl pattern is not allowed. Bypassing type safety is not allowed.

The fundamental problem that you'll run into is that C++'s access levels work with one type at a time. ML modules don't have this problem, because a single signature can specify multiple abstract types. An implementation can have access to the representations of all three types (graph, node, edge), while at the same time hiding these representations from all clients.

> Are you talking about something like Go interfaces?

Yes.

> For my use cases static polymorphism is generally preferable to dynamic polymorphism and in those cases where dynamic polymorphism is desirable C++ style interfaces are often sufficient.

I agree: static polymorphism is preferable whenever possible. It is easier to reason about both for language users (who care about correctness) and compiler writers (who care about optimizations). However, sometimes you want dynamic polymorphism, and that's what objects (in the object-orientation sense, which you can think of as “thingies that have vtables”) are for.

I think the general consensus of the C++ community is that we need Concepts or something like it. I don't think anyone is really claiming no other language offers something similar. I'd love to learn more Haskell but for my primary domain (games and VR) it's not a terribly practical option. This gets back to my original point - yes Concepts will make C++ more complex (in the sense of adding features) but I think it will make the language better / simpler to use in practice and I look forward to it being standardized and widely available so I can use it.

> The concrete representation of these three types must be hidden from the user by language-enforced mechanisms.

This seems to be more an issue of encapsulation than support for ADTs. There's value in hiding concrete representations (ABI compatibility) but C++ works well for my use cases most of the time with concrete representations visible (and this helps with performance which is important in my domain).

As a language user I don't think it's just compiler writers who care about performance :)

I do see the value in something like Go interfaces although it's not a problem I encounter that frequently in my domain. Type erasure is a handy technique in those situations and I think better language support to eliminate some of the boiler plate is desirable. More complexity :)

> Here's a thought exercise: Design an API for manipulating graphs, nodes and edges. The concrete representation of these three types must be hidden from the user by language-enforced mechanisms. Using `friend` is not allowed. Using the pimpl pattern is not allowed. Bypassing type safety is not allowed.

Perhaps I am not understanding your example, but...

I'd have a Node class, an Edge class, and a Graph class. None would be friends of any of the others. Many of the API functions would take a Graph (plus other parameters), but you might like them to be able to operate on an Edge or even a Node as well. But the way I think you handle this is by having a conversion operator (which is also a constructor). That is, if I have a Graph constructor that takes an Edge parameter, and one that takes a Node parameter, now I can use an Edge or a Node as a parameter to a function that takes a Graph.

Note that this does not require Graph to know about the internal details of Edge or Node. Also, it does not bypass type safety. It does sometimes return a different type than you passed in, but I'd argue that you want that: If you call addEdgeToGraph you expect to get a Graph back, and you will, even if you passed in a Node in place of the Graph.