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

C++ #10 C++14 New Features

#C++#Programming#ComputerSystems

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&lt;T>, std::is_same_v&lt;A, B> β€” are variable templates (added in C++17), and are a good deal shorter than std::is_integral&lt;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.

  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