
Ill-Advised C++ Rant, Part 2 - vaidyk
http://www.codersnotes.com/notes/cpp-rant-2/
======
alschwalm
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.

------
porges
> Calculating the size of a static array.

std::size is in C++17:
[http://en.cppreference.com/w/cpp/iterator/size](http://en.cppreference.com/w/cpp/iterator/size)

------
jonstewart
I mean, finally getting standard networking and filesystem libraries is a
really nice thing...

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

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

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

~~~
kibwen

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

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

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

~~~
cousin_it
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 :-(

~~~
Manishearth
> It's a pretty sweet design

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.

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

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

There's an example in the rfc: [https://github.com/rust-
lang/rfcs/blob/master/text/1236-stab...](https://github.com/rust-
lang/rfcs/blob/master/text/1236-stabilize-catch-panic.md#background-what-is-
exception-safety-in-rust)

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/](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.

~~~
cousin_it
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 :-)

~~~
Manishearth
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). :)

~~~
cousin_it
You can see the invalid state even without catch/recover, e.g. from
destructors that are called during unwinding.

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

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

~~~
ryandrake
What possible use is there for being able to convert the name of an enum as a
string that isn't totally bug-ridden the minute you write it?

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

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

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

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

Also don't forget [Duff's
device]([https://en.wikipedia.org/wiki/Duff's_device](https://en.wikipedia.org/wiki/Duff's_device))
for how it could actually be expressive.

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

~~~
chrisseaton
If you could write it as a function why would you be writing a macro in the
first place?

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

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

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

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

~~~
maxlybbert
I recognize that there is some use for macros, but the valid cases have been
getting smaller over time.

------
chris_wot
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/](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...](http://www.open-
std.org/jtc1/sc22/wg21/docs/papers/2015/n4428.pdf)

    
    
      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 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<C>::class_members::get<I>
      Requires: I >= 0 && I < size
      
      Provides information about the I’th (zero­indexed) 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.

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

------
johnydepp
do you realize this would crash with empty array: #define countof(X)
(sizeof(X) / sizeof((X)[0]))

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

~~~
stormbrew
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..._

~~~
hendzen
Before the array decays to a pointer, you can get its length.

Here is the implementation of std::begin for arrays in libc++:
[https://github.com/llvm-
mirror/libcxx/blob/60d223df071f6e3d4...](https://github.com/llvm-
mirror/libcxx/blob/60d223df071f6e3d4ffa29331ed466fff563096f/include/iterator#L1430-L1436)

~~~
stormbrew
Quite right, I'm apparently rusty on my stdlib knowledge. Didn't know about
the reference trick.

