C++11 Features
Complete reference for C++11 language and library features — the revision that modernised C++ from the ground up.
C++11since C++11C++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.
#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 moveThe 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
#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
#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
#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.
#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
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 0constexpr
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 morenoexcept
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
#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
// 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 onlyoverride and final
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
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 chainstatic_assert
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 validLibrary Additions
Smart Pointers
#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
#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
#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); // 8Prefer lambdas over std::bind in C++14+ for readability and performance; std::bind is retained for completeness.
Hash Containers
#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>
#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
autofor 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_ptrover rawnew/delete; escalate toshared_ptronly when shared ownership is genuinely required. - Use
overrideon every overriding function — it turns silent signature mismatches into compile errors. - Replace
NULLand0for pointers withnullptreverywhere; it is typed and resolves overloads correctly. - Avoid
std::bindin new code; a lambda captures intent and compiles to better code.
Common Pitfalls
initializer_listconstructor 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 aninitializer_listconstructor.autostrips references andconst:auto x = some_ref;gives a copy, not a reference. Useauto&orconst auto&deliberately.- Moved-from objects are valid but unspecified: after
std::move(obj), do not readobjuntil 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::threaddestructor callsstd::terminateif the thread is joinable and was not joined or detached — always join or detach before destruction.
Concurrency Library in Depth
C++11 introduced the first standardised memory model and a full suite of concurrency primitives. Before C++11, multi-threaded code relied on platform extensions (pthreads, Win32 threads) with no language-level guarantees; C++11 defined happens-before relationships that compilers must respect across all architectures.
// C++11
#include <thread>
#include <mutex>
#include <condition_variable>
#include <atomic>
#include <future>
#include <chrono>
// --- std::thread ---
void worker(int id, std::atomic<int>& done) {
// do work
++done;
}
// --- mutex + condition_variable ---
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void producer() {
{
std::lock_guard<std::mutex> lk(mtx);
ready = true;
}
cv.notify_one();
}
void consumer() {
std::unique_lock<std::mutex> lk(mtx);
cv.wait(lk, [] { return ready; }); // spurious-wakeup safe
}
// --- std::future / std::async ---
int heavy_compute(int n) { return n * n; }
void async_demo() {
// C++11: launch in a new thread, get result later
std::future<int> fut = std::async(std::launch::async, heavy_compute, 42);
// ... do other work ...
int result = fut.get(); // blocks until ready; re-throws exceptions
}std::promise<T> is the write end of a future channel: one thread sets the value via promise.set_value() and another retrieves it through the associated future.get(). std::packaged_task<F> wraps any callable so its return value or exception is captured into a future automatically.
// C++11
#include <future>
#include <thread>
std::promise<int> prom;
std::future<int> fut = prom.get_future();
std::thread t([&prom] {
prom.set_value(99); // or set_exception(std::make_exception_ptr(...))
});
int v = fut.get(); // 99
t.join();std::atomic<T> guarantees indivisible load/store/RMW operations. For primitive types (int, bool, pointers) the standard library typically implements these without a mutex. Use std::memory_order_relaxed / _acquire / _release / _seq_cst to tune the fence strength when you understand the synchronisation requirements.
Uniform Initialisation Caveats
Brace initialisation is a powerful unifier but std::initializer_list constructors take priority in overload resolution, producing surprising results. When a type has a constructor that accepts std::initializer_list<T> and the brace-enclosed elements are convertible to T, that constructor wins even over a seemingly better match.
// C++11
#include <vector>
void overload_surprise() {
std::vector<int> a(10, 2); // 10 elements, each 2 — calls (count, value) ctor
std::vector<int> b{10, 2}; // 2 elements: 10 and 2 — calls initializer_list ctor
// The same trap with user-defined types:
struct Widget {
Widget(int w, int h) { /* size ctor */ }
Widget(std::initializer_list<int> il) { /* list ctor */ }
};
Widget w1(640, 480); // size ctor
Widget w2{640, 480}; // list ctor — both 640 and 480 go into the list
}The workaround is to use parentheses when you specifically want the non-list constructor and braces only when you mean to pass a list. For class template authors, mixing std::initializer_list constructors with constructors that take numeric arguments requires careful documentation.
// C++11 — narrowing is still caught even inside initializer_list
#include <vector>
void narrowing_check() {
// double to int is a narrowing conversion — ill-formed with brace-init
// std::vector<int> bad{1.5, 2.7}; // compile error
// Explicit cast suppresses the error but is your responsibility
std::vector<int> ok{ static_cast<int>(1.5), static_cast<int>(2.7) };
}A related subtlety: auto x{42} deduces std::initializer_list<int> in C++11/14 but deduces int from C++17 onward. Always prefer auto x = 42; for scalar initialisation to avoid the ambiguity entirely.
C++11 to C++14 Migration Highlights
C++14 is best understood as C++11 with the sharp edges filed off. The three most impactful improvements — generic lambdas, variable templates, and relaxed constexpr — remove boilerplate without changing the underlying model.
// C++14: generic lambdas — auto parameters create a template operator()
#include <algorithm>
#include <vector>
#include <string>
auto print_any = [](const auto& v) {
for (const auto& x : v) { /* print x */ }
};
std::vector<int> vi{1, 2, 3};
std::vector<std::string> vs{"a", "b"};
print_any(vi);
print_any(vs); // same lambda, different instantiationGeneric lambdas replace most uses of hand-written function objects in algorithmic code. The auto&& capture form combined with std::forward<decltype(x)>(x) achieves perfect forwarding inside a lambda — a pattern that required verbose template machinery in C++11.
// C++14: variable templates — parametrised compile-time constants
template<typename T>
constexpr T pi = T(3.14159265358979323846L);
float pif = pi<float>; // 3.14...f
double pid = pi<double>; // full precision
// C++14: relaxed constexpr — loops, multiple statements, local variables allowed
constexpr int sum_to(int n) {
int total = 0;
for (int i = 1; i <= n; ++i)
total += i;
return total;
}
static_assert(sum_to(10) == 55, "");C++14 also introduced std::make_unique<T> (filling the gap left in C++11), std::exchange, digit separators (1'000'000), and binary literals (0b1010). The combined effect is that a codebase written in idiomatic C++14 reads significantly cleaner than one restricted to C++11 while remaining fully ABI-compatible with C++11 object files from the same compiler.