loading...
Cover image for Modern C++ Isn't Scary

Modern C++ Isn't Scary

jdsteinhauser profile image Jason Steinhauser ・6 min read

C++ has a reputation for being an obtuse, antiquated language that is only for neckbeards and angry professors. Sure, it's fast, but why do you want to learn all of that nasty overhead just for a little more speed? And I love my package manager and tooling! Does C++ have anything modern?

This is exactly how I felt after every time I looked at C++ or worked on a C++ project since I graduated college. However, I've had to write a considerable amount of C++ recently and I have to say...

C++17 ain't your dad's C++.

Less horrendous syntax

C++ is notorious for its horrendous syntax. However, starting with C++11 the syntax has gradually been made much more appealing. A lot of it involves the use of the keyword auto. You can now use range-based for loops like in Python and C# (technically foreach):

double arr[3] = { 1.0, 2.0, 3.0 };

for (auto& x : arr) {
    std::cout << x << std::endl;
}

The & after auto signifies that we don't want to make a copy of the item in the array (or std::vector or any other iterable collection).
Auto can be used to destructure std::tuples and std::pairs, as well as structs and arrays! If we use our array above, we can destructure the whole thing:

auto [a, b, c] = arr;
// auto [a, b] won't compile because our array is length 3

You can use it in for loops on std::maps as well:

for(const auto& [k,v] : dictionary) {
...
}

Destructuring tuples works the same way:

std::tuple<double, std::string> t{ 4.0, "GPA" };
auto [value, label] = t;

That tuple looks to be defined in standard, verbose C++. Well, they've got a fix for that too...

Template Parameter Inference

If the type can be inferred in a template type, then there is no need to specify the types! I can rewrite the std::tuple definition above as:

std::tuple t{ 4.0, R"(GPA)" };
// or better yet
auto t = std::tuple{ 4.0, R"(GPA") };

The R"(...)" creates a string literal instead of a character array. This is considerably less bulky than previous iterations of the C++ language standard, and it makes type signatures a lot less irritating. You'll still have to fully qualify the type on function return types, but you can auto the pain away everywhere else.

The language standard committee has also added std::tie to integrate with pre-existing code as well, where you want to tie values as output of a function to variables that already exist. The language has really made it MUCH nicer than it used to be regarding functions that have multiple output values, and I'm quite satisfied with it.

Literals

C++ has long had some literals for integer and floating point types (i.e., defining zero as an unsigned long as 0UL or as a float as 0.0f). In C++11/14, have expanded the scope of literals to include booleans, strings (both UTF-8 and UTF-16), units of time (42ns is the same as std::chrono::nanoseconds(42)), and a lot more! There is also the option for user-defined literals, and the documentation is pretty solid. This has been one of the more exciting features for me personally!

More platform independent libraries

Several things were classically OS-specific in C++; there were no overarching abstractions. Thankfully, in C++11 and beyond that has been remedied.

For example, if you wanted to sleep the current thread for 5 seconds, in grandpa's C++ you'd write:

#include <unistd.h>

...
void sleep_func(unsigned int milliseconds) {
    usleep(milliseconds * 1000);
}
...

unsigned int sleep_seconds = 5;
sleep_func(sleep_seconds * 1000);

and that would work for *nix environments only. If you wanted to do sleep for some unit other than microseconds (the Unix implementation's unit of measure), you would have to do the conversion yourself. While not difficult, it's still prone to human error. If you wanted to do it on a specific thread... well, that's a whole different story. In C++11, you can use the std::chrono library, with a variety of clocks, as well as the std::thread library for thread-specific tasks.

#include <chrono>
#include <thread>

...
void sleep_func(std::chrono::duration duration) {
    std::this_thread::sleep_for(duration);
}
...

auto sleep_seconds = 5;
sleep_func(std::chrono::seconds(sleep_seconds));

There are several pre-defined durations in the std::chrono namespace also, so that it is incredibly clear to see how the units of your time span, and all the conversions are handled by the compiler. Less work for us!

They've also finally implemented a filesystem abstraction in C++17! It was experimental in C++14 but officially became a part of the language standard with the last release.

Creating objects in template classes

In the "good ol' days" of C++, using template collections was super annoying. If you wanted to push something to the back of a queue, for instance, you would have to create the object and then pass a copy of that object into the queue.

std::vector<thing> things;

for (int i = 0; i < 50; i++) {
   thing t("foo", i);
   things.emplace_back(t);
}

A lot of template classes now have functions that would copy previously that have an Args&&-style implementation now so that a new object of that type can be created in place in the template class! That looks something like:

std::vector<thing> things;

for (int i = 0; i < 50; i++) {
    things.emplace_back("foo", i);
}

This saves some copying overhead and speeds things up as well, and discourages the use of pointers in collections (when appropriate).

Better Pointers

Let's face it: dealing with raw pointers SUCKS. Allocating memory. Freeing memory. Creating a pointer in one scope and assuming ownership transfers to a new scope later. All of these cases require a lot of ceremony, can cause memory leaks, and require a lot of mental overhead to make sure you don't cause memory leaks. Thankfully, C++11 brought different pointer types developed in the Boost library into the language specification.

Unique Pointers

std::unique_ptr<T> can only ever be referenced by one object. If ownership needs to be transferred to a different scope (but still only maintain one copy), std::move can be used to transfer ownership. This can be useful in factories, or any other time that you might want to create something and pass its ownership to another object. For example, you may want to create a stream of bytes coming off a Socket and pass the ownership of that data to a requesting object.

Shared Pointers

std::shared_ptr<T> are officially one of my favorite things in C++. If some point needs to be referenced by multiple objects (like, say, a Singleton for a Websocket), in old school C++ you would've created a raw pointer to that object and destroy it on cleanup or after its last use... hopefully. Raw pointers are one of the single biggest contributors to memory leaks in C++. They probably rank up there with NULL pointers as a billion dollar mistake.

Thankfully, shared pointers are now a thing and widely accepted in the C++ community. When copied (and they should always be copied, not passed by reference or pointer), they increment an internal reference count. When these copies are destroyed, the reference count is decremented. When the reference count reaches zero, then the object is destroyed and the memory is freed up. No more manual memory management hassle! Yay! You can still shoot your foot off with shared pointers if you don't strictly copy them, but there's a better safety mechanism available now over shared, raw pointers, IMHO.

Conclusion

Though it still feels like it has more syntax than necessary, C++ is not as bad as I remembered it being. I've enjoyed the past 4 months developing in it on a daily basis much more than I have in past jobs. If you check it out again, hopefully it won't be as scary to you either!

Happy coding!

Posted on by:

jdsteinhauser profile

Jason Steinhauser

@jdsteinhauser

15+ years of analysis and development. Father of 3. Passionate about testing, functional programming, and pretty graphs.

Discussion

markdown guide
 

What's scary about C++ is not the syntax. Yeah it looks a bit weird these days and all but that's not the problem. (I programmed in C++ for 10 years)

The problem is best illustrated with books like "Effective C++" and "More Effective C++" aka. "The 100 things you have to bear in mind to avoid inadvertently creating a memory leak".

With the addition of more features in the language I'm pretty sure they've created another 100 things that you need to bear in mind to avoid implicit memory leaks. So the surface area of expertise needed to do this increases.

At some point it just becomes easier to select a cleaner language.

 

This is exactly it!! There seem to be 2 ways to do memory: the first is to let the developer handle memory like in C. The second is to let the language handle memory, like in GC languages or in Rust.

If you let the developer handle memory that's all fine. Using malloc() and free() is actually easy once you get used to it. You have to figure out when objects get created and when they get destroyed, and plan things out accordingly, but it generally works out. Which is why memory leaks in C programs are rare and, for good code, easy to fix.

Letting the language handle the memory is also fine. You get a performance hit, at least traditionally with GC (can't speak for Rust. haven't used it), but in most cases it's worth it to free the developer of having to think about memory usage, and allow him to spend more time thinking about other problems or writing code.

However, what you get in C++ is an attempt to get the best of both worlds. It does this, not by having some sort of unifying idea like Rust's ownership thing (again, haven't used), but by adding on features (stuff the developer has to worry about), as if that's going to fix anything. It seems only to be digging a bigger hole for itself.

 

The problem isn't with malloc and free (or new and delete in the case of C++).. but with things like implict object creating when returning values the wrong way, or by improperly setting up copy constructors and things like that. Or the need to make destructors virtual if you use inheritance (don't do it? memory leak)

If it only was a matter of new/delete then it would be simple, like C is.
C++ is unwieldy.. too many rules. There are better, more productive languages these days.

 

I am extraordinarily confused when it comes to the C++ community. On the one hand they encourage code reuse: it's OOP after all. On the other, code becomes obsolete if it's not using the latest practices (last 5 years).

 

At some point, most languages take a turn for the better, or for the worse. Maybe C++ is finally heading in the right direction. Thanks

 

I've been considering starting a C++ project about 5 minutes ago and what discouraged me in the end is the absence of dependencies management. Is there any good solution for that? Development inside Docker? An emerging dependencies management solution?

Thanks :)

 

A lot of libraries are being built with CMake now, which makes it pretty easy to do dependency management. I've also had good luck with vcpkg from Microsoft. They're not as full featured as yarn, mix, or Maven yet, but they're a lot better than a decade ago!

 

Oh yes CMake is definitely a step forward, but you still need a package manager (other that apt I mean). I've looked a bit, there is things like Conan coming up but I have no idea the actual value of them.

 

Amazing post! I also wanted people to know that Modern C++ is great and should be used more frequently than Traditional C++ but there are very less people who know about it as no good books and documentation for beginners.

I myself have learned from 5 blogs, GitHub, drafting papers, some books etc.

 

Excellent! I'm glad to hear that it was helpful for you!

C++ does have a ways to go to have the tooling that languages developed decades later have. I think a lot of it has to do with backwards compatibility, and honestly I'm glad to see languages like Rust and GoLang come along and try to address those issues in new ways.

And I agree, leave C alone! It does exactly what it needs to do, and quite efficiently.

Other languages just take care of this stuff.. you don't need to worry about it.
C++ is by default leaky.. its like a gun that unless you aim it perfectly will always shoot you in the foot.
Its possible to write good code, but there's a lot to take in to get it right.

I'd say if you don't declare your destructor virtual, you've made a dangerous coding error and the compiler should tell you so. But everything is on manual in C++ (except for the proliferation of memory leaks).

Leave our beloved C devoid of any new unnecessary "features" 😁

 

Great post, as someone who has been eyeing C++ from the sideline I've indeed seen a lot of improvement with C++14 and especially 17, enough to make me want to start using it more.

I'm still waiting for modules to drop before I really dive into C++ though. The whole headers & include system is just a little too archaic and messy for my taste (coming from JS modules and Rust's module system).

 

Modules are getting a little better. vcpkg from Microsoft does a decent job, and a lot of projects that are built with CMake can be included directly from their Github repos like GoLang.

 

I spent a while in modern c++ I liked it but it's still really verbose. I am working in rust and find the experience more enjoyable. But don't let that stop anyone modern c++ really is easier.

 

Tbh I see this synthax way worse then the old one. But it should be my bias...

 

I completely respect your opinion. I'm curious to know what you don't like about it though!

 

Well I started using c++ a lot of years ago and I've never found the new auto/for stuff really terse. When I say I prefer the old synthax I mean that in c++ I always iterate over indeces 'for(int i...)'.
Other languages are different. I heavily use 'array.map()' in JS, or a ton of 'foreach' in .net, and analogous constructs in Python.
But c++... No they have added more noise to my eyes. It is a matter of taste, nothing really technical: just taste.

The same goes with the whole pointer stuff. Ok, smart pointer are a thing. But when I code with pointers I prefer to stay low level... It chrushes my mind to understand all the cool new stuff and I end adding more bugs!

Let consider that nowdays I use c++ for hi perf computations only (machine vision) so the context can also add bias...

 

This definitely makes me want to (finally) take a look at C++. Thanks for sharing, this is awesome!

 

more like unnecessarily evil..

for example if you do inheritance you better remember to make your destructor virtual. Lots and lots of these rules..

 

Maybe, but try to convince my manager...

 

What does your manager want to use? Or is he/she/they just wary of C++ in general?