Skip to content
C++
Language
since C++11
Intermediate

C++11 Features

Complete reference for C++11 language and library features — the revision that modernised C++ from the ground up.

C++11since C++11

C++11 is the ISO standard ratified on 12 August 2011 that introduced move semantics, lambdas, variadic templates, a threading model, and dozens of other features that collectively transformed C++ into a modern systems language.

Overview

C++11 ended an eight-year gap after C++03 and is widely considered the dividing line between "old C++" and "modern C++". The changes span three tiers: core language semantics (move semantics, rvalue references), metaprogramming machinery (variadic templates, constexpr, decltype), and a dramatically expanded standard library (smart pointers, <thread>, <chrono>, <regex>, hash containers). Every feature described below requires at minimum a C++11-conforming compiler; later additions in C++14/17/20 are noted explicitly.


Core Language Features

Move Semantics and Rvalue References

C++11 introduced rvalue references (T&&) and the companion utilities std::move and std::forward. Move semantics allow classes to transfer ownership of resources instead of copying them.

cpp
#include <utility>
#include <vector>

class Buffer {
    int* data_;
    std::size_t size_;
public:
    explicit Buffer(std::size_t n) : data_(new int[n]), size_(n) {}

    // C++11: move constructor
    Buffer(Buffer&& other) noexcept
        : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;
        other.size_ = 0;
    }

    // C++11: move assignment
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }

    ~Buffer() { delete[] data_; }
};

Buffer make_buffer(std::size_t n) { return Buffer(n); } // NRVO or move

The Rule of 5 (extended from Rule of 3 in C++11) means that if you define any of destructor, copy constructor, copy assignment, move constructor, or move assignment, you must consider all five.

Lambda Expressions

cpp
#include <algorithm>
#include <vector>

void demo_lambdas() {
    std::vector<int> v = {4, 1, 7, 2, 9, 3};

    // C++11: basic capture by value and reference
    int threshold = 5;
    auto count_above = std::count_if(v.begin(), v.end(),
        [threshold](int x) { return x > threshold; }); // captures threshold by value

    // C++11: mutable lambda — local copy is modifiable
    int total = 0;
    std::for_each(v.begin(), v.end(),
        [&total](int x) mutable { total += x; });

    // C++11: immediately invoked
    auto result = [](int a, int b) { return a * b; }(6, 7); // 42
}

Lambdas generate a unique, unnamed closure type. The type is not speakable; use auto or std::function<R(Args...)> (C++11) when you need to store one.

auto and decltype

cpp
#include <map>
#include <string>

// C++11: auto deduces from initialiser, strips cv-qualifiers and references
auto x = 42;           // int
auto& y = x;           // int&
const auto z = x;      // const int

// C++11: decltype preserves exact type including references
decltype(x) a = 0;     // int
decltype((x)) b = x;   // int& — extra parens = lvalue expression

// C++11: trailing return type for dependent types
template<typename A, typename B>
auto multiply(A a, B b) -> decltype(a * b) {
    return a * b;
}

// C++14 simplification: return type deduction without trailing syntax
// template<typename A, typename B>
// auto multiply(A a, B b) { return a * b; }

void iterate(const std::map<std::string, int>& m) {
    for (auto it = m.begin(); it != m.end(); ++it) { // C++11
        // it is std::map<std::string,int>::const_iterator
    }
}

Range-Based for

cpp
#include <vector>
#include <string>

void process(std::vector<std::string>& names) {
    for (auto& name : names) {         // C++11: by reference
        name += " (processed)";
    }
    for (const auto& name : names) {   // C++11: by const reference — prefer for read-only
        // use name
    }
}

The loop calls begin() and end() via ADL, so it works on any type that provides those — including your own containers.

Uniform Initialisation

C++11 standardised brace-init ({}) as a universal syntax that prevents narrowing conversions and works on aggregates, constructors, and even scalars.

cpp
#include <vector>
#include <string>

struct Point { int x, y; };

void init_examples() {
    int a{42};                          // scalar
    Point p{1, 2};                      // aggregate
    std::vector<int> v{1, 2, 3, 4};    // initializer_list constructor
    std::string s{"hello"};

    // Narrowing is ill-formed — caught at compile time
    // double d = 3.14;
    // int bad{d};  // error: narrowing conversion
}

std::initializer_list<T> (C++11) is the mechanism that lets constructors accept brace-enclosed lists.

nullptr

cpp
void f(int);
void f(char*);

// C++98: f(NULL) — ambiguous or calls f(int)
// C++11: f(nullptr) — unambiguously calls f(char*)
f(nullptr);

int* p = nullptr;           // clearly a null pointer, not integer 0

constexpr

cpp
constexpr int factorial(int n) {  // C++11: must be a single return statement
    return n <= 1 ? 1 : n * factorial(n - 1);
}

constexpr int f120 = factorial(5); // evaluated at compile time

// C++14 relaxed constexpr: loops and local variables allowed
// C++20 constexpr: virtual functions, try/catch, and more

noexcept

cpp
void swap_safe(int& a, int& b) noexcept {  // C++11
    int tmp = a; a = b; b = tmp;
}

// noexcept(expr) — conditional specification
template<typename T>
void move_if_noexcept_demo(T& a, T& b)
    noexcept(noexcept(std::swap(a, b))) {
    std::swap(a, b);
}

Marking move constructors and swap noexcept is critical: std::vector will use moves during reallocation only if they are guaranteed not to throw.

Variadic Templates

cpp
#include <iostream>

// C++11: parameter pack
template<typename T>
void print(T val) { std::cout << val << '\n'; }

template<typename T, typename... Args>
void print(T first, Args... rest) {
    std::cout << first << ' ';
    print(rest...);   // recursive pack expansion
}

// C++17 fold expressions make this cleaner, but C++11 recursion works
print(1, "hello", 3.14, 'x');

Scoped Enumerations

cpp
// C++98: unscoped, pollutes enclosing namespace, implicit int conversion
enum Color98 { Red, Green, Blue };

// C++11: scoped, no implicit conversion, controllable underlying type
enum class Color : uint8_t { Red, Green, Blue };

Color c = Color::Red;  // must qualify
// int i = c;          // error: no implicit conversion
int i = static_cast<int>(c);  // explicit only

override and final

cpp
struct Base {
    virtual void draw() const;
    virtual void resize(int factor);
};

struct Derived : Base {
    void draw() const override;         // C++11: compiler verifies override
    void resize(int factor) override;
    // void drwa() const override;      // C++11: error caught at compile time
};

struct Leaf final : Derived {           // C++11: cannot be further derived
    void draw() const override;
};

Deleted and Defaulted Functions

cpp
struct NonCopyable {
    NonCopyable() = default;                          // C++11: explicit default
    NonCopyable(const NonCopyable&) = delete;         // C++11: suppress copy
    NonCopyable& operator=(const NonCopyable&) = delete;
};

// Also useful to delete non-sensical overloads:
void process(int);
void process(double) = delete;  // prohibit accidental float->double->int chain

static_assert

cpp
template<typename T>
void must_be_integral(T) {
    static_assert(std::is_integral<T>::value,  // C++11
                  "T must be an integral type");
}
// C++17: static_assert(expr) without message is also valid

Library Additions

Smart Pointers

cpp
#include <memory>

// C++11: unique ownership
auto buf = std::make_unique<int[]>(1024);  // make_unique: C++14
std::unique_ptr<int[]> raw(new int[1024]); // C++11 form

// C++11: shared ownership with reference counting
auto shared = std::make_shared<std::string>("hello");
std::weak_ptr<std::string> weak = shared;  // non-owning observer

if (auto locked = weak.lock()) {           // safe access
    // locked is shared_ptr, shared stays alive
}

Prefer std::make_shared (C++11) over new; it performs a single allocation for both the object and control block.

Threading

cpp
#include <thread>
#include <mutex>
#include <atomic>

std::mutex mtx;
std::atomic<int> counter{0};  // C++11: lock-free for suitable types

void worker(int id) {
    ++counter;                          // atomic increment
    std::lock_guard<std::mutex> lk(mtx); // C++11: RAII lock
    // critical section
}

void launch() {
    std::thread t1(worker, 1);
    std::thread t2(worker, 2);
    t1.join();
    t2.join();
}

C++11 defined the first standardised memory model, making portable concurrent code possible without platform-specific intrinsics.

std::function and std::bind

cpp
#include <functional>

int add(int a, int b) { return a + b; }

std::function<int(int, int)> fn = add;        // C++11: type-erased callable
auto add5 = std::bind(add, std::placeholders::_1, 5); // C++11: partial application
int result = add5(3); // 8

Prefer lambdas over std::bind in C++14+ for readability and performance; std::bind is retained for completeness.

Hash Containers

cpp
#include <unordered_map>
#include <unordered_set>

std::unordered_map<std::string, int> freq;  // C++11: average O(1) lookup
freq["foo"]++;
freq.reserve(1024);                         // hint bucket count upfront

<chrono>

cpp
#include <chrono>

auto start = std::chrono::steady_clock::now();          // C++11
// ... work ...
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
    std::chrono::steady_clock::now() - start);

Best Practices

  • Default to auto for iterator and complex return types; reserve explicit types where clarity is needed for readers.
  • Mark move constructors noexcept — containers use this information to avoid defensive copies during reallocation.
  • Prefer unique_ptr over raw new/delete; escalate to shared_ptr only when shared ownership is genuinely required.
  • Use override on every overriding function — it turns silent signature mismatches into compile errors.
  • Replace NULL and 0 for pointers with nullptr everywhere; it is typed and resolves overloads correctly.
  • Avoid std::bind in new code; a lambda captures intent and compiles to better code.

Common Pitfalls

  • initializer_list constructor hijacking: std::vector<int> v(10, 0) creates 10 zeros; std::vector<int> v{10, 0} creates two elements. Always know whether a type has an initializer_list constructor.
  • auto strips references and const: auto x = some_ref; gives a copy, not a reference. Use auto& or const auto& deliberately.
  • Moved-from objects are valid but unspecified: after std::move(obj), do not read obj until you have assigned it a known value.
  • Lambda capture by reference outliving the captured variable: capturing a local by & in a lambda stored beyond the enclosing scope causes undefined behaviour.
  • std::thread destructor calls std::terminate if the thread is joinable and was not joined or detached — always join or detach before destruction.

See Also