Use C++ Better!

December 30, 2018

Writing C++ on its own isn’t that difficult; despite the language’s complexity and quirkiness, I’d argue it’s pretty easy to get a grasp on. But it’s that complexity and quirkiness that I think makes good C++ harder to come by. It’s easy to toss away allocators, create costly abstractions, and introduce memory leaks for quickly-hacked, pseudo-elegant code that “just works”.

And sure, it works, but that stuff’s not helping you improve yourself as a programmer. Writing good C++ is a frustrating, fantastic way to improve yourself as a programmer. While I am nowhere near an expert on the language, here are some thingsSome of these apply to programming in general, not only in C++. I previously didn’t care about that now have made me appreciate C++ and use it well (or at least better).

1: Don’t abuse the STL

Alright, so here’s the scene: you pull up to class, and your prof drops a godsend BOMB on you - a set of a hecka pretty, efficient, convenient abstractions known as the STL. You will never write a container or adapter again! for loops and imperative styles are things of (literally) yesterday!

Ideally, and hopefully, this will be the case - that’s what the STL is there for! But realistically, some problems are easier solved the “classic” way than through these fancy abstractionsIn fact, this has been a recent cause of controversy in one the modules introduced to C++2a. .

Consider two implementations of a function that finds the index of a value in a vector:

It’s more confusing, and actually, less efficient! The for_each version must iterate through the entire vector, even if it finds val earlier on.

I’ll certainly admit that this contrived, and that the most elegant way to write this function actually relies on the STL
auto valItr = std::find(vec.begin(), vec.end(), val);
auto dist = std::distance(vec.begin(), valItr);
. But you get the point. There’s a reason the world doesn’t run on remove_copy_if.

2: Move semantics are ur friend

Most don’t learn about move semantics until later in their C++ journey, as it’s often not mission-critical to students. Moves are incredible for reducing costIn fact, they’re the reason assignment operators are advised not to return const references since C++11. in function calls and passing values. The pass by value then move idiom is one I didn’t pick up for a while, but that is extremely useful:

If the member type B is move-constructable and cheap to move, this is a great pattern to use - at worst, one copy and one move will occur, which with the assumptions above is cost-equivalent to one copy. And you get the bonus of reducing constructors to 1.

Note that this is not a hard-and-fast rule - if B is not move-constructable, or moving a B is basically a copyFor instance, if it contains a fixed-size array , then you have two copies on your hand, which sucks. If in doubt, experiment and profile for your use case.

3: lldb is ur best friend

I don’t think this one has any caveats - learning a great debugger like lldb or gdb will exponentially reduce the amount of time you spend filling holes in your projects, teach you about the evaluation of C++ programs, and make it easier for you to approach large (read: enormous) codebases where one bug can span thousands LOCs.

There’s no demonstration or exploration for this one. Just do it, get good at it, and consider a powerful IDE like CLion or Code::Blocks to supplement your skills.

4: Don’t expect defined undefined behavior

C++ has a lot of undefined behavior you can’t expect to execute the way you want. The common examples here are array bounds and no primitive default initialization, but I will present what may be a lesser-known one: undefined expression evaluation order.

Imagine we are parsing a set of key-value pairs in a mutable-state parser. Of course, we would want to parse the key first, then the value, then create a representation of this pair. This might look something like:

But the standard doesn’t define the order of evaluation in parts of an expression. In most cases, like numerical addition, this is okay - but here, expectation of evaluation order could be catastrophic. If parseValue() is evaluated first, at best the pair representation will be incorrect. At worst, the program blows up.

The really nasty thing about this you often don’t discover such undefined behavior until after much experimentation, because this may work fine for one application or compiler and only bounce later, on another. My recommendation is to always separate concerns, read up on uncertainties, and experiment!

5: boolean zen

We’ve all seen ridiculous conditionals like this:

But what about more subtle indirections?

Learning boolean zen requires good knowledge of language APIs and maybe is not a big deal, but can equally lead to writing cleaner, clearer, more maintainable code. Not to mention approval from the boolean buddhaGrace DePietro, a la Roth .

6: ___ is not ___

Last but not least, as the most general and harshest note, <THING_A> is not <THING_B>. In Chemistry, Hydroboration-Oxidation and Oxymercuration-Demercuration both add water across an alkene, but do so very differently and are used for unique reasons. In computer science, all general-purpose languages may be used accomplish the same thing, but often have individual advantages that make them more or less useful for a particular domain. Let’s look at a couple examples.

The first is a common scenario: you want to prototype something quickly, or are in some kind of time-sensitive competition, and you need to read a file to a string. In C++, you would probably write something like

Okay, cool! 10 lines, not too bad! But then you look over, and the woman next to you wrote this bad boy:

Yeah, she’s got you beat in prototyping speed and abstraction. Sure, the Ruby interpreter will never come close to the speed of C++ binaries, but in this situation that doesn’t matter. C++ is not Ruby.

It’s important to recognize that different languages have different feature sets, and to not get caught up trying to do something the particular way another language does - that can often be impossible. For example, consider a visitor pattern for an enum in Rust:

ADT-style enums do not exist in C++, so to mimic this pattern, we must think about what similar useful features C++ has. Of course, the most powerful one for this problem is polymorphism.

As an interesting note, this kind of implementation cannot be replicated in Rust, which does not support reflection. So the independence is mutual!

Rah rah, and C++ is not Rust.

<< All Posts   :dolphin: