It is now 45+ years since C++ was first conceived. As planned, it evolved to meet challenges, but many developers use C++ as if it was still the previous millennium. This is suboptimal from the perspective of ease of expressing ideas, performance, reliability, and maintainability. Here, I present the key concepts on which performant, type safe, and flexible C++ software can be built: resource management, life-time management, error-handling, modularity, and generic programming. At the end, I present ways to ensure that code is contemporary, rather than relying on outdated, unsafe, and hard-to-maintain techniques: guidelines and profiles.
- Introduction
- C++ Ideals
- Resource Management
- Modularity
- Generic Programming
- Guidelines and Enforcement
- The Future
- Summary
- References
C++ is a language with a long history. This leads many developers, teachers, and academics to overlook decades of progress and describe C++ as if today was still the second millennium where phones had to be plugged into walls and most code was short, low-level, and slow.
If your operating system has maintained compatibility over decades, you can run C++ programs written in 1985 today on a modern computer. Stability – compatibility with earlier versions of C++ – is immensely important, especially for organizations that maintain software systems for decades. However, in essentially all cases, contemporary C++27 can express the ideas embodied in such old-style code far simpler, with much better type-safety guarantees, and have them run faster using less memory.
This post presents the key contemporary C++ mechanism designed to enable that. At the end (§6), it describes techniques for enforcing such modern use of C++.
Consider a simple program that writes every unique line from input to output:
import std;
using namespace std; // make all of the standard library available
int main() // print unique lines from input
{
unordered_map<string,int> m; // hash table
for (string line; getline(cin,line); )
if (m[line]++ == 0)
cout << line << ‘\n’;
}
Connoisseurs will recognize this as the AWK program (!a[$0]++).
It uses an unordered_map, the C++ standard library version of a hash table, to hold unique lines and output only when a line is seen the first time.
The for-statement is used to limit the scope of the loop variable (line) to the loop.
Compared to older C++ styles, what is notable is the absence of explicit:
- Allocation/deallocation
- Sizes
- Error handling
- Type conversions (casts)
- Pointers
- Unsafe subscripting
- Preprocessor use (in particular, no #include).
Still, this program is quite efficient compared to older styles, more efficient than most programmers could write given a reasonable amount of time. If more performance is needed, it can be tuned. One important aspect of C++ is that code with a reasonable interface can be tuned to specific needs, and even use specialized hardware. This can be done without disturbing other code and often without modifying compilers.
Consider a variant of that program that collects unique lines for later use:
import std;
using namespace std; // make all of the standard library available
vector<string> collect_lines(istream& is) // collect unique lines from input
{
unordered_set s; // hash table
for (string line; getline(is,line); )
s.insert(line);
return vector{from_range, s}; // copy set elements into a vector
}
auto lines = collect_lines(cin);
Since I didn’t need a count, I used a set, rather than a map. I returned a vector rather than a set because vector is the most widely used container. I didn’t have to specify the vector’s element type because the compiler deduced it from the set’s element type.
I used the from_range argument to tell the compiler and a human reader that a range is used, rather than other possible ways of initializing the vector. I would have preferred to use the logically minimal vector{m} but the standards committee decided that requiring from_range would be a help to many.
Experienced programmers will notice that this collect_lines() copies the characters read. That could be a performance problem, so in §3.2, I show how to tune collect_lines() to avoid that.
What’s the point of these small examples? To show some contemporary C++ before going into technical details and hopefully to shake some people out of their decades-old complacent miscomprehensions.
Back to Top
My aims for C++ can be summarized as
- Direct expression of ideas
- Static type safety
- Resource safety (aka “no leaks”)
- Direct access to hardware
- Performance (aka efficiency)
- Affordable extendibility (aka Zero-overhead abstraction)
- Maintainability (aka comprehensible code)
- Platform independence (aka portability)
- Stability (aka compatibility)
This has not changed since the earliest days [BS1994], but C++ was meant to evolve, and contemporary C++ can deliver such properties in code much better than earlier versions of C++.
C++ code embodying these ideals is not achieved simply by applying all the latest features and only those. Some key features and techniques are old
- Classes with constructors and destructors
- Exceptions
- Templates
- std::vector
- …
Other key features are more recent
- Modules (§4)
- Concepts (for specifying generic interfaces; §5.1)
- Lambda expressions (for generating function objects; §5.1)
- Ranges (§5.1)
- Constexpr and consteval (for compile-time computation; §5.2)
- Concurrency support and parallel algorithms
- Coroutines (missing for decades thought they were an essential part of early C++)
- std::shared_ptr
- …
What matters is to use the language and library features as a coherent whole in ways that suits the problem to be solved.
The value of a programming language is in the range and quality of its applications. For C++, the evidence is its amazing range of application areas: foundational software, graphics, scientific application, movies, games, automobiles, language implementation (and not just C++ implementation), flight controls, search engines, browsers, semiconductor design and manufacture, space probes, finance, AI, and much, much more. Given the billions of lines of C++, we cannot change the C++ language incompatibly. However, we can change the way C++ is used.
In the rest of this paper, I focus on
- Resource management (including control of lifetime and error handling)
- Modules (including eliminating the preprocessor)
- Generic programming (including concepts)
- Guidelines and enforcement (How can we guarantee that what we write really is “21st Century C++”?)
Naturally, that is not all C++ offers, and much good code is written in ways not listed here. For example, I left out object-oriented programming because many developers know how to do that well in C++. Also, very high-performance code and code manipulating hardware directly require special attention and techniques. The extensive C++ concurrency support deserves at least a separate paper. However, the key to most good software is type-safe interfaces that contains sufficient information to allow optimization and run-time checking of properties that cannot be guaranteed at compile-time.
Back to Top
A resource is anything we must acquire and later release (give back) explicitly or implicitly. For example, memory, locks, file handles, sockets, thread handles, transactions, and shaders. To avoid resource leaks, we must avoid manual/explicit release. People – even programmers – are notoriously bad at remembering to return what they borrowed.
The basic C++ technique for managing a resource is to root it in a handle that is guaranteed to release the resource when the handle’s scope is left. For reliability, we cannot rely on explicit delete, free(), unlock(), etc. in application code. Such operations belong in resource handles. Consider:
template<typename T>
class Vector { // vector of elements of type T
public:
Vector(initializer_list<T>); // constructor: acquire memory; initialize elements
~Vector(); // destructor: destroy elements; release memory
// …
private:
T* elem; // pointer to elements
int sz; // number of elements
};
Here, Vector is a resource handle. It raises the level of abstraction from the machine-near pointer plus a count of elements to a proper type with guaranteed initialization (the constructor) and clean-up (the destructor). The standard-library vector that this Vector is meant to illustrate also provides comparisons, assignments, more ways of initializing, resizing, support for iteration, etc. That provides the programmer with a vector that from a language-technical point of view behaves much like a built-in integer type despite being a (standard-library) resource handle and having very different semantics. We can use it like this:
void fct()
{
Vector<double> constants {1, 1.618, 3.14, 2.99e8};
Vector<string> designers {“Strachey”, “Richards”, “Ritchie”};
// …
Vector<pair<string,jthread>> vp { {“producer”,prod}, {“consumer”,cons}};
}
Here, constants is initialized by four mathematical and physical values, designers by three hopefully well-known programming language designers, and vp by a producer-consumer pair. All are initialized by the appropriate constructors and released upon scope exit by the appropriate destructors. The initialization and release done by constructors and destructors is recursive. For example, the construction and destruction of vp is non-trivial since it involves a Vector, a pair, strings (handles to characters), and jthreads (handles to operating system threads). However, it is all handled implicitly.
This use of constructor-destructor pairs (often called RAII – “Resource Acquisition Is Initialization”) doesn’t just guarantee release of resources, it also minimizes resource retention, thus providing a significant performance advantage compared to many other techniques, such as memory management relying on a garbage collector.
Controlling the lifetime of objects representing resources is necessary for simple and efficient resource management. C++ offers 4 points of control defined as operations on a class (here named X):
- Construction: Invoked before first use: establish the class invariant (if any). Name: constructor, X(optional_arguments)
- Destruction: Invoked after last use: release every resource (if any). Name: destructor, ~X()
- Copy: make a new object with the same value as another; a=b implies a==b (for regular types). Names: copy constructor, X(const X&) and copy assignment, X::operator=(const X&)
- Move: Move resources from object to object, often between scopes. Names: move constructor, X(X&&) and move assignment, X::operator=(X&&)
For example, we might elaborate our Vector like this:
template<typename T>
class Vector { // vector of elements of type T
public:
Vector(); // default constructor: make an empty vector
Vector(initializer_list<T>); // constructor: acquire memory; initialize elements
Vector(const Vector& a); // copy constructor: copy a into *this
Vector& operator=(const Vector& a); // copy assignment: copy a into *this
Vector(Vector&& a); // move constructor: move a into *this
Vector& operator=(Vector&& a); // move assignment: move a into *this
~Vector(); // destructor: destroy elements; release memory
// …
};
Assignment operators must release any resources owned by the target object. Move operations must transfer all resources to the target and ensure that they are no longer in the source.
Given that framework, let’s have another look at the collect_lines example from §1. First, we can simplify it a bit:
vector<string> collect_lines(istream& is) // collect unique lines from input
{
unordered_set s {from_range,istream_iterator<string>{is}); // initialize s from is
return vector{from_range,s};
}
auto lines = collect_lines(cin);
The istream_iterator<string>{is} allows us to treat the input from is as a range of elements, rather than laboriously explicitly apply input operations to the stream.
Here, the vector is moved out of collect_lines() rather than copied. The worst-case cost of a vector’s move constructor is 6 word copies, three to copy the representation and three to zero-out the original representation. This is still the case if the vector has a million elements.
Even this small cost is eliminated in many cases. Since 1983 or so, compilers have known to construct the returned value (here,vector{from_range,s};) in the target (here, lines). This is referred to as “copy elision.”
However, the strings are still copied from the set into the vector. That could be costly. In principle, the compiler could deduce that we don’t use s again after making the vector and just move the string elements, but today’s compilers are not that smart, so we must explicitly ask for a move:
vector<string> collect_lines(istream& is) // collect unique lines from input
{
unordered_set s {from_range,istream_iterator<string>{is}); // initialize s from is
return vector{from_range,std::move(s)}; // move elements
}
This still leaves one redundant copy, the copy of characters from the input buffer into the set’s string elements. If that’s a problem, we can eliminate that also. However, doing so involves only conventional low-level techniques, so it is beyond the scope of this paper. Such code is messier but, as aways, C++ code with well-specified interfaces is tunable. Also please remember: never optimize without measurement showing the need to.
One of the key aims of C++ is resource safety: no resource is leaked. This implies that we must prevent resource leaks in error conditions. The basic rules are:
- Don’t leak a resource
- Don’t leave a resource in an invalid state
So, when an error that cannot be handled locally is detected, before exiting a function we must:
- Put every object accessed into a valid state
- Release every object that the function is responsible for
- Leave it to some function up the call chain to deal with resource-related problems
This implies that “raw pointers” cannot reliably be used as resource handles. Consider a type Gadget that may hold resources such as memory, locks, and file handles:
void f(int n, int x)
{
Gadget g {n};
Gadget* pg = new Gadget{n}; // explicit new: don’t!
// …
if (x<100) throw std::runtime_error{“Weird!”}; // leaks *pg; but not g
if (x<200) return; // leaks *pg; but not g
// …
}
The explicit use of new to place the Gadget on the heap is a problem the picosecond its result is stored in a “raw pointer” rather than in a resource handle with a suitable destructor. Local objects are simpler and typically faster than use of explicit new.
For a reliable system, we need an articulated policy for handling errors. The best way to do that in general is to distinguish errors that can be handled locally by an immediate caller, from errors that can be handled only far up a call chain.
Use error codes and tests for failures that are common and can be handled locally
Use exceptions for failures that are rare (“exceptional”) and cannot be handled locally
- The alternative is expensive “error-code hell” where every caller up the call stack must remember to test
- Failure to check for an exception gives termination, not wrong results
In some important applications, unconditional immediate termination isn’t an option. Then, we must either remember to test every error return code and catch every exceptions somewhere (e.g., in main()) and do whatever appropriate response is required.
To the surprise of many, exceptions can be cheaper and faster than consistent use of error codes even for small systems [KE2024; GCC2024].
Error handling based on exceptions doesn’t work with pointers used as resource handles. To get simple, reliable, and maintainable error-handling, we must rely on exceptions and RAII as well error-codes for errors that should be handled locally. Consider:
void fct(jthread& prod, jthread& cons, string name)
{
ifstream in { name };
if (!in) { /* … */ } // possible failure expected
// …
vector<double> constants {1, 1.618, 3.14, 2.99e8};
vector<string> designers {“Strachey”, “Richards”, “Ritchie”};
auto dmr = “Dennis M. ” + designers[2];
// …
pair<string,jthread&> pipeline[] { {“producer”, prod}, {“consumer”, cons}};
// …
}
How many tests would we need for this (artificial, but not unrealistic) small example if we couldn’t rely on exceptions? The example involves memory allocation, nested construction, an overloaded operator, and acquisition of a system resource.
Unfortunately, exceptions have not been universally appreciated and used everywhere they would have appropriate. In additions to overuse of “naked” pointers, it has been a problem hat many developers insist to use a single technique for reporting all errors. That is, all reported by throwing or all reported by returning an error code. That doesn’t match the needs of real-world code.
Back to Top
The preprocessor that C++ inherited from C is essentially universally used, but it is a major obstacle to tool development and compiler performance. In contemporary C++, macros used to express constants, functions, and types have been replaced with properly typed and scoped constants, compile-time evaluated functions, and templates [BS2022]. However, the preprocessor has been essential for expressing a weak form of modularity. Interfaces to libraries and other separately compiled code is represented as files containing C++ source text and #included.
An #include directive copies the source text from such a “header file” into the current translation unit. Unfortunately, this implies that
#include “a.h”
#include “b.h”
Might have a different meaning than
#include “b.h”
#include “a.h”
This is the source of subtle bugs.
An #include is transitive. That is, if a.h contains an #include “c.h” the text of c.h also becomes part of every source file that use #include “a.h”. This is a source of subtle bugs. Since a header file is often #included in dozens or hundreds of source files, this also implies much repeated compilation.
These problems with the use of header files to fake modularity have been known from before C++ was born, but defining an alternative and introducing it into billions of lines of code is not trivial. However, C++ now offers modules that deliver proper modularity. Importing modules is order independent, so
import a;
import b;
means the same as
import b;
import a;
The mutual independence of modules implies improved code hygiene. It makes the subtle dependency bugs impossible.
Here is a very simple example of a module definition:
export module map_printer; // we are defining a module
import iostream; // we import modules needed for the implementation\
import containers;
using namespace std;
export // this template is the only entity exported
template<Sequence S>
void print_map(const S& m) {
for (const auto& [key,val] : m) // access key and value pair from m
cout << key << ” -> ” << val << ‘\n’;
}
Because import is not transitive, users of map_printer do not gain access to the implementation details needed for print_map.
A module needs to be compiled once only, independently of how many times it is imported. This implies very significant improvements in compile time. A user reports [DE2021]:
#include <libgalil/DmcDevice.h> // 457440 lines after preprocessing
int main() { // 151268 non-blank lines
Libgalil::DmcDevice(“192.168.55.10”); // 1546 milliseconds to compile
}
That’s 1.5 seconds to compile almost ½ million lines of code. That’s fast! However, the compiler is doing far too much work.
import libgalil; // 5 lines after preprocessing
int main() { // 4 non-blank lines
Libgalil::DmcDevice(“192.168.55.10”); // 62 milliseconds to compile
}
That’s a 25 times speedup. We cannot expect that in all cases, but a 7-to-10 times advantage to import over #include is common. If you #included that library in 25 source files. That would cost 1.5 seconds 25 times where the import would clock in at 1.5 seconds in total.
The complete standard library has been made into a module. Look at the traditional “hello world!” program [BS2021]:
#include <iostream>
int main()
{
std::cout << “Hello, World!\n”;
}
On my laptop, it compiled in 0.87 seconds. Replace the #include<iostream.h> with import std; and the compile time dropped to 0.08 seconds despite that at least 10 times as much information was made available.
Reorganizing significant amount of code isn’t easy or cheap, but in the case of modules, the benefits are significant in terms of code quality and massive in terms of compile time.
Why do I – in this sole case – bother to explain “the bad old way?” Because #includes are pervasive, as they have been almost from the birth of C, and many developers find it hard to imagine C++ without it.
Back to Top
Generic programming is a key foundation of contemporary C++. It has been so since before “C with Classes“ was renamed “C++” but only recently (C++20) has the language support approximated the ideals.27
Generic programming, that is, programming with types and functions parameterized by types, offers
- Terser and more readable code
- More direct expression of ideas
- Zero-overhead abstraction
- Type safety
Templates, the C++ language support for generic programming, are pervasive in the standard library:
- Containers and algorithms
- Concurrency support: threads, locks, …
- Memory management: allocators, resource handles (e.g., vector and list), resource-management pointers, …
- I/O
- Strings and regular expressions
- And much more
We can write code that works for all suitable argument types. For example, here is a sort function that accepts all types that meet the ISO C++ standard’s definition of a sortable range:
void sort(Sortable_range auto& r);
vector<string> vs;
// … fill vs …
sort(vs);
array<int,128> ai;
// … fill ai …
sort(ai);
The compiler has enough information to verify that the types of vs and ai have what Sortable_range requires; that is, a random-access range of values of types that can be compared and swapped as needed for sorting. If the arguments are not suitable, the error is caught by the compiler at the point of use. For example:
list<int> lsti;
// … fill lsti …
sort(lsti); // error: a list doesn’t offer random access
According to the C++ standard, a list isn’t a sortable range because it doesn’t offer random access.
5.1. Concepts
A concept is a compile-time predicate. That is, a function to be executed by the compiler, yielding a Boolean. It is mostly used to express requirements for the parameters of a template. A concept is often built from other concepts. For example, here is the Sortable_range required by the sort above:
template<typename R>
concept Sortable_range =
random_access_range<R> // has begin()/end(), ++, [], +, …
&& sortable<iterator_t<R>>; // can compare and swap elements
This says that a type R is a Sortable_range if it is a random_access_range and has an iterator type that is sortable. The random_access_range and sortable are concepts defined in the standard library.
A concept can take one or more arguments and can be built from fundamental language properties. To specify a property of a type directly in terms of the language (as opposed in terms of other concepts), we use “use patterns” [GDR2006]. For example:
template<typename T, typename U = T>
concept equality_comparable = requires(T a, U b) {.
{a==b} -> Boolean;
{a!=b} -> Boolean;
{b==a} -> Boolean;
{b!=a} -> Boolean;
}
The constructs in the {…} must be valid and return something that matches the concept specified after ->. So here, the listed use-patterns (e.g., a==b) must return something that can be used as a bool.
Usually, as in the sort example, checking that a type matches a concept is done implicitly, but we can also be explicit using static_assert:
static_assert(equality_comparable<int,double>); // succeeds
static_assert(equality_comparable<int>); // succeeds (T2 is defaulted to int)
static_assert(equality_comparable<int,string>); // fails
The equality_comparable concept is defined in the standard library. We don’t have to define it ourselves, but it’s a good example.
We want to write code that works for all suitable argument types. However, many (probably most) algorithms take more than one template argument type. The means that we need to express relationships among those template arguments. For example:
template<input_range R, indirect_unary_predicate<iterator_t<R> Pred>
Iterator_t<R> find_if(R&& r, Pred p);
This says that find_if takes an input range r and a predicate p that can be applied to the result of an indirection through r’s iterator. For example:
vector<string> numbers; // strings representing numbers; e.g., “13” and “123.45”
// … fill numbers …
auto q = find_if(numbers, [](const string& s) { return stoi(s)<42; });
The second parameter of the call of find_if is a lambda expression. It generates a function object that executes stoi(s)<42 when invoked in the implementation of find_if for an argument s. Lambda expressions (typically just called “lambdas”) have proven immensely useful and popular in contemporary C++.
We have always had concepts. Every successful generic library has some form of concepts: in the designer’s head, in the documentation, or in comments. Such concepts often represent fundamental concepts of an application area. For example:
- C/C++ built-in types: arithmetic and floating [K&R1978]
- The C++ standard-library: iterators, sequences, and containers
- Mathematics: monad, group, ring, and field
- Graphs: edges and vertices, graph, DAG, …
C++20 didn’t introduce the idea of concepts; it just added direct language for concepts. A concept is a compile-time predicate. Using concepts is easier than not using them. However, like for every novel construct, we must learn to use them effectively.
A concept is an example of a compile-time function. In contemporary C++, any sufficiently simple function can be evaluated at compile time:
- constexpr: can be evaluated at compile time
- consteval: must be evaluated at compile time
- concept: evaluated at compile time, can take types as arguments
This applies to both built-in and user-defined types. For example:
constexpr auto jul = weekday(December/24/2024); // Tuesday
To allow consteval and constexpr functions and concepts to be evaluated at compile time, they cannot
- have side effects
- access non-local data
- have undefined behavior (UB)
However, they can use extensive facilities, incl. much of the standard library
Thus, such functions are the C++ version of the idea of a pure function and a contemporary C++ compiler contains an almost complete C++ interpreter. Compile-time evaluation is also a boon to performance.
Back to Top
Contemporary styles yield major benefits. However, upgrading code is hard and often expensive. How can we modernize existing code? Avoiding sub-optimal techniques is difficult. Old habits die hard. Familiarity is often mistaken for simplicity. Much confusing and outdated information is circulated on the web and in teaching material. Also, old code often offer outdated-style interfaces, thus encouraging the use of older styles of use. We need help to guide us to better styles of code.
Stability/compatibility is a major feature. Also, given the billions of lines of C++ code, only gradual adoption of novel features and techniques is feasible. So, we can’t change the language, but we can change the way it is used. People (quite reasonably) want a simpler C++, but also new features, and insist that their existing code must continue to run.
To help developers focus on effective use of contemporary C++ and avoid outdated “dark corners” of the language, sets of guidelines have been developed. Here I focus on the C++ Core guidelines that I consider the most ambitious [GC].
A set of guidelines must represent a coherent philosophy of the language relative to a given use. My principal aim is a type-safe and resource-safe use of ISO standard C++. That is
- Every object is exclusively used according to its definition
- No resource is leaked
This encompasses what people refer to as memory safety and much more. It is not a new goal for C++ [BS1994]. Obviously, it cannot be achieved for every use of C++, but by now we have years of experience showing that it can be done for modern code, though so far enforcement has been incomplete.
A set of guidelines has strengths and weaknesses:
- Is available now (e.g., The C++ Core Guidelines [CG])
- Individual rules can be enforced or not
- Enforcement is incomplete
Building on guidelines, we need enforcement:
- A profile is an enforced coherent sets of guidelines rules [CG,BS2022]
- Being worked on in WG21 and elsewhere [BS2022b, HS2024b]
- Are not yet available, except for experimental and partial versions [KR2019, Google2024, KV2024]
When thinking about C++, it is important to remember that C++ is not just a language but part of an ecosystem consisting of implementations, libraries, tools, teaching, and more. In particular, developers using C++ rely on facilities far beyond what is available in C.
Simple subsetting of C++ doesn’t work: We need the low-level, tricky, close-to-the-hardware, error-prone, and expert-only features to implement higher-level facilities efficiently and to enable low-level features where needed. The C++ Core Guidelines use a strategy known as subset-of-superset [BS2005]:
- First: extend the language with a few library abstractions: use parts of the standard library and add a tiny library to make use of the guidelines convenient and efficient (the Guidelines Support Library, GSL).
- Next: subset: ban the use of low-level, inefficient, and error-prone features.
What we get is “C++ on steroids”: Something simple, safe, flexible, and fast; rather than an impoverished subset or something relying on massive run-time checking. Nor do we create a language with novel and/or incompatible features. The result is 100% ISO standard C++. Messy, dangerous, low-level features can still be enabled and used when needed.
Different application domains have different needs and thus need different sets of guidelines, but initially the focus is on “The core or the C++ Core Guidelines.” The rules we hope that everyone eventually could benefit from
- No uninitialized variables
- No range or nullptr violations
- No resource leaks
- No dangling pointers
- No type violations
- No invalidation
Two books describe C++ following these guidelines except when illustrating errors: “A tour of C++” for experienced programmers [BS2022] and “Programming: Principles and Practice using C++” for novices [BS2024]. Two more books explore aspects of the C++ Core Guidelines [JD2021; RG2022].
A pointer doesn’t have the associated information needed to allow range checking. However, range checking is a must for memory safety and also for type safety because we cannot allow application code to read or overwrite objects beyond the range of the objects pointed to. Instead, we must use an abstraction with enough information to range check, such an array, a vector, or a span.
Consider a common style: a pointer plus an integer supposedly indicating the number of elements pointed to:
void f(int* p, int n)
{
for (int i = 0; i<n; i++)
do_something_with(p[n]);
}
int a[100];
// …
f(a,100); // OK? (depends on the meaning of n in the called function)
f(a,1000); // likely disaster
This is a very simple example using an array to show the size. Since the size is present, checking at the point of call is possible (though hardly ever done) and typically a (pointer,integer) pair is passed through a longer call chain making verification difficult or impossible.
The solution to this problem is to tie the size firmly to the pointer (like in Vector; §3.1). That’s what a span does:
void f(span<int> a) // a span holds a pointer and the number of elements pointed to
{
for (int& x: s) // now we can use a range-for
do_something_with(x);
}
int a[100];
// …
f(a); // type and element count deduced
f({a,1000}); // asking for trouble, but marked syntactically and easily checkable
Using span is a good example of “Make simple things simple” principle. Code using it is simpler than the “old style”: shorter, safer, and often faster.
The span type was introduced in the Core Guidelines support library as a range-checked type. Unfortunately, when it was added to standard library, the guarantee for range checking was removed. Obviously, a profile (§6.4) enforcing this rule must range check. Every major C++ implementation has ways to ensure this (e.g., GCC standard library hardening [KV2024], Google Spatial safety [Google2024], and Microsoft Visual Studio’s static analyzer [KR2019]). Unfortunately, there isn’t yet a standard and portable way of requiring it.
Some containers, notably vector, can relocate their elements. If someone outside the container obtain a pointer to an element and use it after relocation, disaster can happen. Consider:
void f(vector<int>& vi)
{
vi.push_back(9); // may relocate vi’s elements
}
void g()
{
vector<int> vi { 1,2 };
auto p = vi.begin(); // point to first element of vi
f(vi);
*p = 7; // error: p is invalid
}
Given appropriate rules for the use of C++ (§6.1), local static analysis can prevent invalidation. In fact, implementations of Core Guidelines lifetime checks have done that since 2019 [KR2019]. Prevention of invalidation and the use of dangling pointers in general is completely static (compile time). No run-time checking is involved.
This is not the place for a detailed description of how this analysis is done. For that see [BS2015; HS2019; BS2024]. However, here is an outline of the model:
The rules apply to every entity that directly point to an object, such as pointers, resource-management pointers, references, and containers of pointers. Examples are an int*, an int&, a vector<int*>, a unique_ptr<int>, a jthread holding an int*, and a lambda that has captured an int by reference.
- Ban use after delete (obviously) and rely on RAII (§3).
- Don’t allow a pointer to escape the scope of what it points to. This means that a pointer can be returned from a function only if it points to something static, points to something on the free store (aka heap and dynamic memory), or was passed in as an argument.
- Assume that a function (e.g., vector::push_back()) that take non-const arguments invalidate. If a pointer to one of its elements has been taken, we ban calls to it. Functions taking only const arguments cannot invalidate, and to avoid massive false positives and preserving local analysis, we can annotate function declarations with [[profiles::non_invalidating]]. This annotation can be validated when we see the function’s definition. Thus, it is a safe annotation rather than a “trust me” annotation.
Naturally, there are many details to address, but they have been tried out in experimental as well as currently shipping implementations.
Guidelines are fine and useful, but it is essentially impossible to follow them consistently in a large code base. Thus, enforcement is essential. Enforcement of rules preventing missing initialization, range errors, nullptr dereferences, and the use of dangling pointers is currently available and have been demonstrated to be affordable in large code bases [KR2019, Google2024, KV2024].
However, key foundational rules must be standard – part of the definition of C++ – with a standard way of requesting them in code to enable interoperability between code developed by different organizations and running on multiple platforms and teaching.
We call an enforced coherent set of guideline rules providing a guarantee a “profile.” As currently planned for the standard, the initial set of profiles (based on Core Guidelines profiles as used for years) are [HS2024b; CG]:
- type – every object initialized; no casts; no unions
- lifetime – no access through dangling pointers; pointer dereference checked for nullptr; no explicit new/delete
- bounds – all subscriping is range checked; no pointer arithmetic.
- arithmetic – no overflow or underflow; no value-changing signed/unsigned conversions
This is essentially “the core of the core” described in §6.1. More profiles will follow given time and experimentation [BS2024b]. For example:
- algorithms – all ranges, no dereferences of end() iterators
- concurrency – eliminate deadlocks and data races (hard to do)
- RAII – every resource owned by a handle (not just resources managed with new/delete).
Not all profiles will be ISO standard. I expect to see profiles defined for specific application areas, e.g., for animation, flight software, and scientific computation.
Enforcement is primarily static (compile-time) but a few important checks must be run-time, (e.g., subscripting and pointer dereferencing).
A profile must be explicitly requested for a translation unit. For example,
[[profile::enforce(type)]] // no casts or uninitialized objects in this TU
Where necessary, a profile can be suppressed for a statement (including compound statements) where needed. For example:
[[profile::suppress(lifetime))] this->succ = this->succ->succ;
The need to suppress verification of guarantees is primarily for the implementation of the abstractions needed to provide guarantees (e.g., span, vector, and string_view), to guarantee range checking, and to directly access hardware. Because C++ needs to manipulate hardware directly, we cannot “outsource” the implementation of fundamental abstractions to some other language. Nor – because of its wide range of applications and multiple independent implementations – can we simply leave the implementation of all foundational abstractions (e.g., all abstractions involving linked structures) to the compiler.
Back to Top
I am reluctant to make predictions about the future, partly because that’s inherently hazardous, and in particular because the definition of C++ is controlled by a huge ISO standards committee operating on consensus. Last I checked, the membership list had 527 entries. That indicates enthusiasm, wide interest, and provides broad expertise, but it is not ideal for programming language design and ISO rules cannot be dramatically modified. Among other subjects, there is work in progress on
- A general model for asynchronous computing [LB2024]
- Static reflection [WC2014]
- SIMD [MK2024]
- A contract system [JB2024]
- Functional-programming style pattern matching [HSPM,PMPM]
- A general unit system (e.g., the SI system) [MP2024]
Experimental versions of all of these are available.
One serious concern is how to integrate diverse ideas into a coherent whole. Language design involves making decisions in a space where not all relevant factors can be known, and where accepted results cannot be significantly changed for decades. That differs from most software product development and most computer science academic pursuits. The fact that almost all language design efforts over the decades have failed demonstrates the seriousness of this problem.
Back to Top
C++ was designed to evolve. When I started, not only didn’t I have the resources to design and implement my ideal language, but I also understood that I needed the feedback from use to turn my ideals into practical reality. And evolve it did while staying true to its fundamental aims [BS1994]. Contemporary C++ (C++23) is a much better approximation to the ideals than any earlier version, including support for better code quality, type safety, expressive power, performance, and for a much wider range of application areas.
However, the evolutionary approach caused some serious problems. Many people got stuck with an outdated view of what C++ is. Today, we still see endless mentions of the mythical language C/C++, usually implying a view of C++ as a minor extension of C embodying all the worst aspects of C together with grotesque misuses of complex C++ features. Other sources describe C++ as a failed attempt to design Java. Also, tool support in areas such as package management and build systems have lagged because of a community focus on older styles of use.
The C++ model can be summarized as
- Static type system
Equal support for built-in types and user-defined types
Value and reference semantics - Systematic and general resource management (RAII)Efficient object-oriented programming
- Flexible and efficient generic programming
- Compile-time programming
- Direct use of machine and operating system resources
- Concurrency support through libraries (supported by intrinsics)
The C++ language and standard library are the concrete expression of this model and a critical part of the ecosystems used to develop software. The value of a programming language is in the quality of its applications.
Back to Top
- [CG] B. Stroustrup and H. Sutter (editors): C++ Core Guidelines.
- [WF2024] W. Childers et al: Reflection for C++26 . WG21 P2996R6. 2024.
- [DE2021] D. Engert: A (Short) Tour of C++ Modules . CppCon 2021.
- [DE2022] D. Engert: Contemporary C++ in Action. CppCon 2022.
- [GCC2024] GCC exception performance .
- [GDR2006] G. Dos Reis and B. Stroustrup: Specifying C++ Concepts. POPL06. 2006.
- [Google2024] A. Rebert et al: Retrofitting spacial safety to hundreds of millions of lines of C++. 2024.
- [HS2019] H. Sutter: Lifetime safety: Preventing common dangling. WG21 P1179.
- [HS2024] H. Sutter: Pattern matching using is and as. WG P392R3. 2024.
- [HS2024b] H. Sutter: Core safety profiles for C++26. WG21 D3081R1. 2024.
- P1179R1. 2019-11-22.
- [JB2024] J. Berne, T. Doumler, and A. Krzemieński: Contracts for C++. P2900R9. 2024.
- [JD2021] J. Davidson and K. Gregory Beautiful C++: 30 Core Guidelines for Writing Clean, Safe, and Fast Code. 2021. ISBN 978-0137647842.
- [KE2024] K. Estell: C++ Exceptions for Smaller Firmware. CppCon 2024. [K&R1978] B.W. Kernighan and D.M. Ritchie: The C Programming Language. Prentice-Hall. 1978. ISBN 01-13-110163-3.
- [KV2024] K.Varlamov and L Dionne: Standard library hardening. WG21 P3471R0. 2024.
- [KR2019] K. Reed: Lifetime Profile Update in Visual Studio 2019 Preview 2. 2019.
- [LB2024] L. Baker et al: A plan for std::execution for C++26. WG21 P3109R0. [MK2024] M. Kretz: std::simd — data-parallel types. WG21 P21928R9. 2024.
- [MPPM] M. Park: Pattern Matching: match Expression. WG21 P2688R2. 2024.
- [MP2024] M. Pusz et al: Quantities and units library. WG21 P3045R2. 2024.
- [RG2022] R. Grimm: C++ Core Guidelines Explained. Addison-Wesley. 2022. ISBN 978-0136875673.
- [BS1982] B. Stroustrup: Classes: An Abstract Data Type Facility for the C Language. SIGPLAN Notices, January 1982.
- [BS1993] B. Stroustrup: A History of C++: 1979-1991. ACM SIGPLAN. March 1993.
- [BS1994] B. Stroustrup: The Design and Evolution of C++. Addison Wesley, ISBN 0-201-54330-3. 1994.
- [BS2005] B. Stroustrup: A rationale for semantically enhanced library languages. LCSD05. October 2005.
- [BS2007] B. Stroustrup: Evolving a language in and for the real world: C++ 1991-2006. ACM SIGPLAN. June 2007.
- [BS2015] B. Stroustrup, H. Sutter, and G. Dos Reis: A brief introduction to C++’s model for type- and resource-safety. Isocpp.org. October 2015. Revised December 2015.
- [BS2020] B. Stroustrup: Thriving in a Crowded and Changing World: C++ 2006–2020. ACM SIGPLAN. June 2020.
- [BS2021] B. Stroustrup: Minimal module support for the standard library. WG21 P2412r0. 2021.
- [BS2022] B. Stroustrup: A Tour of C++ (3rd Edition). Addison-Wesley. 2022. ISBN 978-0-13-681648-5.
- [BS2022b] B. Stroustrup and G. Dos Reis: Design Alternatives for Type-and-Resource Safe C++. WG21 P2687R0. 2022.
- [BS2024] B. Stroustrup: Programming: Principle and Practice using C++. Addison-Wesley. 2024.ISBN 978-0-13-830868-1.
- [BS2024b] B. Stroustrup: A framework for Profiles development. WG21 P3274R0. 2024.
- [BS2024c] B. Stroustrup: Profile invalidation – eliminating dangling pointers . WG21 P3346R0.
Bjarne Stroustrup is the designer and original implementer of C++. He also is a professor of Computer Science at Columbia University, and was the recipient of the NAE Charles Stark Draper Prize for 2018 for conceptualizing and developing the C++ programming language.