Part III — The Standard Library¶
Chapter 10 — Modern C++ Fluency¶
C++ has had a major update every 3 years since 2011 (C++11/14/17/20/23/26). These features, used casually, are the difference between current C++ and 2008-era C++.
auto — type inference (C#'s var)¶
auto count = 42; // int
auto it = widgets.begin(); // saves the long iterator type
auto& w = widgets[0]; // auto alone COPIES - add & to alias
const auto& name = GetName(); // the read-only idiom
Trap: auto strips references: auto w = widgets[0] is a copy. Muscle memory: const auto& for reading, auto& for modifying, plain auto only when you want a copy.
auto has a sibling you will read long before you write it: decltype(expr) is "the type this expression has", answered by the compiler and never at runtime — C# has no counterpart, since typeof hands you a runtime object and var is only the auto half. Two spellings cover almost every sighting. using SetCallback = decltype(&Device_SetCallback); names a vendor function-pointer type without retyping its signature, so it cannot drift from the header; and decltype(auto) in library forwarding code means "whatever this returns, reference and all", which you may skip over. Chapter 38's queue uses the library form, std::invoke_result_t<F>, which is decltype of a call with the plumbing hidden.
Lambdas — capture is explicit (no GC to keep captures alive)¶
int threshold = 10;
auto f1 = [threshold](const Widget& w) { return w.size > threshold; }; // COPY
auto f2 = [&threshold](const Widget& w){ return w.size > threshold; }; // REF
auto f3 = [=](...) { ... }; // everything used, by copy
auto f4 = [&](...) { ... }; // everything used, by reference
auto f5 = [this](...) { ... }; // capture enclosing object's this
auto MakeGetter() {
int local = 5;
return [&local] { return local; }; // BUG: dangling reference!
// fix: [local] - copy it
}
Key principle: "Capture by reference only when the lambda won't outlive the scope; by copy (or move) when it escapes — stored, returned, or run async."
Algorithms + lambdas (C++'s LINQ, roughly)¶
auto it = std::find_if(v.begin(), v.end(),
[](const Widget& w) { return w.selected; });
std::sort(v.begin(), v.end(),
[](const Widget& a, const Widget& b) { return a.size < b.size; });
// C++20 ranges - even closer to LINQ (Where + Select, lazy):
auto big = v | std::views::filter([](auto& w){ return w.size > 10; })
| std::views::transform([](auto& w){ return w.name; });
std::optional\<T> — "maybe a value" (C#'s T?)¶
std::optional<Widget> FindByName(const std::string& name);
if (auto w = FindByName("wall"); w.has_value()) {
Use(*w); // or w->name
}
auto w2 = FindByName("x").value_or(Widget{}); // ?? equivalent
Key principle: "A function that can fail to produce a value returns optional\<T>, not a null pointer or a magic value like -1."
Three things optional is not, each a place the T? reflex misfires. It is not a nullable reference: std::optional<T&> does not exist before C++26 and the compiler refuses it outright, so Widget? w stays a const Widget* (or an optional<std::reference_wrapper<Widget>>, if you must). It is not ?.: C++17 has no null-propagating call, and w->Name on an empty optional is not a null-reference exception but Chapter 3's quiet undefined behavior — the value reads as garbage, the program carries on, and this book's sanitizer flags say nothing about it (a hardened standard library, Chapter 13, is what traps it); check first, or call value(), which throws bad_optional_access and is the one spelling that fails loudly. And optional<bool> is not a three-state flag, it is three-state nothing — write the enum class. C++23 adds and_then and transform, the ?. chain; until your toolchain has them, Recipe 19 in Appendix F is the by-hand form.
std::variant — the tagged union with the compiler on your side¶
C# has no closed sum type. When a value is "one of these three things" you write a small class hierarchy and pattern-match on it — switch (e) { case Fault f: ... } — and the runtime carries the real type for you. C++ has that too (Chapter 5), and it also has the older spelling you will read in every vendor event struct: a kind field next to a union, with nothing checking that the tag and the payload agree. std::variant is that union with the tag enforced.
struct Temperature { int centi; };
struct Fault { int code; };
struct Heartbeat {};
using Event = std::variant<Temperature, Fault, Heartbeat>; // exactly one of these
Event e = Fault{7};
if (auto* f = std::get_if<Fault>(&e)) Alarm(f->code); // the 'as' test: nullptr if not
std::holds_alternative<Fault>(e); // the 'is' test
std::get<Temperature>(e); // throws bad_variant_access - never garbage
std::visit(overloaded{ // the switch: one lambda per alternative
[](const Temperature& t) { Plot(t.centi); },
[](const Fault& f) { Alarm(f.code); },
[](Heartbeat) { Tick(); },
}, e);
overloaded is a two-line idiom that turns a handful of lambdas into one callable; C++17 does not ship it, every codebase has one, and Recipe 20 in Appendix F spells it out. std::monostate is the "not yet set" alternative for a variant that must be default-constructible. And the property the C union never had: leave one alternative out of the visit and the program does not compile — libc++ says so in as many words, std::visit requires the visitor to be exhaustive. A switch on a kind field with a missing case compiles and falls through.
That fixed list is the whole trade: a variant's alternatives are closed at the point it is spelled, where a virtual hierarchy (Chapter 5) stays open to whoever subclasses it later. Which of the two a given set of types wants is Appendix H's procedure 4, which routes here for the mechanism and adds only the choice. std::any exists too, as the object box; it is almost never what you want, and reaching for it usually means the set was closed and nobody wrote it down.
Key principle: "A closed set of alternatives is a std::variant by value, visited exhaustively; an open set someone else extends is a virtual base behind unique_ptr."
std::string_view — non-owning view of a string¶
A pointer + length, like C#'s ReadOnlySpan<char>. Replaces const std::string& for read-only string parameters. Danger: non-owning means it can dangle — never store a string_view to a temporary.
Try it (30 seconds). Return a string_view of a local std::string from a function and read it at the call site under ASan. clang already objects at compile time (-Wreturn-stack-address), and the run is a textbook heap-use-after-free — Chapter 31 teaches you to read that report; here it is enough to watch the trap fire.
And string_view is the string-shaped case of an idea your C# already names in general: Span<T>/ReadOnlySpan<T> over any contiguous buffer is std::span<T> — C++20, so this book's C++17 exercises spell the same thing as the pointer-plus-length pair you will meet in every C API of Chapter 16. When your codebase has span, use it; until then you are writing span by hand and should feel no shame. When a view is the right parameter shape at all — against const&, a sink, or a plain pointer — is Appendix H's parameter procedure, which routes here for the mechanism and adds only the choice.
Structured bindings (C# 7 deconstruction)¶
auto [it, inserted] = myMap.insert({key, value});
for (const auto& [name, widget] : widgetMap) { // KeyValuePair unpacked
std::cout << name;
}
constexpr — computation at compile time¶
constexpr int Square(int x) { return x * x; }
constexpr int area = Square(12); // computed by the COMPILER
std::array<int, Square(4)> buffer; // usable where constants are required
What does static_assert do, and how do I use it?¶
static_assert(condition, "message") asks the compiler, not the running
program, whether condition — anything usable where the constexpr
section above required one — is true. If it is not, the build fails right
there with your message attached, and the check costs nothing at run time
because there is no run time yet: the failing build never produces a
program to run. C++17 also allows the one-argument form,
static_assert(condition), which has no message and instead prints the
condition's own source text when it fails. The two everyday uses are a
portability assumption ("this platform's int is four bytes", the trap the
"Small but telling details" list below names) and a configuration mistake a
human could make that no type catches on its own — two named constants that
must never collide.
// A portability assumption, checked once instead of trusted forever: this
// chapter's own note is that 'int' is not guaranteed 32 bits the way C#'s
// is, and this line is how the code that relies on it says so.
static_assert(sizeof(int) == 4, "this file assumes a 32-bit int");
// Three build-profile endpoints. Each must be set, and no two may collide -
// a collision would mean a build that silently talks to the wrong server
// the day a profile is misconfigured. constexpr std::string_view compares
// with == at compile time, so the guard runs once, at the build, never
// again at every startup the way a runtime check would.
constexpr std::string_view kDevEndpoint = "https://dev.example.internal";
constexpr std::string_view kUatEndpoint = "https://uat.example.internal";
constexpr std::string_view kProdEndpoint = "https://prod.example.internal";
static_assert(kDevEndpoint != kUatEndpoint, "dev and uat endpoints must differ");
static_assert(kDevEndpoint != kProdEndpoint, "dev and prod endpoints must differ");
static_assert(kUatEndpoint != kProdEndpoint, "uat and prod endpoints must differ");
// The one-argument, C++17 form: no message string, so a failure prints the
// condition's own source text instead.
static_assert(!kDevEndpoint.empty());
In C#: there is no compiler-time assert in the language itself; the
nearest things are a [Conditional("DEBUG")] Debug.Assert or a unit
test, and both run after the build — the compiler has no opinion on them.
Roslyn analyzers and source generators can reject code at build time, but
that is a separate project-level tool, not a keyword every method can
reach for. Chapter 41 returns to static_assert paired with
<type_traits>, once there is a type to ask questions about rather than a
plain value.
Habit: I write an invariant a config or a type must hold as a
static_assert next to the constants it governs, not as a comment above
them — a comment can drift silently, a static_assert cannot.
Small but telling details¶
nullptr // never NULL or 0
enum class Color { Red, Blue }; // scoped enum, like C# enums
using WidgetList = std::vector<Widget>; // modern typedef
uint32_t, int64_t // from <cstdint>: 'int' size isn't
// guaranteed! (C# int is always 32-bit)
In Rust¶
Most of this chapter is Rust's default setting. let infers like auto and cannot deduce a reference away, because a reference is a type. Closures capture by inference and move is the explicit word for the escaping case — the capture rule above, enforced: a closure that borrows a local cannot be stored past the local's life. Iterator adapters are algorithms plus lambdas with the LINQ laziness restored (filter, map, collect), and they are the idiom rather than the loop. Option is optional; an enum with data is variant, and match is the exhaustive visit with the compiler refusing a missing arm. &str is string_view with the dangling case made a compile error by lifetimes. Destructuring is let (a, b) = pair;, and const fn is constexpr. What Rust adds that this chapter cannot is the checker behind each of these — the lifetime on the view and the capture is a fact the compiler knows, not a comment.
In the wild: C-style SDKs¶
Most actively maintained SDKs now require C++17, so nearly all of this is usable in your plug-in or driver code — the exception being the ranges above, which are C++20; every maintained toolchain has had them for years, so whether you may write them is a property of the codebase's -std= setting and policy, not of your compiler's age — Chapter 8's dialect lesson again. The professional style: modern C++ in your logic — optional, lambdas, RAII wrappers — with a thin, disciplined layer where you touch the raw C API. The older the SDK's surface, the more valuable the modern layer you build on top of it.
The baseline question deserves a straight answer while we are here, because a current reader is entitled to ask why this book pins -std=c++17 when C++20 is complete in all three compilers and GCC now defaults to it. Because vendor-SDK work inherits its host's toolset: the pinned compiler of Chapter 13's checklist, the plug-in ABI, the embedded toolchain two versions behind — your floor is set by the oldest thing you must link against, and C++17 is what that world lets you rely on. Everything taught here is valid C++20 and C++23. A newer-standard codebase changes spellings — jthread for thread-plus-join, erase_if for erase-remove, format for the stream dance, span for pointer-plus-length — and this book flags each of those where it teaches the C++17 form. The lessons don't move; only the spellings do — and Appendix K is the table of them, standard by standard, with the probe that tells you which one a toolchain is actually speaking.