Just to note, the proposal the author lists as "so complicated" is actually just fixing an inconsistency in the grammar where "class" and "typename" are not always synonymous in the context of template declarations. Specifically:
template<class T> class Foo;
template<typename T> class Foo;
Are the same. But, not when declaring a template with a parameter that is itself a template:
template<template<typename> class T> struct Foo; // Compiles
template<template<typename> typename T> struct Foo; // Does not compile
The proposal allows the second form to compile, fixing this inconsistency.
It depends on your definition of "standard." POSIX is a standard, and it's the standard nearly everybody has used for networking and filesystem access.
Yeah, writing C++ code that works on both Windows and *Nix means I can't use straight POSIX, and usually wind up with Boost ASIO and Filesystem. Putting them into the standard library means I've got two fewer libraries I need to worry about in autoconf.
I tend to agree, but gave up on fixing C++ a decade ago.
I hope Rust is the future; it deals with all these issues. But Rust seems to be starting out at the complexity level it took C++ two decades to achieve.
Even supposing that Rust and C++ have the same level of complexity, the style and consequences of their complexity are wildly different. A lot of the various pieces of complexity of C++ needs to be held in the head of a human who is writing it, but most of Rust's complexity can be left to the compiler and humans only need to page in the pieces the compiler points out.
C++'s complexity often manifests as, basically, a list of rules[1] that the programmer should follow perfectly to reduce the risk of their compiled code not doing what they want. A lot of C++'s hairiest complexity is driven by weird interactions between "independent" language features (often driven in part by the goal of C backwards compatibility), and other complexities are just driven by the C-style mindset of hoping/requiring that programmers don't make mistakes. A lot of these (especially in the latter category) result in code that compiles fine, but misbehaves at runtime.
On the other hand, neither of these categories apply to Rust: Rust's complexity is mostly pushed into the compiler, which tries to flag problems early. Instead of having a list of rules they need to follow themselves, programmers have a compiler that checks their code against the list (the result ends up being somewhat similar to the C++ core guidelines). Computers are far less fallible than humans, and so programmers can have more trust that they won't miss something. Of course, this definitely can have the effect of making writing code seem hard because it forces the programmer to resolve/defend against a lot of errors up front.
[1]: https://github.com/isocpp/CppCoreGuidelines/blob/master/CppC... (I should say that a lot of these rules are great, and apply more broadly than just C++, but significant chunks are "work-arounds" for things C++ compilers don't check and things that the standard library often doesn't help with either, particularly in the resource management and concurrency sections.)
> Rust seems to be starting out at the complexity level it
> took C++ two decades to achieve.
You say this every time that Rust is compared to C++ (which is a lot!), but I have yet to see an elaboration. What in particular are you talking about?
Rust doesnt have objects, but otherwise is very compelling. I still use C++ but I'm also looking forward to the day (soon) where I can use Rust instead for many projects. With more languages coming to LLVM I expect there will be other solutions as well.
C++ took years until it got a ranged for loop. It was so important you had frameworks like Qt providing their own preprocessor to support this functionality. C++ is a language that has a lot of baggage to be backwards compatible and incorporates multiple turing complete languages. When was the last time functionality was removed from C++ to support a better design?
Why the hell do we still have header files with raw includes? That's about as sophisticated as copy and paste. The macro preprocessor is stuck in the 70s, probably because the C++ grammar is so hard to parse to write something better. Most template metaprogramming outside of generics is garbage that could be replaced with a better preprocessor and usually the result of premature optimization or avoiding deficiencies in the language and creates custom ad-hoc knowledge that isn't easily transferred to other programmers.
Rust has a better preprocessor, cleaner design, no includes, and generics without template metaprogramming. I shouldn't have to mention it but it has things that most modern languages do like ranged for loops, flexible switch/match statements, clojures, and automatic type inference. It doesnt have virtual methods, template metaprogramming, or exception handling.
If you're curious, I'd recommend flipping through the syntax guide, there are a lot of well designed concepts:
Indeed, but you're refuting Animats' comment, and you and I happen to be in agreement that Rust and C++ are nowhere comparable in terms of language complexity (largely due to C++'s C-compatibility and the burden of 30 years of backwards-compatible language evolution).
My original question still stands, because I'm curious to know how he perceives Rust's complexity to be equivalent to that of C++'s.
Ah I read his comment as a compliment to Rust, but upon re-reading it seems like he wasn't referring to the "complexity" of Rust as a good thing - what he views as complexity perhaps I view as good design :)
Rust encourages writing imperative code in a functional style, like this:
fn run_query() -> Result<PgResultSetOrWhatever, String> {
PostgresConnection::connect("postgres://localhost:5432/postgres", &NoSsl)
.and_then(|conn| conn.prepare("SELECT ir FROM x"))
.and_then(|stmt| stmt.query([]))
.map_err(|e| format!("{}", e))
}
This is a strange way to write control structures. Each object gets to define its own control structure syntax. Then there's the "try!" macro, which generates an invisible return on error. Cargo has their own "try!" macro, and it's slightly different. All this puts a layer of macros on top of control flow. There's a rationale for that, but it doesn't help readability.
Exceptions were such a mess in C++ that they've scared people away from the concept. But they work well in Python, especially in conjunction with "with" clauses. The machinery in Rust to avoid exceptions is more complex than exceptions. It took C++ years, and Boost, to get to this level of wallpapering over a mess.
> Each object gets to define its own control structure syntax.
This is true in every language: types get to define their API, and languages with first-class functions (or something similar) get to define things that act like a control structure.
In any case, almost all interactions like that go through Option or Result, i.e. people are almost never defining them themselves, it's all standard. (Of course, it isn't quite as standard as, say, the Monad and do-notation of Haskell, which cover a sizeable portion of the space of possible reasons to implement ones own control flow, all with the same syntax.)
> Cargo has their own "try!" macro, and it's slightly different.
I don't think this is true, AFAICT cargo just uses the standard one. That said, I have this vague recollection that, a while ago, cargo used to use the current definition of `try!`, while the one in the standard library was strictly less flexible. That is, cargo was serving as a prototype of the generalised try! that's now standard.
> But they work well in Python, especially in conjunction with "with" clauses.
It's not immediately obvious what difference you see between Python exceptions and C++ ones. It seems to me that a lot of the niceness of Python exceptions (conversely, difficulty of C++ ones) are driven by other choices in language design, and Rust generally trends toward C++ for choices like this.
In any case, "with" clauses are... something Rust doesn't have a strong need for, or, more specifically, Rust (and C++) already handles 99% of the use-cases for them. "with" clauses are designed as a way to have scoped-based resource management in a language without timely clean-up, and so don't make nearly as much sense when the language already gives that.
The try macro is going to be replaced by cleaner syntax soon (along with control flow based catch syntax). It's not "a layer of macros", its one macro, which everyone knows about, so it's not invisible. No different from a return or throw statement -- the control flow escape hatch is "invisible" there, too, but everyone knows what a return/throw are, so it's perfectly visible. It's the same situation with try -- everyone knows what it does; so it's not invisible.
Rust doesn't encourage writing things monadically. You can write them as nested if lets if you want; indeed; many people do exactly that (I prefer doing this too, or using try).
Where's the wallpapering? There's try!, and a couple of monadic methods on Result, and that's about it? Monadic error handling is not a new idea, and it's not really complicated either (well, if you force people to understand monads first, it is, but that's totally unnecessary and nobody does that). This is no more complicated than vanilla C++ exceptions. It's different, and different from what people are used to, but not new.
Also, this isn't even part of the language, it's part of the stdlib. If anything that is a point for Rust, since C++ needs language integration for exception handling, Rust doesn't (and thus the language is simpler in this axis). It will soon become a part of the language; but only as some sugar.
And this is a very specific example. Overall, where does "Rust start at C++s complexity level"?
> Each object gets to define its own control structure syntax.
Technically only Result and Option do, the objects above are just Results. While you can create your own enums for error handling, most people don't, so there's no repetition of control flow syntax.
This is true anywhere, each object always gets to define its own utility methods. You have the same on the std::exception types in C++.
> The machinery in Rust to avoid exceptions
This is not "to avoid exceptions", it shouldn't be viewed that way. Sure, the Rust designers don't want exceptions in the language, but monadic error handling is a proper, tried-and-tested solution for error handling, not a "last resort".
For what it's worth, I agree with Animats that Rust's error handling is too complicated.
My dream language would have catchable, unchecked, untyped exceptions. I.e. you can throw and catch strings anywhere without declaring that upfront. You can even throw from a destructor while unwinding, whereupon the new string gets appended to the old string and life goes on. It's a pretty sweet design:
1) Easy to read code with standard control constructs.
2) No performance overhead in the common case.
3) No dispatching on error types, therefore less temptation to use errors for control flow.
4) No distinction between recoverable errors (option) and unrecoverable errors (panic). I feel that distinction is in the eye of the beholder, especially if you need to catch errors from code you don't control.
5) No distinction between code that can cause errors and code that cannot, thus higher-order functions become easier to write.
I'm hard pressed to name any advantages of Rust's model compared to the above. Rust can't even claim to be transparently callable from languages that don't support exceptions, because panics exist :-(
Yeah, it is, but it wouldn't interact well with Rust's safety guarantees :)
(You're analysing this feature in a vacuum, but that's not how language design is done.)
> Easy to read code with standard control constructs.
I think this is in the eye of the beholder, `try!` or `?` or `catch` are (would be -- for the latter two) easy to read for Rust folks. It's just different. do-notation in Haskell is similarly "standard" for functional types, but it's totally alien to C++ folks. The main reason folks find try/catch/throw easier to understand is because they're used to it. Once I learned how enums worked in Rust, `Result` was a very straightforward and simple thing. I actually personally find `try!` easier to read, since I know exactly where a return can happen, unlike C++ where the control flow is totally obscured.
This leads to the rug getting pulled out from underneath you, which can be bad for safety. In fact, Rust's `recover()` needs to be careful about types allowed to cross a recover boundary so that it can be 100% memory safe.
So the code might be easy to read, but not easy to reason about.
> No dispatching on error types, therefore less temptation to use errors for control flow.
I don't get what you mean here. Rust doesn't dispatch on error types, though it does dispatch on Result (which may contain an error). I don't see what's wrong with this. This lets you program in a functional way (and like I mentioned, you don't have to), which isn't a bad thing.
Errors _are_ a control flow thing, you can't escape that. C++ handles the control flow around errors with try, throw, and catch, Rust does it with Result and its methods (and the try macro).
> especially if you need to catch errors from code you don't control.
`recover()` exists. Use it sparingly; it's basically only for cases when you want to catch panics in code you don't control (even then, try other solutions), or stop panics from crossing FFI boundaries.
Panics are supposed to be for irrecoverable or impossible things, where "irrecoverable" is usually a statement that only makes sense in an application, not a library. So this problem in theory shouldn't come up (I haven't seen it happen much in practice).
I think the problem of forgetting to catch an exception from code you don't control because you don't know it's there is a much more pressing problem than having stray panics (since panics are relatively rare).
> No distinction between code that can cause errors and code that cannot, thus higher-order functions become easier to write.
Result<T,E> is also a type. Treat it as any other return type in a higher order function and it works. In `Fn(...) -> T`, `T` can be a Result type, no problem.
> Rust can't even claim to be transparently callable from languages that don't support exceptions, because panics exist
and so does `recover()`.
> No performance overhead in the common case.
IIRC it doesn't turn out to be much, but ICBW. I think someone looked into this.
About higher order functions: putting effects in the type forces you to make a distinction between map and mapM. You can't unify them because one takes a -> b and the other takes a -> m b. Same for any other higher order function, you have to write two versions (or more if you don't have HKT).
About memory safety: can you give an example where a naive implementation of catchable exceptions would break memory safety? I know about RecoverSafe but it seems to be solving a different problem (marking types whose logical invariants are preserved in case of panic).
> forces you to make a distinction between map and mapM. You can't unify them because one takes a -> b and the other takes a -> m b.
You have the same distinction in exceptions? One map would bubble the exception, the other would internally catch and exclude. You don't have to write two versions, map still works for cases with and without Result, but it will not have the additional behavior of excluding errors. You want that additional behavior, hence you write mapM; just like you would in C++.
> About memory safety: can you give an example where a "naive" implementation of catchable exceptions would compromise memory safety? I know about RecoverSafe but it seems to be solving a different problem (marking types whose logical invariants are preserved in case of panic).
In Rust logical invariants are used to enforce memory safety. If the destructor of something within a vector panics, that might result in an invalid vector. There's a pattern colloquially called "pre pooping your pants" which would help in this specific case (http://cglab.ca/~abeinges/blah/everyone-poops/), but not in general. Writing unsafe code in Rust involves careful bookkeeping if you want to be sure the code is safe; and that goes out the window when the rug can be pulled out from underneath you any time.
Well, that example doesn't use catch, so it doesn't really tip the scales on catchable vs uncatchable exceptions. It does show that you have to be extra careful when making function calls from unsafe blocks, and that destructors are especially error prone. I agree with all that :-)
Recover is basically catch, it shows how a panic can leave something in a invalid and unsafe state, which can be accessed after recovery (without recover you wouldn't be able to access it). :)
Ah, I see. Right, that works in this case, but it doesn't work in all -- you can come up with a situation where the destructor is fine but the recovery can lead to arbitrary unsafe things.
dbaupp's gotten here first, but let me add my own. :)
> Then there's the "try!" macro, which generates an
> invisible return on error.
You can't criticize `try!` for this in one breath and then go on to suggest exceptions in the next, considering that exceptions insert invisible returns into your entire call stack. And an exception in C++ can be thrown on nearly any operation imaginable (even assignment!); meanwhile, `try!` is explicit, and a function that uses it won't even compile unless its signature explicitly returns a Result.
As someone trying to learn/experiment with Rust, the syntax is pretty complex. Now granted it comes with some benefits, and I realise there are a limited number of characters available to use (at least that everyone in the world has on their keyboards), but stuff like the lifetime char ' are IMO way too easy to mistake when quickly glancing at code for strings.
Do you happen to be using a syntax highlighter that doesn't recognize the single apostrophe and therefore highlights a huge quantity of code as though it were a string? (This syntax is familiar to OCaml, so it's not like editors are incapable of recognizing this, it's just not usually the default.) This was an annoyance back before text editors had modes for Rust syntax, but I haven't seen it in the wild in years.
If not, then it's simple to know when something is a lifetime vs. when it's a character literal, even if you're quickly scanning. Lifetimes appear in type declarations, so they'll be in struct definitions and function signatures. Character literals can't appear in those positions (and character literals are extremely rare in Rust already, so much so that in the past I actually petitioned to have them removed!).
I also don't think that Rust's syntax is of comparable complexity to C++'s, e.g. the number of uses `const` in C++ (and on a technical level, Rust's grammar is much simpler than C++'s), so I don't think that syntactic complexity is what Animats was referring to.
> leaving off the semicolon on the last statement causes
> that to be the return value of a function.
This is imprecise. Leaving off the semicolon on the last statement of any block causes the block to evaluate to the result of that statement, and functions are blocks. This is familiar from any other everything-is-an-expression language (mostly functional languages, and Ruby as well) and means that, for example, Rust doesn't need both an `if` construct and a ternary operator (as C does), which ultimately means less syntactic complexity (which is what the grandparent is commenting on). Furthermore, the fact that Rust is statically-typed and that it requires function signatures to be explicitly typed means that accidental implicit return values don't invisibly change the semantics of your program as they can in dynamic languages or languages with whole-program type inference. If you forget a semicolon, it will be a compiler error.
> functions can't capture free variables in lexical scope
This is because functions are allowed to be mutually-recursive, which means that you don't require forward declarations in the language (again, reducing syntactic complexity). Furthermore, closures introduce their own costs when operating at the systems level. AFAIK Rust and C++ are the only languages that manage support for closures without requiring heap allocations, and Rust additionally guarantees that your closures can close over references without accidentally allowing the closure to outlive the referent, something which is an important concern when using C++ closures.
> try reading some non-trivial Rust code that uses generics
> - it's just as incomprehensible and unmaintainable as C++
Rust generics are enormously less powerful than TMPL. They're also much more strongly-typed (good for maintainability), produce good error messages rather than the infamous template spew (good for maintainability), and produce errors at the definition site rather than the use site, thus requiring many fewer tests (good for maintainability).
A bunch of those are of the form 'you know there are better ways to go about doing that sort of thing' (like 'maybe consider using a std::vector instead of that static array' and 'just make a minimal ctor for initialization, you'll save typing in the long run' and 'when do you need to know the largest value in an enum, and why can't you just toss in a dummy last element, also why aren't you using enum class') if not outright 'jesus, don't put yourself in a position where you'd want to do that' (like the 'iterate over members of a struct' or the type stuff). And I had never heard of those 'fourcc' codes and have no idea why anybody would want them in a language standard.
That said, there are some reasonable points, like '#pragma once' and 'enum-to-string' (extreme disagreeage on the 'string-to-enum' direction tho).
Constructors are often brought up as a reaction to C99 initializers, but they're really not the same thing at all until and unless we get named function arguments in C++. Constructors as they exist are equivalent to the normal initializer syntax, which C++ does support.
Serialization. And don't tell me that it falls under "bug-ridden the minute you write it" because more defensive programmers than you and I have been using it responsibly in a dozen other languages for more than a dozen years.
He has a some good points, but the "switch" statement needs an integer for a reason... a switch can be converted a jump table in assembly language, so that the "case" can be arithmetically determined and instantly jumped to. The reason being: MOAR SPEED! A jump table is a lot faster than a sequence of else-if statments.
The compiler knows the type of the value it switched upon and generates code accordingly (efficient code on int values, dumb if-chain on other values), doesn't it? Sure it may become hard to tell if the code is efficient or not, but that property has already long been lost with operator overloading.
I don't see why that follows. If switch is given an integer, the compiler could create a jump table, otherwise it can use an if statement block (or some other kinds of implementation). There's nothing in C++ that says certain statements must always have the same compiled code.
So why can't the compiler be smart enough to use a jump table when this is possible, and fall back to a sequence of else-if statements when it's not? The compiler should even be able to make a more optimal sequence of assembly tests and jumps than by writing this code manually.
Who forgets to break in switch statements? I mean yea, there was a time when I started programming I did this once or twice but not for a quite long time. Seems to me more like a request for sloppy programmers than anything else.
I found it funny how many of the complaints were really about the preprocessor. Many of them have already been addressed. If you don't like defining a macro as "do { ... } while (0)", try using an inline function. Even C has inline functions nowadays, and that's been true for more than a decade.
That's an obvious question. A lot of macros should be functions.
The most common case I'm aware of are functions like toupper(), islower(), ispunct(), etc. that the C Standard allows to be macros for performance (the actual work in the function is testing or setting a single bit, and the overhead of a function call really matters in that case).
Inline functions would have the same performance, and you can get a pointer to them without doing the #undef rigamarole you see when a macro definition might muck things up.
Inline functions aren't guaranteed to be inlined. There are compiler specifics that add stronger hints to the optimizer to inline but behavior isn't always consistent from one compiler to the next. The only portable way to force something to be inlined is to use a macro.
Why is it essential that anything is inlined? Maybe if the compiler isn't inlining it had a good reason for that? Maybe it has some understanding of the trade off between specialisation and code size for these particular functions which you don't.
In most cases I would wager I have a better idea of where I want the optimizations to be applied in code than the compiler does.
Macros, unlike inline functions, will always give you the result you are looking for. You don't have to worry about things like regressed performance because a new compiler versions has tweaked inlining heuristics.
The use of "inline" is diminished in much real world C++ too. Because it's used liberally in user code and added implicitly to member functions defined in a class definition there already is much code that is inline-worthy. There is no way in C++ to say "I explicitly want THIS code to be inlined HERE", unless you use a macro.
In theory you can get benefits out of using PGO but PGO is also non-standard, not available on all compilers, and a pain to setup.
Inline functions will also never be able to replace the diagnostics available with the preprocessor; being able to extract the line and file for things like assertions is something you can't do with inline functions.
Inline functions have their use, but so do preprocessor pseudo-function macros, and saying that there isn't a valid use for these macros is claptrap that I'd attribute to someone who hasn't shipped performance sensitive code in C or C++ before.
Because compilers need to work for any program, thus tend to the best common denominator, while you, the programmer, can design something for a particular use case. In this case, hot loops - at best a compiler could know about the runtime intensity by using a runtime profile and then deciding that, yes, in this case it's actually best to inline something. But having a compile -> link -> run (profiled) -> compile -> link workflow is much too bothersome and slow (so most don't do this) and it's much more manageable to sit down and turn on your brain when programming. This stance of "no premature optimisation" has gone overboard IMO.
1. Be more precise. You want the cardinality of the set of items of the array. When you say you want the "size", you sound like you want to know the physical size in bytes of the array.
But yes, it would be good. But if you want to know why that macro is so problematic, have a read of the following:
std::enum_traits<E>::enumerators::size
The number of enumerators in the enumerator list of E.
3. Same deal, see in the same proposal:
std::enum_traits<E>::enumerators::get<I>::identifier
A std::string_literal(N4121) holding the identifier of the enumerator.
The identifier is encoded in UTF8 format, with any UCNs decoded.
3. #pragma once... the entire way of including headers into code is kind of broken. Having to implement a compilation firewall (aka pImpl) just to ensure that when you change a private member definition you need to recompile all other classes that rely on it seems so incredibly broken to me.
The LibreOffice code is littered with pImpls. It doesn't make it easier to read or understand the code, or even maintain it, at least in IMO. And without them, the compilation time is huge, every time I touch VCL code in anger I fear I'm wasting some other poor devs time in compilation time.
4. C99 designators - no opinion on this.
5. Binary - only if you can specify endianness.
6. FourCC doesn't seem like something for a standard... maybe that's just me though.
7. All macro criticisms - someone just please implement another macro processor in the standard already!
8. Iterating fields - back to that C++17 proposal again:
std::class_traits<C>::class_members::get<I>
Requires: I >= 0 && I < size
Provides information about the I’th (zeroindexed) public member
of C that satisfies the above criteria, in declared order.
9. Breaking by default in switch statements... ugh. Lots may disagree with me though. That .. gcc extension is pretty cool though! Add that to the standard, by all means!
10. Agreed on strongly typed typedefs
11. See that C++17 proposal I linked to previously - I think that has everything you'd want! High time too.
Yep that sure looks like a fine C++17 proposal (although I didn't understand a word of how it would actually be implemented).
How much do you want to bet that it won't be accepted? :).
Like a lot of the other good proposals (std::optional anyone...?) I wouldn't be surprised if it never sees the light of day.
No, because the expression is not evaluated at any time. Only the type of the expression is used.
A much better implementation would be
using std::begin, std::end;
#define countof(x) (end(x) - begin(x))
(except for the double evaluation, but this is better as a template function instead of a macro anyway) because it'll work on any type that supports random-access iterators which include arrays but also std::array and vector.
Corrected below, leaving here for posterity and to make sure this conversation isn't confusing in the future. The only thing I'll note is that you get a compile error on a zero length array, so the OP of this chain turns out right in a way heh.
Start ignoring here: Are you suggesting end() and begin() can be called on an array? As far as I know they can't. Apparently you can deduce array length in a constexpr function now in C++11 (which I only learned just now but also couldn't get working quickly, so have some salt with that), but before that arrays always degrade to pointers when passed as function arguments so there's (afaik) no way to extract their length from their type...