Skip to content
C++
Language
since C++14
Basic

C++14 Features

Overview of C++14 language and library additions: generic lambdas, variable templates, relaxed constexpr, return type deduction, and more.

C++14since C++14

C++14 is a minor revision of C++11, published in December 2014, that filled in gaps and relaxed restrictions rather than introducing sweeping new paradigms.

Overview

C++14 arrived three years after C++11 and deliberately kept scope narrow. Where C++11 rewrote the language, C++14 polished it β€” lifting artificial restrictions, adding missing utilities (std::make_unique was simply forgotten in C++11), and making template and lambda machinery more ergonomic. Almost every feature here is something C++11 users wished they had the day they shipped their first modern-C++ codebase.

The standard is split into language changes and library additions. Together they represent a net reduction in boilerplate without any conceptual overhead.


Language Features

Return Type Deduction for Functions

C++11 required a trailing return type when using auto. C++14 removes that requirement: the compiler deduces the return type from the return expression directly.

cpp
// C++11 required
auto divide(double a, double b) -> double { return a / b; }

// C++14: trailing type unnecessary
auto divide(double a, double b) { return a / b; }

// Works for templates too β€” the key practical win
template <typename N, typename D>
auto safe_divide(N num, D den) {
    return num / den;  // deduced at instantiation
}

Multiple return statements are fine as long as they deduce to the same type. Recursive functions require at least one return that does not recurse to appear first.

decltype(auto) is also introduced in C++14. It preserves reference and cv-qualifiers from the return expression, unlike plain auto which strips them:

cpp
int x = 42;
int& ref = x;

auto        f1() { return ref; }  // returns int (copy)
decltype(auto) f2() { return ref; }  // returns int& (reference preserved)

Generic Lambdas

C++14 allows auto in lambda parameter lists, effectively turning the lambda into a function-object template. This was one of the most requested features because it eliminates boilerplate for single-use local algorithms.

cpp
auto add = [](auto a, auto b) { return a + b; };

add(1, 2);       // int
add(1.5, 2.5);   // double
add(std::string{"foo"}, "bar");  // std::string

// Practical: sort by any comparable field
std::vector<std::pair<int,std::string>> v = {{3,"c"},{1,"a"},{2,"b"}};
std::sort(v.begin(), v.end(), [](auto& lhs, auto& rhs){
    return lhs.first < rhs.first;
});

Lambda Init-Capture (Generalized Lambda Capture)

C++11 lambdas could capture by value or by reference but could not move-capture or introduce new names. C++14 generalises the capture clause:

cpp
// Move-capture: transfer ownership into the lambda
auto ptr = std::make_unique<Widget>();
auto f = [p = std::move(ptr)]() { p->process(); };

// Rename on capture
int total = 0;
auto accumulate = [sum = total](int x) mutable { sum += x; return sum; };

// Compute a value at capture time
auto offset_add = [base = compute_base()](int x) { return base + x; };

This makes lambdas fully viable for ownership-transferring callbacks (e.g. posting work to a thread pool).

Variable Templates

C++14 lets you parameterise a variable declaration on a template parameter. The primary use case before C++17 _v helpers was mathematical constants and type-trait shorthands.

cpp
template <typename T>
constexpr T pi = T(3.141592653589793238462643383L);

// Use at the right precision automatically
float  area_f = pi<float>  * r * r;
double area_d = pi<double> * r * r;

// Library-style: expose a trait value without ::value noise
template <typename T>
constexpr bool is_integral_v = std::is_integral<T>::value;  // C++14 pattern
// (C++17 standardised this as std::is_integral_v<T>)

Relaxed constexpr Functions

C++11 constexpr functions were limited to a single return expression β€” no local variables, no loops, no if statements. C++14 removes almost all of those restrictions.

cpp
// C++11: forced into recursive ternary gymnastics
constexpr int factorial_11(int n) {
    return n <= 1 ? 1 : n * factorial_11(n - 1);
}

// C++14: just write normal code
constexpr int factorial_14(int n) {
    int result = 1;
    for (int i = 2; i <= n; ++i)
        result *= i;
    return result;
}

static_assert(factorial_14(10) == 3628800, "");

Mutable local variables, loops, and conditionals are all permitted. virtual functions and try/catch remain disallowed until C++20.

Binary Literals and Digit Separators

cpp
// Binary literals (C++14)
constexpr uint8_t mask = 0b1010'1100;  // 0xAC

// Digit separators: single quote as visual grouping
constexpr long population = 8'100'000'000L;
constexpr double avogadro = 6.022'140'76e23;
constexpr uint32_t ipv4    = 0xC0'A8'00'01;  // 192.168.0.1

The separator has no semantic effect; it exists purely for readability.

Aggregate Initialization with Default Member Initializers

C++11 allowed default non-static member initializers but accidentally excluded aggregates from direct-list-initialization when those defaults were present. C++14 fixes that:

cpp
struct Config {
    int timeout = 30;
    bool verbose = false;
    std::string host = "localhost";
};

Config c1{};              // all defaults
Config c2{60};            // timeout=60, rest default
Config c3{10, true};      // timeout=10, verbose=true, host="localhost"

Library Features

std::make_unique

The most embarrassing omission from C++11 was std::make_unique. C++14 adds it, completing the smart-pointer factory idiom.

cpp
// Before C++14: raw new visible, exception-unsafe in some call sites
std::unique_ptr<Widget> w(new Widget(arg1, arg2));

// C++14
auto w = std::make_unique<Widget>(arg1, arg2);
auto arr = std::make_unique<int[]>(1024);  // array form

Prefer make_unique over new: it is exception-safe, avoids repeating the type, and clearly expresses intent.

Type Trait Aliases (_t suffixes)

C++14 adds using alias templates for every type-transforming trait, eliminating ::type noise:

cpp
// C++11
typename std::remove_reference<T>::type
typename std::add_const<T>::type

// C++14
std::remove_reference_t<T>
std::add_const_t<T>
std::enable_if_t<condition, T>  // especially valuable in SFINAE

_v variable templates for value traits (std::is_integral_v<T>) were not standardised until C++17, though you can define your own with the variable template syntax described above.

Heterogeneous Lookup and Transparent Functors

std::less<void> (and other comparison functors with void specialisation) performs transparent comparison: no implicit conversion to the key type. Associative containers accept a comparator type as a third template argument, enabling find/lower_bound without constructing a key object.

cpp
std::set<std::string, std::less<>> names = {"alice", "bob", "carol"};

// C++11: constructs a std::string from the literal
names.find(std::string{"alice"});

// C++14: compares directly, no allocation
names.find("alice");   // const char* compared heterogeneously

std::integer_sequence / std::index_sequence

Utility for template metaprogramming with parameter packs and tuple manipulation:

cpp
template <typename Tuple, std::size_t... Is>
void print_tuple_impl(const Tuple& t, std::index_sequence<Is...>) {
    ((std::cout << std::get<Is>(t) << ' '), ...);  // fold, C++17 syntax
}

template <typename... Ts>
void print_tuple(const std::tuple<Ts...>& t) {
    print_tuple_impl(t, std::index_sequence_for<Ts...>{});
}

std::exchange

Atomically replaces a value and returns the old one β€” useful for move constructors and state machines:

cpp
// Manual swap pattern
T old = std::move(obj.resource);
obj.resource = nullptr;

// With std::exchange
T old = std::exchange(obj.resource, nullptr);

// Move constructor pattern
MyType(MyType&& other) noexcept
    : data_(std::exchange(other.data_, nullptr))
    , size_(std::exchange(other.size_, 0)) {}

User-Defined Literals for Standard Types

cpp
using namespace std::literals;

auto s  = "hello"s;    // std::string
auto ms = 250ms;       // std::chrono::milliseconds
auto s2 = 1s;          // std::chrono::seconds
auto z  = 3.0 + 4.0i;  // std::complex<double>

Common Pitfalls

Return type deduction and decltype(auto) confusion. Use plain auto when you want a value return; use decltype(auto) only when you need to propagate reference or cv-qualifiers β€” typically in generic forwarding wrappers.

Init-capture copies, not moves, by default. [x = expr] copies expr into x. To move: [x = std::move(expr)]. Forgetting std::move with move-only types causes a compile error, but forgetting it with copyable types silently copies.

Relaxed constexpr does not imply consteval. A C++14 constexpr function can still be called at runtime. If an argument is not a constant expression, evaluation happens at runtime with no error. Use static_assert to verify compile-time evaluation when correctness depends on it.

_t aliases require <type_traits>. Unlike the ::type members which have always been there, the alias templates are new in C++14 and require an up-to-date include and -std=c++14 or later.


Migrating from C++11 to C++14

Upgrading is low-risk because C++14 is a strict superset of C++11 at the language level. No previously valid C++11 code is broken by the new standard; the migration is purely additive.

Concretely, swap in these improvements as you touch existing code:

cpp
// C++11 β†’ C++14 quick wins

// 1. Drop trailing return types where auto deduction suffices
auto compute(int x) -> int { return x * 2; }   // C++11
auto compute(int x)         { return x * 2; }   // C++14

// 2. Add std::make_unique everywhere unique_ptr is constructed with new
std::unique_ptr<Foo> p(new Foo(1, 2));           // C++11
auto p = std::make_unique<Foo>(1, 2);            // C++14

// 3. Replace verbose trait spellings
typename std::enable_if<std::is_integral<T>::value>::type  // C++11
std::enable_if_t<std::is_integral<T>::value>               // C++14

// 4. Move-capture in lambdas
auto cb = [w = std::move(widget)]() { w->render(); };  // C++14 only

The only flag you need is -std=c++14 (GCC/Clang) or /std:c++14 (MSVC 2015+). Standard library headers are unchanged β€” the additions are purely additive symbols.


Generic Lambdas in Practice

Generic lambdas are syntactic sugar for local function-object templates, and they compose cleanly with STL algorithms. Each auto parameter becomes a deduced template parameter on the call operator.

cpp
// C++14 β€” works with any container whose elements support operator<
auto min_element_val = [](const auto& range) {
    return *std::min_element(range.begin(), range.end());
};

std::vector<int>    ints{3, 1, 4, 1, 5};
std::vector<double> dbls{2.7, 1.4, 3.1};
int    a = min_element_val(ints);  // int
double b = min_element_val(dbls);  // double

Generic lambdas work with std::bind replacements and higher-order combinators:

cpp
// C++14 β€” compose two unary operations
auto compose = [](auto f, auto g) {
    return [f, g](auto x) { return f(g(x)); };
};

auto square  = [](auto x) { return x * x; };
auto negate  = [](auto x) { return -x;    };
auto neg_sq  = compose(negate, square);

static_assert(neg_sq(3) == -9, "");  // resolved at compile time

Caveat: each distinct auto parameter is independently deduced; there is no way to constrain two auto parameters to the same type within the lambda syntax itself. For that, either use a real template or wait for C++20 concepts.


Variable Templates: Practical Examples

Variable templates close the gap between constexpr values and type-parameterised constants β€” something that previously required a full struct with a static constexpr member.

cpp
// C++14 β€” mathematical constants at any floating-point precision
template <typename T> constexpr T e    = T(2.718281828459045235360287L);
template <typename T> constexpr T pi   = T(3.141592653589793238462643L);
template <typename T> constexpr T ln2  = T(0.693147180559945309417232L);

double circumference = 2.0 * pi<double> * radius;
float  energy_f      = mass * pi<float>;

The pattern is also useful for policy values and tag dispatching:

cpp
// C++14 β€” per-type buffer sizes
template <typename T>
constexpr std::size_t cache_line_count = 64 / sizeof(T);

// std::array<float, cache_line_count<float>> fits exactly in one cache line
std::array<float,  cache_line_count<float>>  row_f;   // 16 floats
std::array<double, cache_line_count<double>> row_d;   //  8 doubles

Variable templates can also be partially specialised, giving you behaviour similar to a type trait without the ::value indirection:

cpp
template <typename T>        constexpr bool is_pointer_v = false;
template <typename T>        constexpr bool is_pointer_v<T*> = true;
template <typename T>        constexpr bool is_pointer_v<T* const> = true;

Deprecated Features in C++14

C++14 formally deprecated a handful of C++11 and older constructs, signalling that they would eventually be removed. Compilers began emitting warnings; C++17 removed some of them outright.

FeatureStatus in C++14Removed in
std::auto_ptrDeprecatedC++17
std::random_shuffle (no engine)DeprecatedC++17
std::bind1st / std::bind2ndDeprecatedC++17
std::ptr_fun / std::mem_funDeprecatedC++17
register keyword (storage class)DeprecatedC++17
TrigraphsDeprecatedC++17
cpp
// C++14 β€” deprecated patterns and their modern replacements

// std::auto_ptr  β†’  std::unique_ptr
std::auto_ptr<Foo>  old(new Foo);   // deprecated C++14, removed C++17
auto modern = std::make_unique<Foo>();  // correct replacement

// std::random_shuffle  β†’  std::shuffle with explicit engine
std::random_shuffle(v.begin(), v.end());  // deprecated
std::mt19937 rng{std::random_device{}()};
std::shuffle(v.begin(), v.end(), rng);    // correct replacement

// std::bind1st  β†’  lambda
auto gt5 = std::bind1st(std::less<int>{}, 5);  // deprecated
auto gt5_l = [](int x){ return 5 < x; };       // correct replacement

Audit any C++11 codebase for these before targeting C++17 β€” upgrading a compiler to -std=c++17 can turn latent deprecation warnings into hard errors.


C++14 in Embedded and Constrained Environments

C++14 is broadly usable on bare-metal and RTOS targets because all five major language features (return type deduction, generic lambdas, variable templates, relaxed constexpr, binary literals) are zero-overhead abstractions resolved entirely at compile time.

Embedded-friendly C++14 patterns:

cpp
// C++14 β€” compile-time lookup table with relaxed constexpr
constexpr std::array<uint8_t, 16> crc4_table() {
    std::array<uint8_t, 16> t{};
    for (uint8_t i = 0; i < 16; ++i) {
        uint8_t crc = i;
        for (int bit = 0; bit < 4; ++bit)
            crc = (crc & 0x8) ? (crc << 1) ^ 0x13 : crc << 1;
        t[i] = crc & 0xF;
    }
    return t;
}
constexpr auto CRC4_TABLE = crc4_table();  // zero runtime cost
cpp
// C++14 β€” bit-field register map with binary literal clarity
struct UartCR1 {
    uint32_t UE   : 1;   // UART enable
    uint32_t RE   : 1;   // receiver enable
    uint32_t TE   : 1;   // transmitter enable
    uint32_t      : 29;
};
constexpr uint32_t BAUD_115200_APB1_45MHz = 0b0000'0001'1110'1110;  // C++14

Caveats for embedded targets:

  • Generic lambdas and variable templates generate additional template instantiations; verify that code-size impact is acceptable on flash-constrained devices.
  • std::make_unique pulls in <memory> and heap allocation β€” skip it on targets without a free store.
  • Relaxed constexpr with loops is safe at compile time; the restriction is only that virtual and exceptions remain disallowed until C++20.
  • GCC ARM Embedded 5.x+ and Clang 3.4+ both support -std=c++14 fully; IAR 8.x and Keil MDK 5.x added support by 2016.

See Also

  • reference/language/cpp11-features β€” the major revision that preceded C++14
  • reference/language/cpp17-features β€” standardises _v trait variables, if constexpr, structured bindings
  • reference/language/cpp20-features β€” concepts, ranges, coroutines, consteval
  • reference/language/constant-expressions β€” full constexpr evolution across standards