13. C++17 features tour¶
Time: ~2 hours Prerequisites: 06 to 11
1. In plain words¶
C++11 was the big change (move semantics, lambdas, smart pointers, auto, threads). C++14 polished
it. C++17 is a collection of medium-sized conveniences that make daily code shorter and safer:
unpacking a pair into two names, declaring a variable inside an if, passing strings without copying,
saying "this function's result must not be ignored", deducing template arguments from constructor
calls. Plus a few new library types (optional, variant, any, string_view, filesystem) and
std::shared_mutex, the reader-writer lock your tcpkv store uses.
This module is the checklist: each feature, what it replaces, the one trap. You have met several already; here they are collected so you can say "C++17" on a resume and mean every item.
2. The real thing¶
2.1 Structured bindings¶
auto [it, inserted] = map.insert({key, value}); // replaces std::pair<iterator,bool> r = ...; r.first; r.second
for (const auto& [name, age] : ages) ... // unpack map entries
auto [count, mean] = compute(v); // any struct with public members, std::tuple, std::array, C array
The names are aliases into a hidden copy (or reference, with auto&/const auto&) of the whole
object. You cannot skip a field or nest them; unused names do not warn.
2.2 if / switch with initialiser¶
if (auto it = m.find(k); it != m.end()) use(*it); else miss(); // 'it' scoped to the if/else
if (std::lock_guard lock(mu); !queue.empty()) pop(); // lock scoped to the statement
switch (auto op = next_op(); op) { ... }
Replaces a variable declared on the line before whose scope leaks to the end of the block.
2.3 std::string_view¶
A (const char*, size_t) pair. Take it as a parameter wherever you used to take const std::string&
and only read: callers with literals, char*, std::string, or substrings pay nothing.
remove_prefix, substr (O(1), no copy), find, comparison operators all work.
Traps: not null-terminated (do not pass .data() to C APIs expecting a C string); does not own
(never store a view to a temporary or to a buffer that will be reused); std::string s = sv; is an
explicit copy by design.
2.4 std::optional, std::variant, std::any¶
Covered in module 10. optional<T>: maybe a T, inline. variant<Ts...>: exactly one of several
types, inline, std::visit. any: any copyable type, type-erased, heap for large payloads,
any_cast checks at run time. Use variant when the set of types is known; any is for genuinely
open-ended plugin data and is rare in good code.
2.5 std::filesystem¶
Portable paths and file operations: fs::path with / joining, exists, file_size,
create_directories, remove_all, directory_iterator, recursive_directory_iterator,
temp_directory_path, rename, last_write_time. Every operation has a throwing overload and an
std::error_code& overload. GCC 9+ needs no extra library; GCC 8 needed -lstdc++fs.
2.6 Parallel algorithms¶
std::sort(std::execution::par, b, e), also par_unseq, seq, for most algorithms plus
std::reduce, std::transform_reduce, std::exclusive_scan. On GCC the implementation is Intel TBB:
#include <execution> and link with -ltbb (apt install libtbb-dev). Your callable must be
thread-safe and must not throw (an exception in a parallel algorithm calls std::terminate).
Speed-ups appear for large inputs with real work per element; a 10k-element sort is faster serial.
2.7 Fold expressions and variadic improvements¶
(args + ...), (std::cout << ... << args), ((xs > 0) && ...) (module 08). Also
if constexpr, and auto as a non-type template parameter.
2.8 Inline variables¶
inline constexpr int kVersion = 3; in a header: one object across all translation units
(module 11). static constexpr members no longer need an out-of-class definition.
2.9 Attributes: [[nodiscard]], [[maybe_unused]], [[fallthrough]]¶
[[nodiscard]]on a function (or a type): discarding the return value is a warning, an error with-Werror. Put it on anything returning an error code, aboolsuccess flag, a handle, or a value whose computation is the only effect (empty(),size()).[[maybe_unused]]silences unused-variable/parameter warnings for things used only under#ifdefor inassert.[[fallthrough]];documents an intentionalcasefall-through so-Wimplicit-fallthroughstays quiet.
2.10 Class template argument deduction (CTAD)¶
std::pair p{1, 2.5};, std::vector v{1, 2, 3};, std::lock_guard lock(mu);,
std::shared_lock lock(rw_mutex);, std::tuple t{1, 'c', "s"};. Deduction follows constructor
parameters or explicit deduction guides (module 10's overloaded uses one). Trap:
std::vector v{other_vector}; deduces vector<int> (copy), not vector<vector<int>>.
2.11 std::byte¶
An enum class over unsigned char with only bitwise operators: raw memory that cannot be
accidentally added or printed as a character. std::to_integer<int>(b) converts explicitly.
Use it for buffers and arena storage (std::byte buf[4096]); it aliases anything like char.
2.12 std::shared_mutex and std::scoped_lock¶
std::shared_mutex (C++17; shared_timed_mutex was C++14): many threads may hold it in shared
mode (std::shared_lock), or one in exclusive mode (std::unique_lock). Right for read-mostly
data (configuration, a key-value cache) where readers outnumber writers heavily. std::scoped_lock
locks several mutexes at once in a deadlock-free order. Both are covered in depth in
04_Concurrency/03_mutexes_lock_guard_unique_lock.md; here you get the shape of the tcpkv store.
2.13 Smaller items¶
Guaranteed copy elision (module 04), std::size/std::data/std::empty free functions,
std::clamp, std::gcd/std::lcm, std::apply, std::invoke, std::not_fn, std::as_const,
std::uncaught_exceptions, __has_include, hexadecimal floating literals, u8 character literals,
static_assert without a message, nested namespaces namespace a::b::c {}, std::map::try_emplace
and insert_or_assign, extract/merge of map nodes, std::sample, std::launder,
std::aligned_alloc, std::hardware_destructive_interference_size, removal of auto_ptr,
random_shuffle, trigraphs, register and dynamic exception specifications.
3. Code¶
code/m13_cpp17_tour.cpp touches every language-level item:
inline constexpr int kProtocolVersion = 3;
auto [count, mean] = compute({1.0, 2.0, 6.0});
for (const auto& [name, age] : ages) ...
if (auto [it, inserted] = ages.insert({"ann", 99}); !inserted) ...
if (auto it = ages.find("bob"); it != ages.end()) ... else ...
std::string_view trim(std::string_view s) { // O(1) slicing, no allocation
while (!s.empty() && s.front() == ' ') s.remove_prefix(1);
while (!s.empty() && s.back() == ' ') s.remove_suffix(1);
return s;
}
std::pair p{1, 2.5}; std::vector words{std::string("a"), std::string("b")}; std::tuple t{1, 'c', "txt"};
template <class... Ts> constexpr bool all_positive(Ts... xs) { return ((xs > 0) && ...); }
[[nodiscard]] bool try_reserve(std::vector<int>& v, std::size_t n);
[[maybe_unused]] bool ok = try_reserve(buf, 2'000'000); assert(!ok);
std::any a = 42; a = std::string("now a string"); std::any_cast<std::string>(&a);
std::byte b{0x0F}; b <<= 4; std::to_integer<int>(b);
fs::path dir = fs::temp_directory_path() / "m13_demo";
fs::create_directories(dir);
for (const auto& entry : fs::directory_iterator(dir)) entry.path().filename(), entry.file_size();
fs::remove_all(dir);
code/m13_shared_mutex_cache.cpp is the read-mostly store:
class KvStore {
public:
std::optional<std::string> get(const std::string& key) const {
std::shared_lock lock(mu_); // readers run concurrently
auto it = map_.find(key);
return it == map_.end() ? std::nullopt : std::optional<std::string>(it->second); // copy out under the lock
}
void put(const std::string& key, std::string value) {
std::unique_lock lock(mu_); // exclusive
map_[key] = std::move(value);
}
private:
mutable std::shared_mutex mu_; // mutable: const get() may lock
std::unordered_map<std::string, std::string> map_;
};
Four reader threads and one writer run against it; the program prints the hit count and the final
state. Note mutable on the mutex and that get returns a copy, never a reference into the map.
4. Under the hood (senior-level)¶
Structured bindings are not variables. auto [a, b] = f(); creates a hidden object e of the
returned type and makes a and b names for e.first/e.second (or get<0>(e), get<1>(e) for
tuple-like types, or the members in order for aggregates). There is no copy of a and b
individually; decltype(a) is the member's declared type, even though a behaves like a reference.
With auto& the hidden object is a reference and no copy happens at all.
std::string_view ABI. Two words, passed in registers (rdi, rsi) on x86-64, trivially
copyable; const std::string& is one pointer but requires a std::string object to exist (a literal
argument creates a temporary with a heap allocation if longer than 15 bytes). string_view::substr is
pointer arithmetic; std::string::substr allocates.
std::variant vs union vs inheritance. variant is a union with an index byte and
compiler-generated copy/move/destroy that switch on the index. Storage is max(sizeof(Ts...))
rounded to max(alignof(Ts...)) plus the index. No heap, no vptr.
std::any storage. libstdc++ stores types that fit in a pointer-sized buffer and are
nothrow-movable inline; everything else is heap-allocated. Each any holds a pointer to a static
manager function per stored type (effectively a hand-rolled vtable), which is how any_cast checks
the type via typeid comparison.
std::shared_mutex on Linux is pthread_rwlock_t (56 bytes, vs 40 for std::mutex). Taking a
shared lock is an atomic increment of a reader count when no writer holds or waits; a writer waits
for readers to drain. glibc's rwlock prefers readers by default (PTHREAD_RWLOCK_PREFER_READER_NP),
which can starve writers under constant read load; the writer-preferring variant exists but
std::shared_mutex does not expose it. Uncontended shared lock/unlock is slower than a plain mutex
(more atomic operations), so a shared mutex only wins when critical sections are long enough or
readers numerous enough that parallel readers pay for the extra cost. Measure. For very short reads a
plain std::mutex or a lock-free snapshot (shared_ptr<const Config> swap) is often faster.
CTAD mechanics. The compiler synthesises a set of function templates from the class's
constructors (and from explicit deduction guides), performs overload resolution on them with the
constructor arguments, and uses the deduced class template arguments. That is why
std::vector v{other}; picks the copy constructor's deduction (vector
[[nodiscard]] codegen. None; it is purely a compile-time diagnostic. The same is true for all
attributes here. [[fallthrough]] suppresses a warning; [[maybe_unused]] suppresses another.
std::filesystem and errors. The throwing overloads throw std::filesystem::filesystem_error
(derived from std::system_error) carrying both paths and the error_code. The error_code&
overloads never throw and are the right choice in servers (fs::remove(path, ec)). Path operations
are string manipulation and never touch the disk; exists, file_size etc. are stat calls.
Parallel algorithms and TBB. libstdc++ delegates to Intel's PSTL on top of TBB's task scheduler
(work-stealing thread pool, one worker per hardware thread). par_unseq additionally allows
vectorisation within a thread, which forbids anything that could deadlock across lanes (no locks in
the callable). Without -ltbb the link fails with undefined tbb:: symbols.
5. In production¶
[[nodiscard]]everywhere a C programmer forgets to check. Onerror_code-returning functions,bool try_*,release(), factory functions returning handles. With-Werrorthe compiler enforces the check that code review used to miss.string_viewparameters,std::stringmembers. Accept views at the API, store strings. A memberstring_viewis a lifetime bug waiting for a reused buffer. In a parser, views into the request buffer are fine within the request's processing; anything that outlives it must copy.- Structured bindings in map loops remove the
.first/.secondnoise that hides bugs (it->second.second). Name the parts. shared_mutexfor config and caches, not for everything. In tcpkv the store is read-mostly, so readers in parallel is a real win; a mostly-written structure under ashared_mutexis slower than under astd::mutex. Profile withperfand look atpthread_rwlock_*time.std::filesystemin servers. Use theerror_codeoverloads; a missing directory is a normal condition, not an exception.fs::renameis atomic within a filesystem (the write-temp-then-rename pattern for crash-safe config updates).- Parallel algorithms are a tool for batch jobs, not for request paths: the TBB pool competes with your own thread pool for cores and adds tail latency. Use them in offline tooling (report generation, migration jobs) where throughput is all that matters.
- 3 AM debugging. A
std::string_viewprinting garbage or crashing inoperator<<: dangling view. gdb shows a_M_strpointer into freed heap. Search for views stored in members or captured by lambdas that outlive the string. ASan reports it asheap-use-after-freewith the allocation and free stacks.
6. Try it¶
- (easy) Build
m13_cpp17_tour.cpp. Remove the[[maybe_unused]]and build with-DNDEBUG. Then remove theif (!try_reserve(...))check. Done when you have seen both warnings become errors and can explain each. - (easy) Rewrite a function of yours taking
const std::string&to takestd::string_view, and call it with a literal, astd::string, and a substring. Count allocations with a countingoperator new. Done when the view version allocates zero times. - (medium) Write a
config_reload(const fs::path&)that writes topath + ".tmp",fsyncs, andfs::renames over the original, usingerror_codeoverloads throughout. Done when killing the process at any point leaves either the old or the new file intact, never a partial one. - (medium) Benchmark
m13_shared_mutex_cache.cppwithstd::shared_mutexvsstd::mutexfor reader:writer ratios 100:1, 10:1, 1:1 and critical sections of 100 ns and 10 us (add a spin). Done when you have a table and can say whereshared_mutexwins on your machine. - (hard) Install
libtbb-devand sort astd::vector<std::uint64_t>of 1M, 10M and 100M elements withstd::sortvsstd::sort(std::execution::par, ...). Done when you have the speed-ups andperf statshows the number of threads used; explain the 1M result. - (hard) Implement
template <class... Ts> class TypeMapusing astd::tuple<std::optional<Ts>...>withget<T>()/set<T>()via fold expressions andif constexpr, and avisit_all(f)that callsfon each present value. Done whenstatic_asserts verify it is constexpr-constructible and a test covers an empty map.
7. Interview questions¶
Q: What are structured bindings and what can you bind?
A declaration auto [a, b] = expr; that names the parts of a pair, tuple, std::array, C array, or
struct with all-public members. The names alias the members of a hidden object (copy, or reference
with auto&); you cannot skip or nest.
Q: std::string_view versus const std::string&: when and why?
Use string_view for read-only string parameters: it is two words passed in registers, accepts
literals and char* without constructing a std::string, and slices in O(1). Keep const std::string&
(or std::string) when you need a null-terminated C string or will store the value. Never store a
view that may outlive its source.
Q: What does [[nodiscard]] do?
Marks a function (or type) whose return value must not be silently discarded; ignoring it produces a
compiler warning, an error with -Werror. It has no runtime effect. Use it on error codes, success
flags, handles and pure computations.
Q: What is CTAD?
Class template argument deduction: the compiler deduces a class template's arguments from the
constructor arguments, so std::pair p{1, 2.5} and std::lock_guard lock(mu) need no angle
brackets. Deduction uses the constructors and any user-written deduction guides.
Q: How does std::shared_mutex differ from std::mutex, and when does it help?
It allows many concurrent shared (read) holders or one exclusive (write) holder. On Linux it is a
pthread_rwlock_t. It helps when reads greatly outnumber writes and critical sections are long enough
to amortise its higher per-lock cost; for very short sections a plain mutex is often faster. Readers
can starve writers with glibc's default policy.
Q: Why does std::vector v{other_vector}; not create a vector of vectors?
CTAD prefers the copy-constructor deduction over the initializer-list constructor when the braced list
holds a single element of the same template. Write std::vector<std::vector<int>> v{other}; or
std::vector v{other, other2}; to get the nested type.
Q: What do you need to use parallel algorithms on GCC?
#include <execution>, an execution policy argument (std::execution::par), and linking against
Intel TBB (-ltbb). The callable must be thread-safe and must not throw.
8. Recap¶
- Structured bindings and
if-initialisers tighten scope and remove.first/.secondnoise. string_viewfor read-only string parameters; never store a view beyond its source's lifetime.optional,variant,any: inline, inline, type-erased; prefervariantoverany.filesystem: use theerror_codeoverloads in servers;renameis atomic.- Parallel algorithms need
-ltbbon GCC and are for batch work. - Fold expressions,
if constexpr, inline variables, CTAD,std::byte. [[nodiscard]]turns forgotten checks into compile errors;[[maybe_unused]],[[fallthrough]]document intent.std::shared_mutex=pthread_rwlock_t; wins only for read-mostly, long-enough critical sections; see 04_Concurrency for the full treatment.- Guaranteed copy elision,
try_emplace,insert_or_assign, nodeextract/merge,std::clamp,std::apply, nested namespaces.