Modern C++ 03
on C++
In a recent episode of CppCast, Jason Turner asked me to give my perspective on what the life of a C++ developer was in the day to day business. As I explained, my company has been releasing versions of a financial software for more than 30 years now and we only made the move from C++03 to C++11 in the last months.
This usually raises the question of code legacy, and how we could manage to stay in the present with a standard that dates back from 7 to 15 years. After all, all this talk about “Modern C++” puts emphasis on the “Modern” parts of the language. So how exactly can we write non-oudated C++ in 2018 without all that?
I would argue that while some concepts are a bit more cumbersome to write in “legacy” C++, there is nothing that really impedes us from adopting a “Modern” approach to our code, even in C++03.
What is Modern C++?
That’s probably the first question to ask: what makes a C++ code “Modern”? Is it the presence of constexpr
, auto
and decltype
? Or maybe lambdas? Move semantics?
Searching Google yields conflicting results. The first hit, What is Modern C++ tells us about the Standard C++ Guidelines and other great resources but offers a few statements such as “C++ before 2011 and since 2011 are different languages. The first is called classical C++, the second modern C++.” with which I respectfully disagree.
The second result is MSDN’s take on Modern C++ and puts more emphasis on idioms and philosophy instead of standard revisions. Facetious commenters may argue that given Visual Studio’s history with the C++ Standard, it was safer to bet on the former than the latter but in any case I find myself in support of the article’s take.
The third result is the obligatory StackOverflow thread with a short but insightful prefered answer: “Extensive use of standard library and STL, exceptions and templates - rather than just C with classes”. Probably incomplete in my opinion, but the opposition of “C with classes” is indeed something that often comes up.
I would also like to note one last result. A little down the list is Andrei Alexandrescu’s excellent Modern C++ Design that re-defined the way we approached generic programming in C++. I believe it’s the first occurence of the expression “Modern C++” in literature.
So what is Modern C++? In my humble opinion, it’s effectively doing away with the inheritance from C and embracing C++ as a different language that is not just an extension of a previous one. In short it focuses on:
- Expressive, zero cost, abstractions
- Strong typing with a preference for static over dynamic
- Concept based generic programming and duck typing
- Stack based scoping of resources (the oddly named ‘RAII’ idiom)
And you don’t need C++11, 14, 17 or 20 to do any of this.
Expressive, zero cost, abstractions
“Zero cost abstraction” is a mantra you hear a lot at C++ conferences. It is said to be able to conjure the blessing of Bjarne’s spirit if spoken loudly three times while standing in front of a mirror on a new moon.
What it means behind the mystique is the idea that we want our code to express our intent to other programmers (and even ourselves) rather than just telling the machine what to do. Basically we want to write in a language directed at humans rather than microprocessors, and of course we want that without any extra cost. We want to have our cake and eat it and C++ let us do just that.
In C, efficiency required to write very low level code. You wrote that while the last
pointer was greater than the first
, you copied the value pointed by first
into a tmp
variable, then copied the memory pointed by last
into the memory pointed by first
, then copied the value from tmp
to the memory pointed by last
, then incremented first
, decremented last
, and looped again. In C++ you’d just call std::reverse()
. At no cost.
All this is achieved through C++ concepts that aren’t new. Classes and methods allow the compiler to see “inside” your objects and their associated function calls to be able to perform most trivial operations (construct, destruct, copy, swap, access members) inline. Templates enable you to write generic algorithms that will perform great for any type you use. SFINAE and traits will allow programmers to offer more specialized versions of a given algorithm if some properties are present (for example taking advantage of the fact that arrays and vectors offer O(1) random access).
A great talk on the matter is Jason Turner demonstrating how to write Pong for Commodore 64 in C++ at CppCon 2016.
About constexprs and lambdas
C+11 and beyond bring new zero-cost abstractions to the table. constexprs allow the compiler to evaluate large and complex bits of code at compile time, leaving only the result in the final binary. Hana Dusíková’s compile-time regular expressions are a magical example of the power of the feature (see it in action in the CppCon 2017 lightning talk).
Still, as Jason (again) and Ben Deane showed at C++Now 2017 and CppCon 2017, one may argue that constexpr aren’t entirely ready yet and suffer from some painful limitations, the most important being the impossibilty to use dynamic allocation, removing almost all STL containers from the table. And if you ever try to use constexpr
before C++14 revisions, you’ll notice the limitations much sooner.
Another big feature about expressive abstractions in C++ are lambdas, those small inline functions you can put directly inside calls to algorithms:
std::transform(begin(radii), end(radii), back_inserter(areas),
[](double r) { return r * r * M_PI; });
But again, C++11 lambdas are just syntaxic sugar. All they do is create a functor and put your code inside. Yes it’s practical and reduces the pain of writing boilerplate code, but the absence shouldn’t stop you from being expressive:
struct DiameterToArea {
double operator()(double r) const {
return r * r * M_PI;
}
};
std::transform(begin(radii), end(radii), back_inserter(areas), DiameterToArea());
I would even argue that since it’s usually considered good practice to name lambdas unless they do really trivial work, you’ll end up writing some boilerplate anyway. C++11 just makes it simpler.
In fact the only thing you can’t do in C++03 to emulate lambdas is C++14 auto
lambdas that return a type that depends on the input. At best you can combine templated functors and traits to have a way to determine the return type without using decltype
.
To be fair, C++14 also brings the ability to return lambdas from functions without relying on std::function
which opens up a new way to design APIs. I’ll come back to it in the third section.
Strong typing with a preference for static over dynamic
C++ is all about types. In C you could define custom types with struct
, but there was no builtin way to define common operations such as construction, destruction, copy, assignment or arithmetic operations.
I still didn’t have the time to read From Mathematic to Generic Programming (shame on me) but my understanding is that it makes a good case of how types should be thought in terms of mathematical algebra. They have intrisic properties, relations (you can compare and possibly order them) and operations you can define through methods, functions and operator overloads.
This leads us to a strong preference in Modern C++ for regular types and value semantics over pointers and references. Values are much more natural to manipulate. You don’t have to worry about ownership and what happens if you assign or copy them. And again most of those things come in the form of idioms and best practices, not language features. You can do all that in C++03.
One big advance of C++11 in terms of regular types is, of course, move semantics which reduce the cost of using values by simply moving the content if possible when transfering from one variable to another, even accross function calls. In C++03 this is mostly limited to (N)RVO unless you design your types with “move” functions (you can rewrite std::unique_ptr
in C++03 for example). It has the drawback that some moves have to be explicit even in cases that would be implicit in C++11, but it can be done. See here.
Another one is user defined literals which extend the type system to constants and literal values, which render the code more expressive and can remove some unnecessary conversions.
The other important idea in Modern C++ when it comes to types is the preference for static over dynamic polymorphism, template
over virtual
. This comes from three factors. One, that inheritance is usually the worst form of composition and dynamic polymorphism requires it by design. Two, that virtual
calls incur a performance penalty that we don’t want to pay for unless there’s a real need. Three, that virtual
also implies pointer or reference semantics which we try to avoid in favor of values.
The STL iterators and their use in algorithms are a great example of static typing in C++, to be contrasted to other languages where every container has to implement IIterable
or ICollection
or something similar.
This again is more a matter of philosophy and isn’t tied to any particular standard revision. Yes some very well known C++ libraries from the 90s and 00s use a lot of inheritance and pointer semantics, but that’s a design choice, not a language restriction. If rewritten today they could (and should :)) absolutely decide to do away with all that, even in C++03 .
Concept based generic programming and duck typing
This one derives from the previous point about static vs dynamic typing (or maybe it’s the other way around). In C++, static polymorphism implies templates, which in turn implies generic programming based on concepts and duck typing.
As I’ve already talked about duck typing in the past, I’ll mostly focus here on what the “new” revisions of the standard bring to the table.
In C++11, we get auto
, which is a great way of working with duck typing. No more concerns about the type of returned values, we only care about the concepts brought by the type. Functions that return iterators are, in my opinion, the canonical example. The actual type is of little importance, all we care about is dereference, increment and comparison. This is such a no-brainer that clang-tidy
has an automatic rule to replace all those for you.
The next step of that idiom can be seen in more recent APIs when it is generalized to all return types. Two good examples are functions that return lambdas and expression templates. The actual type is not only of no consequence, but trying to use it in client will even hinder the ability of the maintainers to evolve the implementation without breaking compatibility.
Can we do without in C++03? Of course. This is one of the best way to learn how to type std::map<std::string, std::pair<int, double>>::const_iterator
while blindfolded. Joking aside, yes the STL and its containers can be used without auto
, and you can even write expression templates in C++03. Of course it makes it more restrictive, for example when trying to store indermediary types in a way that won’t break at every upgrade (you usually only typedef
the final types of expression templates). As for returning lambdas, you obviously can’t (it’s not even possible in C++11), but it’s possible to return functors, or to re-implement std::function
(even if I wouldn’t recommend the latest).
As for true concepts inside the language, well even if you use C++17 in production it’s not there yet. The alternatives are the good old SFINAE which we’ve been using for decades, or a re-implementation of requires()
using C++14 constexprs. Those of you who read the great Modern C++ Design we mentioned earlier know that you can get pretty advanced generic programming using only C++03, even if the code gets a bit complex to read at some point.
Stack based scoping of resources
Of all those “Modern” concepts, RAII is probably the oldest in the C++ community, dating back from the early design phases of C++ in the 80s and coined in The Design and Evolution of C++ by Bjarne himself.
From there we got std::auto_ptr
in C++98 and std::unique_ptr
/ std::shared_ptr
in C++11 and there was never another client code that contained an explicit new
or delete
. Oh wait, I’m confusing this with an alternate reality. In our world, it’s easy to find code from the 2000s with explicit heap allocation and no smart pointers, as for example in this QT4.8 Getting Started sample from 2011.
If you watch Herb Sutter’s keynote from CppCon 2016, he gives a nice recap of how to use smart pointers and allocation, with large preference for local (stack allocated) objects, followed by std::unique_ptr
and finaly std::shared_ptr
(in exceptional cases).
Again nothing new here, quite the opposite in fact. You could find std::auto_ptr
in C++98. Boost has been offering shared_ptr
and unique_ptr
(or scoped_ptr
) for a long time now. And as I said before, you can also rewrite std::unique_ptr
in C++03, with the limitation that they won’t work in STL containers (due to the absence of implicit move). And the idea of having allocation in the constructor and guaranteed deletion in the destructor is as old as C++ itself.
In conclusion
If you’re thinking “great, now my boss will cite this article as a reason not to upgrade our compilers”, that’s not my intent here.
The point I’m trying to make is that Modern C++ has almost nothing to do with C++11, 14 or 17 and that being stuck with C++03 is no reason for not using patterns and idioms recognized by the whole community.
Yes the new revisions of the standard bring a lot of improvements in terms of productivity and that alone should be reason enough to upgrade. Another one is that libraries written today will not support your old compiler and again we’d be more productive using Beast or range-v3 or sqlpp11 than rewriting something similar in C++03 by ourselves.
But let’s not forget that the pillars of Modern C++ are not tied to a particular revision of the standard and should be the foundation of any code we write and any class we teach, regardless of the toolchain we use.