Eleven items that form the backbone of everyday modern C++: braces, nullptr, scoped enums, deleted functions, override, const_iterators, noexcept, constexpr, thread-safe const, and the special member function rules.
C++11 gave us three ways to initialize a variable:
c++ int x(0); // parentheses int y = 0; // equals sign int z{ 0 }; // braces (uniform initialization)
Braces can be used everywhere: variables, data members, default arguments, heap objects, even default member initializers inside a class. Neither parentheses nor the equals sign can claim that. This universality is why the Standard calls it uniform initialization.
c++ double x = 3.14, y = 2.72; int sum1{ x + y }; // ERROR: narrowing conversion int sum2( x + y ); // OK: silently truncates int sum3 = x + y; // OK: silently truncates
And the most vexing parse — accidentally declaring a function when you wanted to default-construct an object — simply can't happen with braces:
c++ Widget w1(10); // calls Widget ctor with arg 10 Widget w2(); // OOPS: declares a function named w2! Widget w3{}; // calls Widget default ctor. No ambiguity.
But braces have a dark side. If a class has a constructor taking std::initializer_list, braces will strongly prefer that constructor — even when another constructor is a better match:
c++ std::vector<int> v1(10, 20); // 10 elements, all value 20 std::vector<int> v2{10, 20}; // 2 elements: 10 and 20
std::initializer_list constructor exists. When it does, braces hijack overload resolution to call it — even if narrowing conversions are required (causing a compile error) or another constructor is a perfect match.See which constructor gets called with each syntax.
std::vector<int> v{10, 20}; create?The literal 0 is an int. NULL is typically defined as 0 or 0L. Neither is a pointer. C++ grudgingly interprets them as null pointers when it has no other choice, but in overload resolution, they behave as integers:
c++ void f(int); void f(bool); void f(void*); f(0); // calls f(int), NOT f(void*) f(NULL); // might not compile, but never calls f(void*) f(nullptr); // calls f(void*) as intended
nullptr has type std::nullptr_t, which implicitly converts to all raw pointer types but never to any integral type. This eliminates the entire class of ambiguity bugs.
0 to a template, the compiler deduces int. Pass NULL, it deduces int (or long). Neither can convert to a smart pointer parameter. Only nullptr deduces std::nullptr_t, which converts to any pointer type.c++ template<typename F, typename M, typename P> auto lockAndCall(F func, M& mutex, P ptr) { std::lock_guard<M> g(mutex); return func(ptr); } lockAndCall(f1, f1m, 0); // ERROR: int can't convert to shared_ptr lockAndCall(f1, f1m, NULL); // ERROR: same problem lockAndCall(f1, f1m, nullptr); // OK: nullptr_t converts to shared_ptr
0 to a template that expects a pointer type fail?Both do the same job — create a shorter name for a type — but using has one killer advantage: it can be templatized.
c++ // typedef: no template support typedef std::unique_ptr<std::unordered_map<std::string, std::string>> UPtrMapSS; // alias declaration: same thing, but clearer using UPtrMapSS = std::unique_ptr<std::unordered_map<std::string, std::string>>;
With function pointers, the alias is dramatically easier to read:
c++ // typedef: the name is buried in the middle typedef void (*FP)(int, const std::string&); // alias: name on the left, type on the right using FP = void(*)(int, const std::string&);
The real power shows up with templates. A typedef can't be templatized directly — you have to wrap it in a struct. An alias template works cleanly:
c++ // Alias template: clean and simple template<typename T> using MyAllocList = std::list<T, MyAlloc<T>>; MyAllocList<Widget> lw; // clean usage // typedef hack: requires struct wrapper + typename template<typename T> struct MyAllocList { typedef std::list<T, MyAlloc<T>> type; }; MyAllocList<Widget>::type lw; // clunky
typename before the dependent type: typename MyAllocList<T>::type. The alias template version doesn't: MyAllocList<T>. Compilers know an alias template names a type, so no disambiguation needed.using) over typedef?C++98 enums leak their enumerator names into the enclosing scope. C++11 enum class keeps them scoped:
c++ // C++98: names leak into surrounding scope enum Color { black, white, red }; auto white = false; // ERROR: white already declared // C++11: names scoped to the enum enum class Color { black, white, red }; auto white = false; // fine, no conflict Color c = Color::white; // must qualify
Scoped enums also prevent implicit conversions to integers — no more accidentally passing a Color to a math function:
c++ // Unscoped: implicit conversion to int enum Color { black, white, red }; if (black < 14.5) { ... } // compiles! comparing Color to double // Scoped: no implicit conversion enum class Color { black, white, red }; if (Color::black < 14.5) { ... } // ERROR: can't compare
int. Unscoped enums need an explicit underlying type to be forward-declared. Forward-declaring enums reduces compilation dependencies — changing an enumerator doesn't force a full recompile.| Feature | Unscoped (enum) | Scoped (enum class) |
|---|---|---|
| Name scope | Leaks into enclosing scope | Contained within enum |
| Implicit conversion | Yes (to int, double, etc.) | No (requires static_cast) |
| Forward declaration | Only with explicit underlying type | Always (defaults to int) |
| Underlying type | Compiler-chosen | int (overridable) |
In C++98, the trick to prevent copying was to declare the copy constructor and copy assignment operator private and leave them undefined. It worked, but errors only surfaced at link time. C++11 gives us = delete:
c++98 // C++98: private and undefined class NoCopy { private: NoCopy(const NoCopy&); // not defined NoCopy& operator=(const NoCopy&); // not defined };
c++11 // C++11: deleted — errors at compile time class NoCopy { public: NoCopy(const NoCopy&) = delete; NoCopy& operator=(const NoCopy&) = delete; };
Two improvements: errors at compile time instead of link time, and they're public by convention so compilers give clear error messages about deletion rather than access violations.
= delete works on any function, not just members. You can delete overloads to prevent unwanted implicit conversions:c++ bool isLucky(int number); bool isLucky(char) = delete; // reject chars bool isLucky(bool) = delete; // reject bools bool isLucky(double) = delete; // reject doubles (and floats!) isLucky(42); // OK isLucky('a'); // ERROR: deleted isLucky(true); // ERROR: deleted isLucky(3.14); // ERROR: deleted
= delete have over the C++98 private-and-undefined trick?Virtual function overriding is one of the most fundamental ideas in C++, and one of the easiest to get wrong silently. For overriding to happen, all of these must match between base and derived:
| Requirement | If mismatched... |
|---|---|
| Function must be virtual in base | New function, no override |
| Names must be identical | New function, no override |
| Parameter types must match | New function, no override |
| const qualification must match | New function, no override |
| Return types must be compatible | Won't compile |
| Reference qualifiers must match (C++11) | New function, no override |
Can you spot the bugs? None of these derived functions override their base counterparts:
c++ class Base { public: virtual void mf1() const; virtual void mf2(int x); virtual void mf3() &; void mf4() const; // not virtual! }; class Derived : public Base { public: virtual void mf1(); // missing const virtual void mf2(unsigned int x); // wrong param type virtual void mf3() &&; // wrong ref-qualifier void mf4() const; // base isn't virtual };
Add override and the compiler catches every one of these bugs:
c++11 class Derived : public Base { public: void mf1() const override; // OK void mf2(int x) override; // OK void mf3() & override; // OK // void mf4() const override; // ERROR: base not virtual };
override on every derived class function that's intended to override a base class virtual. It costs nothing and catches a class of bugs that are otherwise completely silent.override?In C++98, const_iterators were impractical: hard to get from a non-const container, and insert/erase wouldn't accept them. C++11 fixes both problems:
c++98 // C++98: painful typedef std::vector<int>::iterator iter; typedef std::vector<int>::const_iterator cIter; cIter ci = std::find(static_cast<cIter>(v.begin()), static_cast<cIter>(v.end()), 1983); v.insert(static_cast<iter>(ci), 1998); // may not compile!
c++11 // C++11: trivial auto it = std::find(v.cbegin(), v.cend(), 1983); v.insert(it, 1998); // just works
cbegin() and cend() everywhere you don't need to modify elements. In C++14, non-member cbegin and cend are also available for maximum generality.Declaring a function noexcept tells the compiler and callers that the function won't emit exceptions. This isn't just documentation — it enables optimizations:
c++ RetType function(params) noexcept; // most optimizable RetType function(params) throw(); // less optimizable (C++98) RetType function(params); // less optimizable
In a noexcept function, the optimizer doesn't have to keep the runtime stack in an unwindable state, and it doesn't have to guarantee objects are destroyed in reverse construction order if an exception escapes. This alone is reason enough to declare functions noexcept when they won't throw.
std::vector::push_back needs a strong exception guarantee. When it reallocates, it can use move (fast) instead of copy (slow) only if the element's move constructor is noexcept. Without noexcept, the vector falls back to copying to maintain the guarantee. Declaring move operations noexcept can dramatically speed up container operations.c++ // Without noexcept: vector copies elements on reallocation Widget(Widget&& rhs); // move ctor, might throw // With noexcept: vector moves elements on reallocation (fast!) Widget(Widget&& rhs) noexcept; // move ctor, won't throw
swap, move operations, and memory deallocation functions are natural candidates for noexcept. Don't slap noexcept on functions that might throw just for optimization — if a noexcept function throws, std::terminate is called.noexcept speed up std::vector reallocation?constexpr means "this value is known at compile time" for objects, and "this function can be evaluated at compile time" for functions. It is not the same as const:
c++ int sz; // non-constexpr variable constexpr auto arraySize1 = 10; // fine: 10 is compile-time const std::array<int, arraySize1> data1; // fine: arraySize1 is constexpr const auto arraySize2 = sz; // fine: const copy of sz std::array<int, arraySize2> data2; // ERROR: arraySize2 not known at compile time
constexpr, not const.constexpr functions are dual-mode: call them with compile-time arguments and they compute at compile time; call with runtime values and they behave like normal functions:
c++ constexpr int pow(int base, int exp) noexcept { auto result = 1; for (int i = 0; i < exp; ++i) result *= base; return result; } constexpr auto numConds = 5; std::array<int, pow(3, numConds)> results; // 3^5 = 243 at compile time!
In C++11, constexpr functions are limited to a single return statement. C++14 lifts this restriction dramatically, allowing loops, local variables, and multiple statements.
constexpr function is called with non-compile-time arguments?A const member function promises not to modify the object. Multiple threads can call it simultaneously without synchronization — at least, that's the expectation. But if the function uses mutable data members for caching, that contract breaks:
c++ class Polynomial { public: using RootsType = std::vector<double>; RootsType roots() const { if (!rootsAreValid) { // read of mutable // ... compute roots ... rootsAreValid = true; // write to mutable } return rootVals; } private: mutable bool rootsAreValid{ false }; mutable RootsType rootVals{}; };
If two threads call roots() simultaneously, they race on rootsAreValid and rootVals. This is undefined behavior even though roots() is const.
std::mutex (for expensive computations) or std::atomic (for single variables like counters) to make const member functions with mutable data truly thread-safe.c++ class Polynomial { public: RootsType roots() const { std::lock_guard<std::mutex> g(m); // lock if (!rootsAreValid) { // ... compute roots ... rootsAreValid = true; } return rootVals; } // unlock private: mutable std::mutex m; mutable bool rootsAreValid{ false }; mutable RootsType rootVals{}; };
const member function with mutable data not thread-safe by default?C++98 had four special member functions the compiler could generate: default constructor, destructor, copy constructor, and copy assignment operator. C++11 adds two more: the move constructor and move assignment operator.
But the generation rules are subtle. The two copy operations are independent: declaring one doesn't suppress the other. The two move operations are not independent: declaring either suppresses the other.
Move operations are generated only when all three conditions are true:
c++11 class Widget { public: ~Widget(); // custom dtor Widget(const Widget&) = default; // force generation Widget& operator=(const Widget&) = default; // force generation Widget(Widget&&) = default; // force generation Widget& operator=(Widget&&) = default; // force generation };
Which declarations suppress which auto-generated functions?