Live data from Hacker News

Ill-Advised C++ Rant, Part 2

codersnotes.com

41–50 of 66 posts

Re: Ill-Advised C++ Rant, Part 2

#41
post #26
post #20

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.

> 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 encourages writing imperative code in a functional style, like this:

    fn run_query() -> Result {
        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.

Re: Ill-Advised C++ Rant, Part 2

#43
post #41
post #26

Earlier quoted context omitted.

> 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 encourages writing imperative code in a functional style, like this: fn run_query() -> Result { 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…

> 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.

Re: Ill-Advised C++ Rant, Part 2

#44
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:

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::enumerators::size
  The number of enumerators in the enumerator list of E.
3. Same deal, see in the same proposal:

  std::enum_traits::enumerators::get::identifier
  A std::string_literal(N4121) holding the identifier of the enumerator. 
  The identifier is encoded in UTF­8 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::class_members::get
  Requires: I >= 0 && I 
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.

Re: Ill-Advised C++ Rant, Part 2

#45
post #34

Earlier quoted context omitted.

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. But maybe that's just me.

No, it's not just you. The more I look at Rust, the less I like. -- 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…

  > 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).

Re: Ill-Advised C++ Rant, Part 2

#46
post #41
post #26

Earlier quoted context omitted.

> 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 encourages writing imperative code in a functional style, like this: fn run_query() -> Result { 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…

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.

Re: Ill-Advised C++ Rant, Part 2

#47
post #20

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.)

Re: Ill-Advised C++ Rant, Part 2

#48
post #41
post #26

Earlier quoted context omitted.

> 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 encourages writing imperative code in a functional style, like this: fn run_query() -> Result { 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…

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".

Re: Ill-Advised C++ Rant, Part 2

#49

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: http://blog.natekohl.net/making-countof-suck-less/ Of course, knowing how many elements are in an array is probably not…

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.

Re: Ill-Advised C++ Rant, Part 2

#50

Earlier quoted context omitted.

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.

Post reply on HN