Skip to content

Instantly share code, notes, and snippets.

Last active September 21, 2022 03:50
What would you like to do?
New C++17 features

Supplement to this video. Watch first 35 min (the language bits), then 1:14:30 - 1:18:15 (the new map features).

You can find a fairly comprehensive list of new features on cppreference or here if you really want to see it all.

Recap of features described in the video

Structured bindings

auto [promise, future] = makePromiseFuture<int>();
for (auto&& [key, val] : someMap) { /* ... */ }

Automatically works with all-public structs, arrays, std::pair and std::tuple. Other types may be supported if they supply a tuple-like protocol (see docs for details).

if and switch with initializers

if (auto status = doThing(); !status.isOK())
    return status;

if (auto num = parsNumberFromString<int>(str); !num.isOK()) {
    return num.getStatus();
} else {
    return doThing(num.getValue());

switch (auto res = inputDocSource->getNext(); res.getStatus()) {
case kEOF:
case kPauseExecution:
    return res;

case kAdvanced:
    return process(res.releaseDocument());

constexpr if

template <typename RetType, typename F, typename... Args>
RetType call(F&& func, Args... args) {
    if constexpr (std::is_same<RetType, void>()) {
    } else {
        return func(args...);

Yes, the feature is called "constexpr if" but spelled if constexpr...

Fold expressions

template <typename Nums>
auto sum(Nums... nums) {
    return (0 + ... + nums);

You will generally want to use "left folds" which are the forms which put the ... before the pack to be expanded.

Class Template Argument Deduction (CTAD)

auto ints = std::vector{1,2,3};
auto lk = std::unique_lock(someMutex);
// used to need std::unique_lock<std::mutex>(someMutex)

std::unique_ptr<SomeType> factory();
auto shared = std::shared_ptr(factory());

// Deduction guides let types customize the behavior:
template <typename T>
MyContainer(const std::vector<T>&) -> MyContainer<T>;
template <typename It>
MyContainer(It begin, It end) -> MyContainer<typename std::iterator_traits<It>::value_type>;

auto in non-type template parameters

template <auto CodeOrCategory>
using ExceptionFor = /* ... */;

try {
} catch(ExceptionFor<ErrorCodes::HostNotFound>& ex) {
} catch(ExceptionFor<ErrorCategory::NotMasterError>& ex) {
    // Used to need ExceptionForCat<...> here.

inline variables

class Classy {
    inline static int count = 0;
    // Used to need to define separately in a cpp file.

    // static constexpr members are implicitly inline.
    static constexpr int kNum = 42;
    static constexpr StringData kName = "Bob"_sd;

constexpr lambdas

constexpr auto example = [] () constexpr {};

// constexpr on lambdas' call operators is automatic if it is legal.
constexpr auto frobnicate = [] (int i) { return i * 42; };
constexpr auto res = frobnicate(21);

Unary static_assert

// used to require a second string argument.
// We use MONGO_STATIC_ASSERT(cond) to get this now.

Guaranteed copy elision

auto lk = std::lock_guard(mutex);

UnmovableType func() {
    return UnmovableType(args);

    // This still isn't legal (no NRVO):
    UnmoveableType out;
    return out;
auto res = func();

Prefer using that style of declaration in C++17 over std::lock_guard lk(mx); as it is more visually distinct from std::lock_guard(mx); and std::lock_guard mx; so you are less likely to accidentally type them, and they will stand out more in reviews.

Nested namespace definitions

namespace mongo::repl {
} // namespace mongo::repl

New APIs for map and unordered_map

// Replaces val if key already present:
auto [it, inserted] = myMap.insert_or_assign(key, val);

// Does nothing (not even construct the value) if key is present:
auto [it, inserted] = myMap.try_emplace(key, args_to_value_constructor...);
map<string, vector<int>>().try_emplace("") // value is empty vector.
map<string, vector<int>>().try_emplace("", 5) // value is vector with 5 zeros.
map<string, vector<int>>().try_emplace("", 5, 1) // value is vector with 5 ones.
map<string, pair<int, string>>().try_emplace("", 5, "one") // value is pair(5, "one").

// Extract node from map:
auto node = map.extract(keyOrIterator);
node.key().mutate(); // key is mutable when outside of any map.

// Steals all nodes from source that don't have equivalent key in map:

Docs: insert_or_assign try_emplace extract merge

Library bits skipped in the video

std::string_view is basically mongo::StringData

optional, variant and any pulled from Boost to std::

boost::filesystem standardized as std::filesystem

Polymorphic Memory Resources (PMR) Allocators

Parallel STL algorithms and execution policies

Useful lib features not covered

C++17 added a lot of lib extensions, but I think only a few will be useful to most of us.

to_chars() and from_chars()

Finally a locale-independent and (hopefully) fast way to convert numbers to and from strings. Explicitly designed for things like JSON parsing/serialization. It has a somewhat complicated, low-level API to avoid requiring allocating or any other potentially expensive operations. We may want to replace the implementation of parseNumberFromString() with a call to from_chars().

invoke() and friends

invoke(f, args...) performs the operation the standard has used the magic INVOKE token for, but never provided a way to use. It calls f passing args..., but in addition to normal functions and function-objects, it also supports pointer-to-members (&type::member) and pointer-to-member-functions (&type::method). For them, it behaves like the implicit this parameter added as an explicit first parameter, and it supports passing both references and pointers to the object.


Variadic lock guard called scoped_lock

Like a mix of lock_guard with lock() to let you acquire multiple locks while avoiding deadlocks. Not movable like unique_lock.

// Two functions with these lines are guaranteed *not* to deadlock:
auto lk = std::scoped_lock(mx1, mx2);
auto lk = std::scoped_lock(mx2, mx1);

// Can also be used just like plain old lock_guard:
auto lk = std::scoped_lock(mx);

Grab bag of small stuff

Language features not covered

In rough order most useful to least.

Improved order of evaluation rules

This is really useful, but hard to demo because it basically just makes code do what we expect when reading it.

Example from the standard:

std::string s = "but I have heard it works even if you don’t believe in it";
s.replace(0, 4, "")
 .replace(s.find("even"), 4, "only")
 .replace(s.find(" don’t"), 6, " ");
assert(s == "I have heard it works only if you believe in it"); // Now OK

This example was included in Bjarne Stroustrup's book The C++ Programming Language, 4th Edition, which was reviewed by several experts but nobody noticed this bug. It actually produces different output on different compilers.

These are the additional rules. All examples use letter order to show evaluation order. Anything indeterminately sequenced will use the same letters.

  • Using an overloaded operator keeps the same order as builtins (A, B even if the comma operator is overloaded, but still don't do that!)
  • Function objects evaluated before arguments ((A(B))(C))
  • Function arguments are still evaluated in arbitrary order, but now at least each argument is fully evaluated before moving on to the next (A(C(B, B), D) or A(D(C, C), B), but not A(D(B, B), C)
    • This means that f(unique_ptr<int>(new int()), may_throw()) is no longer a potential leak. Item 17 from Effective C++ can now be forgotten!
  • Assignment evaluates RHS before LHS (B = A and B += A)
    • This prevents leaving an unwanted element in code like my_map[key] = may_throw()
  • Object before member access, including methods and pointer-to-member usages (A.B(C), A->B(C), (A.*B)(C), (A->*B)(C))
    • This makes chaining/fluent APIs work as intended
  • Container before subscripts (A[B])
  • Shifts (AKA stream-to and stream-from) are left to right (A << B << C and A >> B >> C and A >> B << C)

You can find the full list of rules (old and new) here.

Lambda capture of *this by value

[this] { method()}; // Captures the object by pointer/reference
[self = *this] { self.method(); } // C++14 capture by copy
[*this] { method(); } // C++17 capture by copy
[self = std::move(*this)] { self.method(); } // Still only way to capture by move

Types with public non-virtual bases can be aggregates

It behaves as-if the bases were initial members, recursively. This lets you avoid writing constructors for simple types.

struct Person { std::string name; };
struct Employee : Person { int id = 0; };

auto person = Person{"John Doe"}; // Valid in C++14
auto employee = Employee{"Bob Smith", 1234}; // Now also valid

New attributes

  • [[nodiscard]] - On functions, issues a warning if the return value is "unused". On types, acts as if all functions that return the type were marked [[nodiscard]]
    • Standardization of non-std attribute we already use on Status, StatusWith, and Future
    • The standard is intentionally vague about what "unused" means to allow compilers freedom to choose their own hueristics, and most seem to do a good job
    • Casting-to-void is defined to suppress the warning ((void)dont_ignore_me();)
    [[nodiscard]] bool returnsFalseOnFailure();
    struct [[nodiscard]] Status { /*...*/ };
    Status doThing();
    void func() {
        returnsFalseOnFailure(); // Warning!
        doThing(); // Warning!
        (void)doThing(); // no warning, result intentionally ignored.
  • [[fallthrough]] - Suppress warnings about implicit fallthrough in switch statements
    switch(var) {
    case A:
      [[fallthrough]]; // no warning
    case B:
      // forgot break; - warning on recent compilers
    case D:
    case C: // No warning if no code between cases.
  • [[maybe_unused]] - Suppress warnings about unused entities. Most useful for variables only used in some build modes.
    [[maybe_unused]] const int orig_size = obj.size();
    // Add and remove items from obj...
    #ifdef DEBUG
      assert(obj.size() == orig_size);

noexcept now part of the type system

using MayThrow = void();
using NoThrow = void() noexcept;

void mayThrow();
void noThrow() noexcept;

MayThrow* a = mayThrow;
MayThrow* b = noThrow; // implicit conversion to weaker type.

NoThrow* d = noThrow;
NoThrow* c = mayThrow; // no longer compiles!

// Never enforced noexcept, but now won't compile.
// Maybe it can be made to actually work in the future?
std::function<void() noexcept> f;

Removed trigraphs

"Finally??!" != "Finally|"
"Enter date ??/??/??" != R"(Enter date \\??)"

Using-declarations can pull in multiple names

Only practical use seems to be pulling in a method from all base types when using a template pack of bases. This makes it easy to build a type to overload lambdas.

using std::unique_ptr, std::shared_ptr; // meh

struct A { int operator()(int); };
struct B { double operator()(double); };

struct meh : A, B {
    using A::operator(), B::operator(); // still meh

template <typename... T>
struct overloaded : T... {
    using T::operator()...; // now we're cooking!

// class template deduction guide:
template <typename... T> overloaded(T...) -> overloaded<T...>;

overloaded<A, B> meh_again; // Overloaded function object
meh_again(1); // returns int
meh_again(1.0); // returns double

auto oh_yeah = overloaded{
    [](int) {},
    [](double) {},

Avoid repeating namespaces in attributes

[[using gnu: cold, noinline]] inline cold_code_path() {}

u8 char literals

I really can't think of a reason to use this unless your source files are in EBCDIC. Only justification I can think of is for symmetry with u8-string literals from C++11.

// New in C++17
auto c1 = u8'c'; // Yup, its a char...

// Before
auto c2 = 'c'; // Also a char
auto c3 = u'c'; // char16_t
auto s1 = u8"s"; // Added in C++11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment