T.TAO
Back to Blog
/9 min read/Programming

C++ #9 C++11 New Features

#C++#Programming#ComputerSystems

This note covers the new features in C++11.

Eight years separated C++11 from the previous standard, and the changes were large enough that people said it felt like a new language. What it introduced was not only syntactic sugar: move semantics changed the cost model of passing by value, smart pointers changed the default approach to resource management, and lambdas made the standard library algorithms genuinely usable. The tour below goes roughly in order of importance rather than alphabetically.

Automatic type deduction β€” auto

auto lets the compiler deduce a variable's type from its initializer, making code more concise and removing redundant type declarations.

C++auto x = 10;                          // int
auto it = vec.begin();                // std::vector<int>::iterator, unspelled
auto p = std::make_unique<Widget>();  // std::unique_ptr<Widget>

The deduction rules matter: auto drops references and top-level const.

C++const std::string s = "hello";
auto  a = s;         // std::string β€” a copy, and the const is gone
auto& b = s;         // const std::string& β€” preserved
const auto& c = s;   // const std::string&, the usual read-only form

This is critical for container iteration: for (auto x : vec) copies every element, which is a real cost when the container holds std::string. The default should be for (const auto& x : vec).

More auto is not automatically better, either. When the returned type is not obvious (auto result = compute();), the reader has to jump to the definition to find out what they are holding. The rule of thumb: use auto when the type is obvious or tedious to spell, and write it out otherwise.

Range-based for loops

A concise syntax for iterating a container:

C++std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto& elem : vec) {
    std::cout << elem << std::endl;
}

It expands into an ordinary loop over begin() / end(). Any type providing those two members β€” or free functions findable by ADL β€” works, including your own.

One trap: do not change the container's size inside a range-based for. A vec.push_back() may reallocate, invalidating the internal iterators, and the behaviour is undefined.

Smart pointers

C++11 introduced std::unique_ptr and std::shared_ptr to manage dynamic memory automatically, avoiding the errors that come with manual deallocation.

  • std::unique_ptr β€” exclusive ownership, movable but not copyable. Zero overhead: it is the size of a raw pointer and its destructor is one delete. This should be the default choice.
  • std::shared_ptr β€” shared ownership, with reference counting managing the lifetime. Not free: there is a control block beside the object, and the count updates are atomic.
  • std::weak_ptr β€” a non-owning observer, used to break shared_ptr cycles.
C++auto p = std::make_unique<Widget>(args...);   // make_unique arrived in C++14
auto q = std::make_shared<Widget>(args...);

void consume(std::unique_ptr<Widget> w);
consume(std::move(p));    // ownership transfers; p is null afterwards

On cycles: two objects each holding a shared_ptr to the other never reach a count of zero, and the memory leaks. Making one direction a weak_ptr fixes it. The convention in a parent/child structure is a shared_ptr down and a weak_ptr back up.

Lambdas

Lambda expressions define anonymous functions concisely, and suit callbacks and small computations particularly well.

C++auto add = [](int a, int b) -> int {
    return a + b;
};
std::cout << add(3, 4) << std::endl;

The full form is [capture](params) specifiers -> return { body }, where the capture list decides which enclosing variables the lambda can see:

C++int factor = 3;
auto f1 = [factor](int x)  { return x * factor; };   // by value: a snapshot
auto f2 = [&factor](int x) { return x * factor; };   // by reference: current value
auto f3 = [=](int x)       { return x * factor; };   // everything by value
auto f4 = [&](int x)       { return x * factor; };   // everything by reference

Capturing by reference demands attention to lifetime. Store the lambda in a member or an async task while the captured local has already gone out of scope, and you have a dangling reference. Default to capturing by value and switch to a reference only when you need to modify outer state.

A lambda is really an anonymous class generated by the compiler: captures become its members and the body becomes operator(). Each lambda therefore has its own distinct type β€” two identical-looking lambdas have different types, which is why you hold them in auto. When a uniform type is required, use std::function, at the cost of type erasure.

Move semantics and rvalue references

C++11 introduced move semantics and rvalue references, written with &&. Move semantics let a program avoid unnecessary copies for better performance.

An rvalue reference binds to a temporary (an rvalue) and transfers its resources through a move constructor or move assignment operator.

C++std::vector<int> vec1 = {1, 2, 3};
std::vector<int> vec2 = std::move(vec1);
// vec1's resources move to vec2, with no copy

There is a simple test for lvalue versus rvalue: if you can take its address it is an lvalue. A named variable is always an lvalue β€” even when its declared type is an rvalue reference:

C++void f(std::string&& s) {
    g(s);              // s is named, so this passes an lvalue!
    g(std::move(s));   // this reaches g's move overload
}

That single point is the source of nearly every move-semantics bug. For how to write a move constructor correctly, why noexcept is decisive, and the Rule of Five, see C++ #7 Copy Control and Operator Overloading.

Perfect forwarding

A T&& in a template is not an rvalue reference but a forwarding reference: given an lvalue it deduces T&, given an rvalue it deduces T. Paired with std::forward, the argument's value category passes through unchanged:

C++template <typename T, typename... Args>
std::unique_ptr<T> make(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

Like std::move, std::forward is only a cast. The difference is that move casts to an rvalue unconditionally, while forward does so only when the original argument was one.

The nullptr keyword

C++11 introduced nullptr as the null pointer constant, replacing NULL and providing stronger type safety.

The problem is that NULL is usually just 0:

C++void f(int);
void f(char*);

f(NULL);      // calls f(int)! Almost certainly not what you meant
f(nullptr);   // calls f(char*)

nullptr has type std::nullptr_t, converts implicitly to any pointer type, and does not convert to an integer. NULL should not appear in new code.

The using keyword and type aliases

using creates type aliases, replacing the traditional typedef syntax and reading better in template contexts.

C++using int_ptr = int*;
typedef int* int_ptr_old;             // equivalent

The real advantage is the alias template, which typedef cannot express:

C++template <typename T>
using Dict = std::map<std::string, T>;

Dict<int> counts;    // std::map<std::string, int>

And because using reads left to right, complicated declarations such as function pointers become much clearer:

C++typedef void (*Callback)(int, const char*);    // read outwards from the middle
using   Callback = void (*)(int, const char*); // name on the left, type on the right

The constexpr keyword

constexpr defines compile-time constants. Unlike const, constexpr guarantees the expression is evaluated at compile time.

C++constexpr int square(int x) {
    return x * x;
}
constexpr int result = square(5);   // 25, computed while compiling

The distinction is worth stating plainly: const means "this variable will not be modified afterwards" β€” read-only-ness; constexpr means "this value is known while compiling" β€” compile-time evaluability. const int n = readFromFile(); is legal; constexpr int n = readFromFile(); is not.

C++11's constexpr functions were heavily restricted β€” essentially a single return statement, so anything with a loop had to be recursive. C++14 relaxed that; see C++ #10 C++14 New Features.

Variadic templates

Variadic templates let a template accept any number of arguments of any type. The type-unsafe varargs of printf finally have a replacement.

C++template <typename... Args>
void log(const Args&... args);

typename... Args is a template parameter pack and args... is a function parameter pack. In C++11, expanding a pack means recursion plus a terminating overload:

C++void print() {}                        // base case

template <typename T, typename... Rest>
void print(const T& first, const Rest&... rest) {
    std::cout << first << ' ';
    print(rest...);                    // peel one off, recurse on the rest
}

print(1, "hello", 3.14);               // prints: 1 hello 3.14

sizeof...(Args) gives the number of elements in the pack. C++17 collapses this recursion into a one-line fold expression: (std::cout &lt;&lt; ... &lt;&lt; args);

The most important uses of variadic templates are inside the standard library itself: std::make_unique, std::tuple and emplace_back all rely on them, combining with perfect forwarding to pass arbitrary constructor arguments straight through to the target type.

Explicitly defaulted and deleted functions

C++11 lets you explicitly request or forbid the compiler-generated special members.

C++class Widget {
public:
    Widget() = default;                          // I do want a default constructor
    Widget(const Widget&) = delete;              // copying is forbidden
    Widget& operator=(const Widget&) = delete;
    Widget(Widget&&) noexcept = default;         // the generated move is fine
};

Before this, the way to forbid copying was to declare the copy constructor private and never define it β€” which produced a link-time error with an unhelpful message. = delete turns it into a clear compile error.

= delete is not limited to special members; it can also suppress unwanted overloads:

C++void process(int);
void process(char) = delete;    // stop char from being promoted to int

Other things worth knowing

override and final. Explicitly mark an overriding virtual function, so a mistyped signature is a compile error rather than a silently-created new function. This is the single most practical defence described in C++ #8 Polymorphism.

Uniform initialization. The {} syntax works for every type and forbids narrowing conversions: int x{3.14}; is an error where int x = 3.14; is only a warning. Mind its interaction with std::initializer_list overloads β€” std::vector&lt;int> v{3, 1} gives two elements, not three copies of 1.

enum class. Strongly typed enumerations that do not convert implicitly to integers and do not leak their enumerators into the enclosing scope. The name-collision problem of plain enum disappears.

Threading in the standard library. std::thread, std::mutex, std::atomic and std::future arrived together with a memory model, giving C++ a platform-independent concurrency story for the first time.

std::array and std::unordered_map. A zero-overhead wrapper for fixed-size arrays, and hash-based associative containers.

Wrapping up

Three C++11 features genuinely changed how people write code: smart pointers made raw new/delete a code smell, move semantics made returning by value cost-free to reason about, and lambdas took standard library algorithms from theoretically usable to actually pleasant. Most of the rest exists to support those three.

  1. 01C++ #1 Data and Memory
  2. 02C++ #2 Struct and Union
  3. 03C++ #3 Pointers and Arrays
  4. 04C++ #4 Functions
  5. 05C++ #5 Objects and Classes
  6. 06C++ #6 Inheritance
  7. 07C++ #7 Copy Control and Operator Overloading
  8. 08C++ #8 Polymorphism
  9. 09C++ #9 C++11 New Features
  10. 10C++ #10 C++14 New Features
  11. 11C++ #11 C++17 New Features