Modern C++ for systems, part 2

C++ was always a decent language for low-level programming. Maybe not the best, but far from the worst. Now, with Modern C++, it gets even better. Newer versions of the standard have simplified some of the more complex portions of the language, while playing to its strengths. In this post, we’ll look at a couple of these strengths, and see how modern versions of C++ only make them that much stronger.

Types

How a language treats the types of values is one of its defining characteristics. At the highest levels (JavaScript, PHP, etc.), types can be so loosely defined that you barely even know they’re there…until they blow up in your face. But on a lower level, when you want to eke out just a little more performance, or where safety is of the essence, you want a strong type system.

C++ provides this in multiple ways, but older C++ was…not exactly fun about it. Variables had to have their types explicitly specified, and some of those type names could run to absurd lengths, especially once templates got involved. Sure, you had typedef to help you out, but that really only worked once you knew which type you were looking for. It was ugly. You could very easily end up with something hard to write, harder to read, and nearly impossible to maintain.

No more. That’s thanks to type inference, something already present in a number of strongly-typed languages, but brought into the core of C++ in 2011. Now, instead of trying to remember exactly how to write std::vector<MyClass>::iterator, you can just do this:

auto it { myVector.begin() };

The compiler can tell what type it should have, and you probably neither need nor care to know. For temporaries with ridiculously long type names, auto is invaluable.

But it’s good everywhere, and not only because it saves keystrokes. It’s safer, too, because declaring an auto variable without initializing it is an error. (How could it not be?) So you know that, no matter what, a variable declared auto will always have a valid value for its type. Maybe that won’t be the right value, but defined is almost always better than undefined.

The downside, of course, is that the compiler may not get the right hints, so you may need to give it a little help. Some examples:

auto n { 42 }; // type is int
auto un { 42u }; // type is unsigned int

auto db { 2.0 }; // type is double
auto fl { 2.0f }; // type is float

auto cstr { "foo" }; // type is const char *

// (C++14) ""s literal form
auto stdstr { "bar"s }; // type is std::string

That last form, added in C++14, requires a bit of setup before you can use it:

#include <string>
using namespace std::string_literals;

But that’s okay. It’s not too onerous, and it doesn’t really hurt compilation times or code size. After all, you’re probably going to be using C++ standard strings anyway. They’re far safer than C-style strings, and we’re after safety, right?

That’s key, because one of the benefits of Modern C++ over C is its increased safety. Type safety, prevention and warning of common coding errors, we want these at the lowest level. And if we can get them while decreasing code complexity? Of course we’ll take that.

Using the compiler

Unlike more “dynamic” languages, C++ uses a compiler. That does add a step in the build cycle, but we can use this step to take care of some things that would otherwise slow us down at run-time. Languages like JavaScript don’t get this benefit, so you’re stuck using minifiers and JIT and other tricks to squeeze every ounce of performance out of the system. With the intermediate step of compilation, however, C++ allows us to do a lot of work before our final product is even created.

In past versions, that meant one thing and one thing only: templates. And templates are great, until you get into some of the hairier kinds of metaprogramming. (Look at the source code to Boost…if you dare.) Templates enable us to do generic programming, but they’re easy to abuse and misuse.

With Modern C++, we’ve got something even better. First off, compilers are smarter now. They have to be, in order to handle the complexities of the language and standard library. (People complain that C++ is too complex, but modern versions have hidden a lot of that in the underbelly of the compiler.)

More important, though, is the notion of constant expressions. Every compiler worth the name can take an expression like 2 + 2 and reduce it to its known value of 4, but Modern C++ takes it to a whole new level. Now, compilers can take some amazing leaps in calculating and reducing expressions. (See that same video I mentioned in Part 1 if you don’t believe me.)

And it only gets better, because we have constexpr to let us make our own functions that the compiler can treat as constant. The obvious ways to use this capability are the old standbys (factorials, translation tables, etc.), but almost anything can fit into a constexpr function, as long as it doesn’t depend on data not available at the time of compilation. With C++14 and 17, it only gets better, as those did away with the requirement of writing everything as a single return statement.

With const and constexpr, plus the power of templates and even metaprogramming libraries like Boost’s Hana, Modern C++ has a powerful tool that most other programming environments can’t match. Best of all, it comes with essentially no additional run-time cost, whether space or speed. For low levels, where both of those are at a premium, that’s exactly what we want. And the syntax has been cleaned up greatly since the old days. (Error messages are still a bit iffy, but they’re working on that.)

Plenty of other recent changes have eased the workload for low-level coding, even as the high levels have become ever simpler. I haven’t even mentioned some of the best parts of C++17, for instance, like constexpr-if and if initializers. But that’s okay. C++ is a big language. It’s got something for everybody. Later, we’ll actually start looking at ways it helps make our code safer and smarter.

Leave a Reply

Your email address will not be published. Required fields are marked *