Ill-Advised C++ Rant, Part 2
codersnotes.com
codersnotes.com
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.std::size is in C++17: http://en.cppreference.com/w/cpp/iterator/size
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.
> 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?But maybe that's just me.
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.
-- functions can't capture free variables in lexical scope. They need an entirely different type and syntax for that (closures).
-- try reading some non-trivial Rust code that uses generics - it's just as incomprehensible and unmaintainable as C++.
It's a huge disappointment.
> 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).That's not entirely that surprising due to the types of problems Rust tries to solve.
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:
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.
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.
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.
> 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.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".
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 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).
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).
There's an example in the rfc: https://github.com/rust-lang/rfcs/blob/master/text/1236-stab...
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.
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.)
That said, there are some reasonable points, like '#pragma once' and 'enum-to-string' (extreme disagreeage on the 'string-to-enum' direction tho).
Also don't forget [Duff's device](https://en.wikipedia.org/wiki/Duff's_device) for how it could actually be expressive.
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.
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.
Nobody in this thread has said that.
But yes, it would be good. But if you want to know why that macro is so problematic, have a read of the following:
http://blog.natekohl.net/making-countof-suck-less/
Of course, knowing how many elements are in an array is probably not a bad feature. (just noticed that porges points out that it's coming in C++17)
2. Completely agree with you on enums. There is a proposal in C++17 to allow for this, see:
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n442...
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.
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.
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.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...
Here is the implementation of std::begin for arrays in libc++: https://github.com/llvm-mirror/libcxx/blob/60d223df071f6e3d4...
int a[] = {4,5,6};
for(int i : a)
;
That is how the above works.One of the uses for countof is (somewhat ironically) doing a enum->string table where you want to enforce the table gets manually updated properly:
enum Color { red, green, blue, num_colors };
const char *color_string[] = { "red", "green", "blue" };
static_assert(countof(color_string) == num_colors);