This note covers the new features in C++14. Next to the upheaval of C++11, C++14 is a minor release: it introduces no new paradigm and instead finishes the things C++11 left half-done. Which is also why almost none of it costs anything to learn, and all of it is pleasant to use.
Function return type deduction
In C++11, deducing a return type meant a trailing return type plus decltype:
C++// C++11
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u) { return t + u; }
C++14 simply lets the compiler work it out:
C++// C++14
template <typename T, typename U>
auto add(T t, U u) { return t + u; }
Deduction follows the same rules as auto variables, which means it drops references and top-level const. To keep them, use decltype(auto):
C++std::vector<int> v{1, 2, 3};
auto get1(std::vector<int>& c) { return c[0]; } // returns int, a copy
decltype(auto) get2(std::vector<int>& c) { return c[0]; } // returns int&
The distinction matters as soon as you write generic forwarding wrappers.
One caveat: return type deduction requires the function definition to be visible at the call site, so it cannot be used on a declaration-only interface in a header.
Generic lambdas
A C++11 lambda had to spell out its parameter types. C++14 allows auto, which is equivalent to generating a closure type with a templated operator():
C++auto print = [](const auto& x) { std::cout << x << '\n'; };
print(42);
print("hello");
print(3.14);
This is what makes lambdas usable for writing generic algorithms. The benefit is most obvious with the standard library:
C++std::sort(v.begin(), v.end(),
[](const auto& a, const auto& b) { return a.score > b.score; });
Previously you had to spell out the full element type here, and change it every time the container changed.
Lambda init-capture
A C++11 capture list could only capture existing variables, by value or by reference. That meant move-only types could not be captured at all β a std::unique_ptr could not go into a lambda.
C++14 lets you initialize a new variable directly in the capture list:
C++auto ptr = std::make_unique<Widget>();
auto task = [p = std::move(ptr)]() { p->run(); }; // ownership moves into the closure
It also works for capturing the result of an expression, so you do not recompute it inside the closure:
C++auto f = [size = v.size() * 2]() { return size; };
std::make_unique
C++11 shipped make_shared and forgot make_unique. C++14 fixed that.
C++auto p = std::make_unique<Widget>(arg1, arg2);
Beyond writing the type name once instead of twice, it closes a real safety hole. Consider:
C++process(std::unique_ptr<Widget>(new Widget), compute());
Before C++17, the evaluation order of function arguments was unspecified. A compiler was free to run new Widget, then call compute(), and only then construct the unique_ptr. If compute() throws, that raw Widget leaks forever. make_unique puts the allocation and the ownership transfer inside a single call, so the window does not exist.
The rule: do not write raw new. With make_unique and make_shared there is almost never a reason to.
Variable templates
C++11 had function templates and class templates; C++14 added variable templates:
C++template <typename T>
constexpr T pi = T(3.1415926535897932385);
float f = pi<float>;
double d = pi<double>;
The _v-suffixed type traits in the standard library β std::is_integral_v<T>, std::is_same_v<A, B> β are variable templates (added in C++17), and are a good deal shorter than std::is_integral<T>::value.
Relaxed constexpr
A C++11 constexpr function was essentially limited to a single return statement, so anything with a loop had to be written recursively:
C++// C++11: recursion only
constexpr int factorial(int n) { return n <= 1 ? 1 : n * factorial(n - 1); }
C++14 allows local variables, loops and branches inside a constexpr function:
C++// C++14
constexpr int factorial(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) result *= i;
return result;
}
This is what turned compile-time computation from a party trick into a practical tool. Lookup tables, bit masks and string hashes can all be computed while compiling.
Smaller changes
Binary literals and digit separators.
C++int mask = 0b1010'1010;
int large = 1'000'000;
The separator ' may go anywhere; it affects readability only, never the value. Useful for bit masks and large constants.
The [[deprecated]] attribute. Marks an interface on its way out; using it produces a compiler warning.
C++[[deprecated("use renderV2() instead")]]
void render();
std::exchange. Writes a new value and returns the old one, which reads nicely in move constructors:
C++Buffer(Buffer&& o) noexcept
: data_(std::exchange(o.data_, nullptr)),
size_(std::exchange(o.size_, 0)) {}
std::shared_timed_mutex. A reader/writer lock in the standard library, allowing many readers or one writer.
Wrapping up
No single C++14 feature is worth stopping to study, but together they make C++11 code visibly shorter: generic lambdas make algorithms generic, init-capture lets ownership enter a closure, and relaxed constexpr makes compile-time computation writable. If your project is still on C++11, moving to C++14 costs essentially nothing in migration and is the best-value upgrade available.