An Interview with Bjarne Stroustrup
Danny Kalev: Although it may be too early to assess the full impact of C++11 right now, which insights and conclusions have you reached already regarding C++11? Are there new features that you have grown to like more than before? Are there features that need further polishing or fixing?
Bjarne Stroustroup: Adding move semantics is a game changer for resource management. It completes what was started with constructor/destructor pairs, the RAII ("Resource Acquisition Is Initialization") idiom, and smart pointers to enable safe, simple, and efficient handling of general resources. A resource is anything that must be acquired and (explicitly or implicitly) released. Examples are memory, locks, file handles, threads, and sockets.
Consider how to return a potentially large number of elements from a function. Here is a conventional C++98 function returning the indices of string matches:
vector<int>* find_all(vector<string>& vs, const string& s) { vector<int>* res = new vector<int>; for (int i = 0; i<vs->size(); ++i) if (vs[i] == s) res->push_back(i); return res; } vector<int>* pvi = find_all(some_vec,"vindaloo"); for (vector<int>::iterator p = pvi->begin(); p!=pvi->end(); ++p) cout << *p << " is a match\n"; // ... delete pvi;
Naturally, this would have been more realistic and interesting if I had used a regular expression (std::regex) to find a match.
I used (error-prone) explicit memory management with new and delete. Why? Because using the free store (dynamic memory) is the only way to return a large amount of memory without copying.
Move semantics allows a significant simplification:
vector<int> find_all(vector<string>& vs, const string& s) { vector<int> res; for (int i = 0; i<vs.size(); ++i) if (vs[i] == s) res.push_back(i); return res; } for (int x : find_all(some_vec,"vindaloo")) cout << x << " is a match\n";
I eliminated the explicit memory managements, but I returned the vector by value! Why is that not a bad performance bug? What if there were 100,000 matches?
C++11 supports moves as well as copies. That is, in addition to providing a copy constructor and a copy assignment, a class (such as vector) can offer a move constructor and a move assignment. A copy operation produces an independent copy of the original value and does not modify its source. A move operation is used when the source is no longer needed so that we can “steal” its representation. Consider a simplified vector:
class vector { // simplified vector of doubles private: double* elem; // pointer to elements int sz; // number of elements public: vector(const vector& a) // copy constructor :elem{new double{a.size()}, sz{a.size()} {} vector& operator=(const vector& a) // copy assignment { vector tmp = a; swap(*this,tmp); } vector(vector&& a) // move constructor :elem{a.elem}, sz{a.size()} { a.elem =nullptr; a.sz = 0;} vector& operator=(vector&& a) // move assignment { delete elem; elem=a.elem; sz=a.sz; a.elem=nullptr; a.sz=0; } // ... };
The && is called an rvalue reference and is what identifies the operations as move operations.
A move operation is conceptually very simple. Even babies understand that when we move an object, we don’t first make a copy and then destroy the original! Consider a graphical representation of the call
vector<int> results = find_all(vs,"Greene King");
Before the return from find_all():
After the return from find_all():
The importance of move semantics is that we can basically eliminate complicated and error-prone explicit use of pointers and new/delete in most code. Such direct manipulation of memory can disappear into the implementation of handles, such as vector.
The move semantics is part of my general aim to make simple things simple – without loss of performance, of course. Other features that support that theme is strongly typed constant (including constexpr functions that can be evaluated at compile time), general type aliases (to clean up the syntax of generic code), and uniform and universal initialization (base on the initializer list {} syntax). The code below has examples. For a list of features, see my C++11 FAQ.
Danny: Currently, the committee is working on a new C++ standard dubbed C++14. Can you tell us more about it? Which new features are in line for example?
Bjarne: My definition of the aim of C++14 is “to complete C++11.” That is, to add things we simply didn’t have time to do before shipping C++11 or things that we didn’t quite know how to design without the experience of using C++. It is not the job of C++14 to add major new features or support significant new programming techniques. That’s for C++17. C++14 is a “minor release.”
C++14 will consist of changes to the standard itself and two or more technical reports. The technical reports will be on a file system interface (known as “File System”) and on a way of expressing constraint on template arguments (known as “Concepts Lite”).
My favorite C++14 feature is undoubtedly constraints. Consider the classical sort() example:
list<string> ls = { “Ms. Piggy”, “Kermit”, “Gonzo” }; vector<string> vs = { “John”, “Paul”, “George”, “Ringo” }; template<class Iter> void sort(Iter p, Iter q); // sort the elements in [p,q) using < sort(vs.begin(),vs.end()); // fine sort(ls.begin(),ls.end()); // disaster
The result of the attempt to sort() the list will be a spectacularly long and confusing list of error messages. Good C++ programmers know that sort() requires a random-access iterator, but the compiler doesn’t read manuals, and I did not actually say what sort() requires of its template argument.
In the committee, we tried to solve this for C++11 with something called “concepts,” but ended up with a complex mess. Fortunately, we backed those “concepts” out before they managed to do harm. For C++14, we can achieve most of what we wanted simply and efficiently. For the example above we can write
template<Random_access_iterator Iter> void sort(Iter p, Iter q); // sort the elements in [p,q) using <
This will make the error message a simple one-liner. There is a branch of GCC that implements this. Importantly, it has less compile-time overhead than workarounds. The design is primarily by Andrew Sutton, Gabriel Dos Reis, and me. The implementation is by Andrew Sutton with some help from Gabriel Dos Reis. Conceptually, this design has roots in extensive work on what concepts would ideally look like and how the ideal uses would look. For example
- Andrew Sutton and Bjarne Stroustrup: Design of Concept Libraries for C++. Proc. SLE 2011. July 2011.
- B. Stroustrup and A. Sutton: Concept Design for the STL. N3351. This is known as “The Palo Alto TR” and is the work of two dozen people, including Alex Stepanov. January 2012.
The basic notion is that a requirement is simply a compile time Boolean function (a predicate) on a type or a set of types. The example above could equivalently be written
template<typename Iter> requires Random_access_iterator<Iter>() void sort(Iter p, Iter q); // sort the elements in [p,q) using <
where Random_access_iterator is just a constexpr function. Now all the rules for “Concepts Lite” fall out of simple Boolean algebra.
For example, I can define a version of sort() that doesn’t require a caller to use iterators explicitly, we
template<Container Cont> requires Random_access_iterator<Iterator<Cont>>() void sort(Cont& c) { sort(c.begin(),c.end()); }
A Container basically is something with a begin(), end(), and a size(). The Iterator<Cont> is Cont’s iterator type. Now, we can sort() a vector:
sort(vs);
The compiler evaluates the predicate to verify that Container<vector>() is true. Given that, is is trivial to add sorting of lists:
template<Container Cont> requires Forward_iterator<Iterator<Cont>>() void sort(Cont& c) { vector<Value_type<Cont>> v {c.begin(),c.end()}; sort(v); copy(v.begin(),v.end(),c.begin()); }
The simplest and typically the most efficient way to sort a list is to copy it into a vector, sort the vector, and copy it back again. That’s what I did. Value_type<Cont> is (obviously) Cont’s value_type. We can now write:
sort(ls);
The compiler picks the right sort(). Obviously, it could not pick any other sort() because their predicates (constraints, concepts) failed.
This makes the definition and use of templates much simpler, much more similar to "ordinary" programming, and much more pleasant to work with. Let me emphasize: there is no run-time overhead, the compilation is faster than workarounds, and the language rules are simple. One of my long-term aims is to make simple things simple. We made progress on that with C++11 and we’ll do even better in C++14.
A description of “Concepts Lite” proposal as it was a couple of months ago can be found on the ioscpp site: http://isocpp.org/blog/2013/02/concepts-lite-constraining-templates-with-predicates-andrew-sutton-bjarne-s.
And yes. I do think 2014 is realistic for a committee decision, and implementations of the major C++14 are already available for experimentation.
Danny: Speaking of the future of C++, there have been several proposals to add modules to C++. Can you shed more light on modules? What are they exactly? What are they good for? Where do the modules proposals stand these days?
Bjarne: I can’t say much. Doug Gregor is working on a proposal based on work he has been doing with C and Objective C at Apple, and David Vandevoorde from EDG is also trying to contribute. The work is done as a WG21 “Study Group” (see http://www.isocpp.org/std/the-committee). There is a strong and appropriate focus on allowing a transition from current techniques for industrial-sized systems, as opposed to defining an ideal module construct from first principles.
Danny: Your The C++ Programming Language, 4th edition covers many C++11 features including concurrency, lambdas, constexpr and regular expressions. Is C++11 is easier to teach than say C++98? How is that reflected in your book?
Bjarne: TC++PL4 is supposed to be comprehensive (and is), so I haven’t been able to take much advantage of the opportunities to simplify the presentation. TC++PL4 is a complete rewrite of TC++PL3 and is organized differently, so maybe it is more approachable. It contains a four-chapter “Tour of C++” that covers the complete language and standard library in less than 100 pages (Chapters 2-5). This tour is freely available on the web, so people can judge for themselves. Apart from the tour, the book is conventionally organized in chapters on “The Basics,” “Abstraction Mechanisms,” and “The Standard Library.” The description of the standard library alone is 424 pages – and those pages are dense with useful information.
Of course, there are chapters on concurrency and one on regular expressions. In fact, there is at least one chapter for each major component of the standard library. Similarly, there are sections on lambda expressions and constexpr, as well as for all the new features, such as auto, range-for loops, uniform and universal initialization using {}, move operations, in-class member initializers, etc. There are many new features. However, TC++PL4 is not simply a list of features. I try hard to show how to use the new features in combination. C++11 is a synthesis that supports more elegant and efficient programming styles.
It may be worth it to be explicit: C++11 feel like a new language. You can write more elegant, better performing and more maintainable code in C++11 than you could in C++98. TC++PL4 reflects that, so even though almost every sentence in TC++PL3 is still correct, the book no longer consistently reflects “best practices.” We have learned a lot over the last 13 years.
TC++PL is – as ever – aimed at people who are already programmers. For complete beginners and for people with weak skills in the kinds of programming most commonly done in C++, I recommend Programming: Principles and Practice using C++. That does not cover C++11, but it does take a modern approach to C++ and programming in general.
Danny: At least some of your readers are newcomers who haven’t programmed in C++ before. What is your advice for learning C++ from scratch? I bet you wouldn’t recommend that they learn C first before learning C++, as some experts used to suggest...
Bjarne: I have doubts about the areas of expertise of people who would still suggest the “C first” approach to C++. My textbook, Programming: Principles and Practice using C++ (PPP), is an extensive demonstration of my ideas of how to teach programming and C++ to novices. It has also been used for medium-level programmers and even industrially for experienced programmers coming to C++ from significantly different languages.
Basically, C is not the best subset of C++ to learn first. The “C first” approach forces students to focus on workarounds to compensate for weaknesses and a basic level of language that is too low for much programming. Consider a trivial function to compose an email address:
string compose(const string& name, const string& domain) { return name+’@’+domain; }
Which can be used like this
string addr = compose(“bs”,”cs.tamu.edu”);
The C version requires explicit manipulation of characters
char* compose(const char* name, const char* domain) { char* res = calloc(strlen(name)+strlen(domain)+2); // space for strings, ‘@’, and 0 char* p = strcpy(name,res); *p = ‘@’; strcpy(domain,p+1); return res; }
Which can be used like this
char* addr = compose(“bs”,"cs.tamu.edu”); // ... free(addr); // release memory when done
Which version would you rather teach? Which version is easier to use? Did I really get the C version right? Are you sure? Why?
Finally, which version is likely to be the most efficient? Yes, the C++ version because it does not have to count the argument characters and does not have to use the free store (dynamic memory) for short argument strings.
This is not an odd isolated example. I consider it typical. So why do so many teachers insist on the “C first” approach? I suspect that it is mainly because that’s what they have done for decades. Because that’s what the curriculum requires. Because that’s the way the teachers learned it in their youth. Because C is considered simpler than C++. Because the students have to learn C (or the C subset of C++) sooner or later anyway.
None of these reasons hold water. In particular, it is easier to learn to write good C code when you know a bit of C++, so maybe we should switch to a “C++ first” approach to teaching C? A good C++ programmer also knows C. PPP has a chapter on C, and I consider PPP a rather good introduction to C, assuming that you need to know both C and C++.
Danny: In one of our former interviews you expressed reluctance towards using lambda expressions and attributes in new C++ projects. Do you still think that these features (especially lambdas) are best avoided?
Bjarne: Every powerful new feature will be overused until programmers settle on a set of effective techniques and find which uses impede maintenance and performance. I don’t think I ever described lambda expressions as “best avoided,” but – as with all new features – I do encourage a bit of caution. I have already seen multi-page lambdas, but I prefer all non-trivial operations to be named. If you need a lambda of more than an expression or two, name it. A good name indicates intent, and separating definition and use gives the opportunity for a well-chosen comment. Complicated expressions (with or without lambdas) can be a source of errors and a maintenance hazard.
So, lambdas can be used for “write-only code.” They can also be used for terse, efficient, and elegant code. There are many situations where a simple lambda is the obviously more readable solution compared to alternatives. For example:
sort(vd,[](double x, double y) { return abs(x)>abs(y); });
Here, I assume that I have defined a sort() function for a container (as in my answer to question 2).
Given that C++11 supports concurrency, a major use of lambdas is to specify tasks to be executed. For example:
parallel_invoke( // invoke the three argument tasks in parallel [&]{ some_function(a,b,c); }, [&]{ some_other_function(c,d,e); } , [&]{ some_third_function(x,y,e); } );
This parallel_invoke() is being proposed for C++17. Here, I take advantage of the lambda's ability to access local data (e.g., a, b, and c). These functions better be independent. In particular, c and e should probably be immutable (const).
Danny: I recently interviewed Robert Seacord, a well-known authority on software security. He sounds only partially pleased with how the C and C++ committees address the issue of security. How much emphasis does the C++ standards committee put on making the language more secure? Are there new proposals that are aimed to improve the inherent security of C++?
Bjarne: I do not consider it the job of a programming language to be “secure.” Security is a systems property and a language that is – among other things – a systems programming language cannot provide that by itself. C++ offers protection against errors, rather than protection against deliberate violation of rules. C++11 is better at that than C++98, but the repeated failures of languages that did promise security (e.g. Java), demonstrates that C++’s more modest promises are reasonable. Unfortunately, “we” have built an infrastructure with weak interfaces, poor hardware protection, and a maze of complex communication protocols that seriously complicates security, but that’s not for any individual language or even for all programming languages to solve. Trying to address security problems by having every programmer in every language insert the right run-times checks in the code is expensive and doomed to failure.
Eventually, people will have to rely on specialized tools rather than simply on language features and coding guidelines.
That said, C++ can be used far better than what you get from a novices from a C or Java background. If you use iostreams rather than stdio, you are immune to simple and direct injection attacks. If you use containers (that know when and how to grow to accommodate more elements) rather than arrays, you don’t encounter buffer overflows. If you leave resource management to handles and containers relying on RAII, rather than littering your code with pointer, news and deletes, you don’t encounter resource leaks or write to freed memory.
However, there is no protection against a cast from an integer value to a pointer (as needed in device drivers) or against deliberate access outside the bounds of an array. Nor, of course, is there any protection against someone passing an unchecked string from input to a database interface.
Using a modern style of C++, you can avoid almost all casts and reduce type violations of all kinds to something close to zero. Conversely, if you insist on sticking to a conventional C style of C++ code, you’ll get conventional C-style problems.
Danny: Some 15 years ago I thought that inline would become similar to register – a keyword that compilers can completely ignore because they know better than the average programmer how to optimize code. Have we reached that point already with respect to inline? Are there other C++ features that seem redundant in 2013?
Bjarne: register was made redundant by a simple and general algorithm that cannot be beaten by humans. There still is no such simple and general alternative to inline, and I don’t expect to see one any day soon. I do not know how well the mythical “average programmer” can handle inline, but certainly restrained use of inline can be massively useful.
My own rule of thumb is to use inlining (explicitly or implicitly) only for simple one- or two-line functions that I know to be frequently used and unlikely to change much over the years. Things like the size() function for a vector. The best uses of inlining is for function where the body is less code than the function call and return mechanism, so that the inlined function is not only faster than a non-inlined version, but also more compact in the object core: smaller and faster.
Danny: You've addressed a lot of unfair criticism and myths about C++ before. One of those myths is that one needs Garbage Collection (GC) for reliable software. What’s your take on that? Would it be correct to say that GC has lost its luster, or are there plans to add it to C++ in the future?
Bjarne: I consider GC the last alternative after cleaner, more general, and better localized alternatives to resource management have been exhausted.
GC is fundamentally a global memory management scheme. Clever implementations can compensate, but some of us remember too well when 63 processors of a top-end machine were idle when 1 processor cleaned up the garbage for them all. With clusters, multi-cores, NUMA memories, and multi-level caches, systems are getting more distributed and locality is more important than ever.
Memory is not the only resource. A resource is anything that has to be acquired and (explicitly or implicitly) released after use. Examples are memory, locks, sockets, file handles, and thread handles. A good resource management system handles all kinds of resources. If the release does not happen, we have a leak, and since there is a finite amount of each kind of resource, eventually the system grinds to a halt. You don’t need an actual leak to cause bad effects; excessive resource retention can be almost as bad. For example, if a system holds on to memory, locks, files, etc., for twice as long, the system needs to be provisioned with potentially twice as many resources.
So, I say that C++ is my favorite GC language because it generates so little garbage. C++11 supports a garbage collection interface, so that you can use GC as a last resort (for memory); but before resorting to that, I suggest systematic use of resource handles: Let each resource have an owner in some scope and by default be released at the end of its owner’s scope. This, by the way, is exactly the way Unix handles memory, locks, and files on a per-process basis. In C++, this is known as RAII (“Resource Acquisition Is Initialization”) and is integrated with error handling in the form of exceptions. Resources can be moved from scope to scope using move semantics or “smart pointers,” and shared ownership can be represented by “shared pointers,” shared_ptr, which is a pointer to a shared object that is released (destroyed) when its last shared_ptr goes out of scope (is destroyed).
In the C++11 standard library, RAII is pervasive: for example, memory (string, vector, map, unordered_map, etc.), files (ifstream, ofstream, etc.), threads (thread), locks (lock_guard, unique_lock, etc.), and general objects (through unique_ptr and shared_ptr). The result is implicit resource management that is invisible in common use and leads to low resource retention durations.