Herb Sutter Unveils C++ Safety Profiles to Eliminate Memory Errors

CppCon 2015: Herb Sutter "Writing Good C++14... By Default" — CppCon

CppCon

Local static analysis rules can eliminate memory and bounds defects from C++ code without requiring garbage collection, runtime overhead, or Rust-style annotations.

AI-assisted summary of CppCon 2015: Herb Sutter "Writing Good C++14... By Default" — original source. Check the original for full context.

Source published:

Key ideas

  • The open-source C++ Core Guidelines project and Guideline Support Library (GSL) aim to make C++ type-safe and memory-safe by default through statically enforced profiles without requiring garbage collection or heavy annotations. (2:30)
  • Complete memory and type safety is split into three foundational pillars—Type Safety, Bounds Safety, and Lifetime Safety—with opt-outs strictly localized to provide explicit audit points in source code. (5:00)
  • Bounds safety enforces that raw pointers reference single objects rather than arrays, replacing pointer-and-length pairs with unified span abstractions like array_view and string_view. (13:30)
  • The Lifetime Safety profile uses intra-procedural static analysis and ownership tracking to eliminate use-after-free and dangling pointer bugs locally per function, achieving whole-program safety without whole-program compile times. (30:00)

Detailed notes

## Background Published by the CppCon channel on YouTube on September 24, 2015, this plenary address by Herb Sutter from CppCon 2015 builds directly upon Bjarne Stroustrup's opening keynote. The talk details the open C++ Core Guidelines project and demonstrates how modern C++ can achieve type safety and memory safety by default through sensible library types, static analysis profiles, and clean coding abstractions.

## TL;DR Herb Sutter presents a practical blueprint to achieve complete type, bounds, and lifetime safety in modern C++ without garbage collection or verbose source annotations. By augmenting modern language features with the Guideline Support Library (GSL)—introducing abstractions like `array_view`, `string_view`, and `not_null`—developers can enforce safe defaults while retaining the language's zero-cost performance. The talk introduces local, flow-sensitive static analysis rules that distinguish resource owners from non-owning pointers, tracking pointee invalidations across scopes and assignments. Supported by live tooling demos from Neil MacIntosh, Sutter illustrates how the Lifetime Safety Profile catches longstanding C++ defects, including dangling temporary references in functions like `std::min`.

## Atmosphere & style The presentation carries the pragmatic, energized momentum of a major architectural milestone delivered before an attentive auditorium of nearly a thousand software engineers. Herb Sutter speaks with natural ease, self-effacing humor, and clear authority, peppering rigorous technical exposition with everyday metaphors like kitchen knife drawers and running gags about compiler humiliation. The live tool demonstrations and collaborative stage moments with Neil MacIntosh infuse the session with technical grit and palpable excitement, turning decades-old memory safety debates into achievable, tangible tooling advancements.

## Bulletin - [02:30](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=150s) The open-source C++ Core Guidelines project and Guideline Support Library (GSL) aim to make C++ type-safe and memory-safe by default through statically enforced profiles without requiring garbage collection or heavy annotations. - [05:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=300s) Complete memory and type safety is split into three foundational pillars—Type Safety, Bounds Safety, and Lifetime Safety—with opt-outs strictly localized to provide explicit audit points in source code. - [13:30](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=810s) Bounds safety enforces that raw pointers reference single objects rather than arrays, replacing pointer-and-length pairs with unified span abstractions like array_view and string_view. - [30:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=1800s) The Lifetime Safety profile uses intra-procedural static analysis and ownership tracking to eliminate use-after-free and dangling pointer bugs locally per function, achieving whole-program safety without whole-program compile times.

## Key takeaways - [05:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=300s) C++ evolution follows a supersetting-before-subsetting approach: adding modern safe abstractions first, then disabling unsafe legacy mechanisms by default. - [06:50](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=410s) Type, bounds, and lifetime profiles guarantee code will not be the root cause of memory or type safety bugs unless developers explicitly invoke local suppressions. - [20:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=1200s) Granular safety suppressions like [[gsl::suppress(bounds)]] are superior to coarse 'unsafe' blocks because they keep unrelated safety checks active. - [13:30](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=810s) Rule one of bounds safety mandates that raw pointers point strictly to single objects, banning raw pointer arithmetic and silent array decay. - [15:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=900s) The array_view and string_view types unify pointer and length into a single vocabulary type, replacing brittle C-style buffers and disparate company string implementations. - [27:50](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=1670s) Lifetime analysis categorizes indirections into Owners (which manage resource allocation and cleanup) and Pointers (non-owning observers like T*, references, and iterators). - [30:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=1800s) Lifetime verification is modular and performed strictly intra-procedurally per function, avoiding scalable bottlenecks associated with whole-program static analysis. - [37:30](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=2250s) Pointers extracted from an owner S are designated as referencing synthetic identity S' and are immediately invalidated whenever S is destroyed or modified via a non-const operation. - [43:24](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=2604s) A temporary owner's lifetime is not extended when binding a reference through an operator*() call, causing expressions like const A& ra = *my_fun(); to dangle. - [57:30](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=3450s) Callees assume input pointer parameters are valid for the full call duration, placing the burden on callers to guarantee pointees are not invalidated via re-entrancy or global side effects. - [01:00:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=3600s) Passing raw pointers extracted from shared pointers (sp.get()) across functions can lead to catastrophic dangling if an aliased operation modifies the underlying shared_ptr during the call. - [01:05:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=3900s) Container operations define explicit lifetime contracts that allow static analyzers to catch mismatched iterator ranges and iterator invalidations at compile time without runtime overhead. - [01:15:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=4500s) Function lifetime analysis defaults assume returned pointers originate from non-const owner or pointer parameters rather than expiring R-value magnets or local temporaries. - [01:20:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=4800s) Long-standing library defects, such as std::min dangling when supplied with an lvalue and a temporary, are caught at compile time by tracking allocations from temporary expressions. - [01:25:35](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=5135s) Enforcing safety profiles enables language simplification, such as safely allowing smart pointers to implicitly convert to raw pointers without fear of accidental pointer arithmetic or dangling.

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=0s) Community Response to the C++ Core Guidelines

Herb Sutter takes the stage to deliver the day-two plenary session, building directly upon Bjarne Stroustrup's opening keynote from the previous day. To gauge the audience's reaction, Sutter asks for a show of hands on how many attendees found at least one practical takeaway they want to bring back and apply at work. Seeing a sea of hands across the packed auditorium, he explains that the new C++ Core Guidelines are fundamentally meant to be put into active practice. Sutter reveals a snapshot taken at 6:00 AM showing the top trending GitHub repositories worldwide over the past week; despite having been public for only a few days, the Core Guidelines repository held the number one spot globally.

Addressing the audience of nearly a thousand developers in the room and the broader online viewership, Sutter emphasizes that this achievement extends far beyond the conference hall alone. He contrasts the Core Guidelines' popularity with the major tech news stories dominating mainstream outlets like CNN that same week—such as XcodeGhost (the first major malware affecting iOS) and Apple's release of iOS 9. Even combined, the trending XcodeGhost and iOS 9 sample projects placed third and fourth, trailing far behind the developer community's overwhelming interest in C++ coding standards.

- The C++ Core Guidelines repository became the #1 trending repository on GitHub worldwide within days of its announcement. - Competing top repos that week included XcodeGhost (#3) and iOS 9 sampler projects (#4). - Sutter highlights that the in-person audience alone represents the high hundreds—the better part of a thousand engineers.

[02:30](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=150s) Modern Defaults and the Analogy of the Knife Drawer

Contrasting legacy C++ idioms on the left side of his slide (manual `new` and `delete`, raw loops, explicit indexing) with modern idioms on the right (RAII, range-based `for`, smart pointers), Sutter acknowledges that while the older syntax still compiles thanks to C++'s legendary backward compatibility, an increasing number of developers write clean, modern code in production every year. This keynote acts as "part two" to Stroustrup's talk, diving into the operational mechanics that attendees were already buzzing about overnight: how to make modern, clean C++ the default while preserving the ability to reach down into low-level features when strictly necessary.

Sutter explains that no one wants to break compatibility or strip features away from developers: “Nobody tries to take anything away successfully from C++ programmers.” Instead, he compares low-level language constructs to professional kitchen tools. C++ programmers are like professional chefs, and using sharp knives is fundamentally part of their craft; however, chefs do not leave the knife drawer wide open on the counter all day. Developers need the ability to open the drawer when they genuinely need a knife, but the default state of the language and tooling should arm the safe, simple, modern constructs.

```mermaid flowchart TD accTitle: The C++ Evolution Strategy accDescr: Moving from a broad superset of old and new features to a safe subset enabled by default with opt-in unsafe access.

subgraph Superset["1. Language Evolution (Supersetting)"] Old["Legacy Features<br/>(new/delete, raw loops, casts)"] New["Modern Features<br/>(smart pointers, range-for, spans)"] Old ~~~ New end

subgraph Strategy["2. Modern Guidance & Safety"] GSL["Guideline Support Library (GSL)"] Checkers["Compiler & Static Analysis Checkers"] GSL --> Checkers end

subgraph Enforced["3. Safe by Default Subset"] SafeDefault["Armed by Default:<br/>Type-, Bounds-, & Lifetime-Safe Code"] EscapeHatch["'Knife Drawer' Opt-In:<br/>Explicit unsafe escape hatch for low-level work"] end

Superset --> Strategy Strategy --> Enforced ```

- The Guidelines are currently around a version 0.6 state, functioning as an open-source living document. - Microsoft has released an initial reference implementation of the Guideline Support Library (GSL), which is cross-platform and compiler-neutral. - Sutter encourages the ecosystem to build alternative GSL implementations and tooling checkers rather than relying on a single vendor's implementation. - Sutter asks core contributors seated in the audience and their colleagues to stand and be recognized for their work on the guidelines and checkers.

[05:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=300s) Complete Toolboxes and the Three Pillars of Type and Memory Safety

Picking up Bjarne Stroustrup's formulation of supersetting followed by subsetting, Sutter explains that C++ has historically excelled at supersetting: new libraries and language features are added without removing existing ones. The objective of the Core Guidelines is to provide high-level, safe replacements for every unsafe part of the language so developers have a complete modern toolbox. Once those safe replacements exist, compilers and static checkers can turn off the unsafe parts by default, leaving explicit opt-ins ("opening the hood") whenever raw pointers or unsafe casts are mandatory.

Sutter directly tackles the skepticism surrounding C++ safety, noting: “A lot of people think that C++ and type and memory safety shouldn't be in the same talk. This talk is about nothing but that.” He breaks type and memory safety down into three concrete constituents:

1. **Type Safety:** Preventing type-punning, ensuring a developer cannot treat an allocation of type `T` as an unrelated type `U`. 2. **Bounds Safety:** Ensuring execution never reads or writes outside the allocated bounds of an object or buffer. 3. **Lifetime Safety:** Ensuring an execution never accesses an allocation that has been freed, destroyed, or gone out of scope.

While future profiles will tackle concurrency safety, safe arithmetic, and contracts like `noexcept` or "no-alloc," the initial core focus centers entirely on enforcing these three pillars.

[06:50](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=410s) The Safety Guarantee and Verifiable Code Reality

Sutter explicitly defines the scope and boundary of the Core Guidelines guarantee: “C++ code compiled in the safe subset, so with these particular parts of guidelines turned on, is never the root cause of type or memory safety errors unless you opened the hood and explicitly said, 'unsafe me, go and let me do something that I know is wrong.'” If a failure occurs in code compiled under these rules, the clean code itself is never the root cause. When developers explicitly opt out via an unsafe escape hatch, they create a clear, self-documenting audit point in the source code.

He clarifies that this model assumes developers trust their compiled dependencies. If code relies on a third-party binary library that has not been recompiled through these checkers, the safety guarantees cannot inspect it. Sutter explains that claiming a truly verifiable whole-program guarantee across an entire industry ecosystem is a myth shared by so-called "safe" languages: “None of the safe languages actually achieve that. Everybody, every program in any language eventually calls unsafe code.” When unsafe foreign code corrupts memory, even provably safe code will crash—yet the root cause resides solely in the unsafe component. By compiling source code with guideline enforcement, teams eliminate safety root causes from all code under their direct control.

- The Core Guidelines enforce guarantees through named "profiles," defined as coherent bundles of rules designed to achieve specific safety guarantees. - Initial profiles cover Bounds Safety, Lifetime Safety, and Type Safety; future candidates include Safe Arithmetic, Concurrency, and No-Alloc/No-Throw profiles. - Unrecompiled third-party libraries require independent auditing, but any code recompiled with the checker rules active gains the safe-by-default guarantee.

[10:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=600s) Type Safety Profile: Eliminating Unsafe Casts and Type Punning

Herb Sutter reiterates the foundational strategy of the C++ Core Guidelines project: “We're supersetting, then subsetting.” The goal of adding new types and semantics to the language is not frivolous novelty; it serves specifically to introduce safe abstractions so that unsafe legacy mechanisms can finally be banned. Sutter acknowledges that modern C++ has already made major strides: “Now C++11 got us most of the way. I'd say it's roughly 60-70% of the way.” However, standardizing C++11 was not a coordinated program to systematically eliminate every remaining vector for undefined behavior. That complete coverage is precisely what the core guidelines initiative aims to deliver.

To achieve robust type safety, the type system requires only a few targeted vocabulary additions. Sutter highlights the addition of an explicit `byte` type. Although a raw byte has the exact same bit layout and size as a character, they represent completely different domain concepts: raw uninterpreted memory should not be modeled as text. Another critical missing piece is `std::variant`, which renders C-style `union`s obsolete.

```mermaid flowchart TD accTitle: Type Safety Rules accDescr: Visual breakdown of forbidden unsafe practices and required type-safe alternatives.

subgraph Forbidden["Banned Unsafe Operations"] Casts["Unchecked static downcasts<br/>reinterpret_cast<br/>const_cast (stripping const)<br/>C-style casts"] TypePunning["Unions<br/>Varargs (var_args)"] Uninit["Uninitialized variables<br/>(locals and members)"] end

subgraph Allowed["Enforced Safe Practices"] ExplicitTypes["Use gsl::byte instead of char*<br/>Use std::variant instead of union"] InitCheck["Mandatory initialization before use"] NarrowCheck["Dedicated Safe Math Profile<br/>(separated due to runtime overhead)"] end

Forbidden -->|"Replaced by"| Allowed ```

Sutter condenses the entire Type Safety Profile into explicit rules across two categories: - **Disallow bad casts**: Ban unchecked `static_cast` downcasts, `reinterpret_cast`, and casting away `const` with `const_cast`. If interacting with const-incorrect legacy libraries is unavoidable, developers must encapsulate that call and perform the cast in exactly one isolated wrapper. - **Enforce safe syntax for C-style casts**: While C-style cast syntax remains valid C++, it internally cascades through four fallback strategies (such as `static_cast`, `const_cast`, and `reinterpret_cast`). Any C-style cast that resolves to a banned operation is flagged as a compile error or strict warning. - **Eliminate type punning and uninitialized storage**: Programmers must initialize all variables—both local variables and struct/class members—before reading them. Naked `union`s and C-style variadic arguments (`...`) are completely prohibited. - **Separation of safe math**: The working group debated whether safe arithmetic (checked operations and narrowing detection) belonged in the Type Safety profile. It was deliberately spun out into its own profile because safe math can introduce runtime overhead, whereas general type-safety enforcement remains zero-cost.

Regarding the Guideline Support Library (GSL), Sutter notes that `variant` is intentionally omitted from the initial GSL reference implementation. The ISO C++ standards committee is very close to consensus—potentially finalizing the specification at the upcoming Kona meeting—making an intermediate custom type redundant: “Why invent another one?”

---

[13:30](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=810s) Bounds Safety: Reimagining Array View and String View

Moving from type safety to bounds safety, Sutter addresses accessing memory buffers without overruns. Here, the primary abstractions are `array_view` and `string_view`. While versions of these types exist in ISO Technical Specifications (TS), the GSL model introduces critical architectural shifts. First, whereas standard proposals often view `string_view` as an immutable read-only window, GSL provides read-write views into strings because modifying continuous text buffers is a standard operational need. Second, GSL collapses these concepts into a single underlying primitive: `array_view` is the sole engine, and `string_view` is merely a templated type alias with specialized defaults. Sutter jokes about multidimensional strings: “says, by the way, you really only want one dimension. We could not think of a reason to have multidimensional strings. If you think of one, let us know. Our imaginations were hampered there.”

Sutter provides full disclosure regarding runtime overhead across the entire Guideline Support Library: - Compile-time checking handles almost everything in the GSL at zero runtime cost. - Only two types incur any runtime overhead: `array_view` (which carries bounds tracking) and `not_null` (which asserts non-null pointer validity). - Active design work focuses on minimizing this small overhead so performance is never an excuse to reject safe types.

With these types available, bounds safety rules can enforce a clean semantic rule: pointers may no longer engage in pointer arithmetic. Sutter summarizes the mental shift: “Another way of stating rule one is a pointer only points to one object. That's what pointers should do.” Any implicit array-to-pointer decay and calls to unsafe C-runtime memory functions are banned.

---

[15:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=900s) Real-World Code Migration and SAL Comparison

To prove these concepts work beyond academic exercises, the GSL team iterated over production systems: kernel routines, network protocol parsers, string processing routines, and network transport pipelines. They explored various design dead-ends before arriving at a minimal, cohesive set of types.

Sutter presents a side-by-side comparison illustrating a common pattern in traditional C and C++ annotated with Microsoft SAL (Source Code Annotation Language). In the legacy code, a buffer is represented as a disjointed pair: a raw pointer `things` and an integer `count`. SAL annotations tell the static analyzer that these two arguments are bound together. However, traditional compilers cannot catch common bugs without auxiliary analyzers: - Off-by-one errors in loops, such as writing `<= count` instead of `< count`. - Sizing blunders when calling `memcpy`, such as passing an element count instead of total bytes (`count * sizeof(Thing)`). - Calling sites that lie about buffer capacities, such as passing a count of 30 when only 10 elements were allocated.

```mermaid flowchart LR accTitle: Legacy Buffers vs GSL array_view accDescr: Flow showing transformation from separated raw pointer and count to a unified array_view.

subgraph Legacy["Legacy C/C++ with SAL"] P["T* ptr"] C["unsigned count"] P -. "SAL annotations tie together".-> C LegacyCall["memcpy(dest, ptr, count) // Bug: element count vs byte count"] end

subgraph Modern["Modern C++ with GSL"] AV["gsl::array_view&lt;T&gt; view"] RangeFor["for (auto& item: view)"] RangeCopy["std::copy(view, dest)"] end

Legacy -->|"Refactored into unified type"| Modern ```

Replacing `pointer + length` with `array_view<T>` resolves these vulnerabilities cleanly: - **Const correctness mirrors pointer semantics**: An `array_view<const T>` allows continuous reading while forbidding writes through the view. A `const array_view<T>` prevents reseating the view to another memory address while permitting element mutations. A `const array_view<const T>` forbids both operations. - **Expressive standard algorithms**: Traversals cleanly map to C++11 range-based `for` loops. Sizing bugs vanish in standard operations; an algorithm like `std::copy` can receive the view directly (anticipating Eric Niebler’s Ranges work) and safely copy elements, correctly handling non-POD objects without manual byte-count math. - **Implicit size deduction**: Array views automatically deduce sizes from C arrays and `std::array` instances without boilerplate. - **Zero-friction caller migration**: Call sites that previously passed `pointer, length` can wrap them inside braces `{ptr, len}`. C++11 uniform initialization constructs the required `array_view` without repeating the type name. If a caller explicitly specifies an incorrect size, compiler analysis and dynamic checking immediately catch the violation.

Crucially, the new abstractions preserve all of the guarantees previously provided by static annotation tools: “Now, in all of this, a key goal was not to have any serious regressions from what static analysis could already do.” Because `array_view` functions as an agreed-upon vocabulary type, the compiler and static analyzers inherently understand its semantics. The type system performs many checks at compile time without extra annotation overhead, and fixed-length array views compute bounds entirely at compile time, meeting the overarching design objective: zero call overhead.

[20:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=1200s) Opting Out of Safety: Why Coarse-Grained `unsafe` Blocks Fail

Herb Sutter discusses the mechanics of bounds checking when indexing into an `array_view`. While individual index operations may perform bounds checks that the compiler aims to hoist out of loops, simply accepting an `array_view` parameter must not impose any calling overhead compared to passing a separate raw pointer and length.

When developers genuinely need to perform an unsafe operation, Sutter explains why C++ avoids the blanket `unsafe` block keyword common in languages like Rust or C#. Early in the guideline design, Sutter initially explored using generic `unsafe` blocks, but feedback convinced him that this approach is flawed because it grants excessive latitude: > “And the reason is, is 'cause it lets you do too much.”

If an entire block is marked unsafe just because the programmer needs an unverified bounds operation that they know is actually safe, the compiler will also silently ignore accidental union misuses, dangerous type punning, or other severe bugs inside that same block. Instead, the Core Guidelines require fine-grained, self-documenting rule suppressions directly at the offending line of code.

```mermaid flowchart TD accTitle: Coarse vs Fine-Grained Safety Suppression accDescr: Diagram comparing broad unsafe blocks that hide unintended bugs versus fine-grained Core Guideline rule suppression.

subgraph Coarse["Broad 'unsafe' Block Model"] U["unsafe {... }"] U -->|Disables checking for| B1["Intended bounds override"] U -->|Accidentally hides| B2["Unintended union misuse / type punning"] end

subgraph Fine["Core Guidelines Fine-Grained Model"] S["[[gsl::suppress(bounds)]]"] S -->|Explicitly allows| G1["Bypasses bounds profile for call (e.g. memcpy)"] S -->|Still enforces| G2["Type safety profile checks remain active"] end ```

By writing explicit directives such as `suppress type.cast` or `suppress bounds` (or specifying an exact guideline rule number), developers isolate the exact assumption they are asking the analyzer to accept. For instance, when invoking a legacy routine like `memcpy`, a developer can mark `[[gsl::suppress(bounds)]]` while still taking advantage of `array_view` utility methods (like retrieving byte counts) to eliminate other classes of sizing mistakes.

- Suppression is granular, targeting specific guideline rule numbers or profile categories rather than turning off safety globally. - Fine-grained suppression documents intent directly in the source without blinding static analysis tools to unrelated mistakes on the same line.

---

[22:20](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=1340s) Universal Interoperability: `array_view` and `string_view`

Beyond safety, non-owning view types provide immense convenience by abstracting contiguous memory layouts. Traditionally, writing a function that accepts contiguous elements across varying container types—such as `std::vector`, `std::array`, a raw pointer/length pair, or a proprietary in-house container—required authoring a template that instantiated separate binary copies for every caller. Replacing that template signature with an `array_view<int>` allows a single, unified function body to accept storage owned by any contiguous container without code bloat.

Sutter highlights that this abstraction is even more powerful for strings. He polls the audience on how many companies maintain custom, homegrown string implementations: > “How many of you have your own homegrown string type at your companies? Don't lie.”

Acknowledging the sea of hands (and teasing those too bashful to raise them), Sutter points out that Microsoft alone has multiple historical string types, alongside implementations from Qt, ICE, and countless legacy codebases. There are far too many string variants in existence to fit on a presentation slide. By adopting `string_view` (and `wstring_view` for wide characters), the ecosystem gains a standard interface for read-only string data:

- Custom string types only need to define a single conversion operator to `string_view` (or `wstring_view`). - Once that conversion operator exists, the custom string immediately interoperates with every modern API that accepts a view. - The Bounds Safety profile relies on `array_view` and `string_view` as the foundational replacements for raw buffer access, strictly disallowing raw pointer arithmetic and out-of-bounds indexing. - Ongoing development focuses on tuning the ABI and calling conventions so passing these structures matches the zero-cost calling conventions of passing a bare pointer and integer length.

---

[24:25](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=1465s) The Lifetime Safety Profile: The 40-Year Challenge

Transitioning to the Lifetime Safety profile, Sutter humorously trivializes the problem by stating that memory safety really comes down to three basic axioms: 1. Delete every heap object once (preventing leaks). 2. Delete it *only* once (preventing memory corruption/double-frees). 3. Never dereference a pointer to a deleted object (preventing dangling pointer errors).

After the audience laughs and applauds his facetious summary, Sutter concedes: > “It's really easy to say what the requirements are. It's way harder to do it.”

Solving dangling and leak issues deterministically in native languages has eluded computer scientists for four decades. Acknowledging key collaborators Bjarne Stroustrup and Neil McIntosh, Sutter reviews the historical approaches to memory safety and explains why prior efforts failed to satisfy modern C++ requirements:

| Historical Approach | Typical Implementations | Why It Fails C++ Requirements | |:--- |:--- |:--- | | **C Memory Safety Dialects** | Cyclone, CCured | Assumed C's simplicity made it easier to secure; in practice, C++ code is simpler to analyze because richer type semantics provide more static context. | | **Garbage Collection / Runtime Overhead** | Various managed runtimes | Violates the zero-overhead principle; introduces nondeterministic pauses and runtime overheads intolerable for C++ systems. | | **Whole-Program Static Analysis** | Prefix (vs. Prefast at Microsoft) | Prefix catches deep errors deterministically via full-program graphs, but cannot scale to massive codebases; runs approach "universe heat death" runtimes. Prefast scales only by relying on heuristics that increase false positives. | | **Heavy Source Annotations** | Rust, Cyclone | Requires intrusive lifetime syntax and borrow annotations that burden developers and signal that the language syntax fails to capture natural code intent. | | **New Programming Languages** | Various | Requires rewriting codebases from scratch rather than evolving existing real-world code. |

---

[27:50](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=1670s) The Core Concepts of Lifetime Safety: Owners vs. Pointers

Sutter pushes back against an overreaction he spotted on Reddit following Bjarne Stroustrup's keynote, where an online commenter claimed that all raw pointers are inherently evil. Sutter counters that raw indirection is fundamental to high-performance programming: > “Raw pointers and references are just fine, and you really want to use them on the stack, especially as locals and especially as parameters, for parameter passing by reference, by pointer, because you want the speed and the leanness of just an address.”

Rather than introducing custom language extensions or complex lifetime annotations, the Lifetime Safety profile extracts the rich ownership, scope, and lifetime concepts already embedded in modern C++ code. The profile categorizes every indirection into one of two fundamental conceptual roles:

```mermaid flowchart TD accTitle: Lifetime Safety Profile - Indirection Concepts accDescr: Categorization of indirections into Owners and Pointers within the Lifetime Safety Profile.

Indirection["Indirection (Addresses / References)"]

subgraph OwnerSub["Owner Concept"] Owner["Owner (e.g., std::unique_ptr, Containers)"] OwnerRule["Guarantees single destruction, proper cleanup; prevents leaks and double frees."] end

subgraph PointerSub["Pointer Concept (Borrow / Observer)"] Pointer["Pointer (e.g., T*, T&, iterators, array_view, ranges)"] PointerRule["Observes without ownership; analyzer ensures it never outlives the target object."] end

Indirection --> OwnerSub Indirection --> PointerSub ```

- **Owner:** An abstraction (such as `std::unique_ptr`, `std::shared_ptr`, or standard containers) that manages the allocation and deterministic cleanup of an underlying resource. Because an owner manages lifetime, the profile's role is simply ensuring it performs destruction exactly once. - **Pointer (Capital "P"):** Any non-owning indirection that observes a target without owning it. This includes raw pointers (`T*`), references (`T&`), standard iterators, range pairs, and view structures like `array_view`. Because these can dangle, the profile tracks their valid range to ensure they never outlive the target storage they observe.

[30:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=1800s) Principles of Local Static Analysis and Tracking Lifetimes

Herb Sutter lays out the primary design goals for lifetime tracking: ensuring pointers never dangle and that resources never leak. Because a raw pointer type like `T*` is fundamentally ambiguous about ownership, the Core Guidelines introduce the `owner<T*>` wrapper. This wrapper is simply a typedef alias, but it conveys clear semantic intent to analyzers and compilers, enabling them to strictly enforce that the resource is deleted properly. Sutter emphasizes that this model relies purely on local, per-function rules rather than whole-program analysis, which does not scale. By compiling and analyzing a single function at a time—matching standard compiler architecture—one achieves whole-program safety guarantees with absolutely zero runtime overhead, zero instrumentation, and zero dynamic bookkeeping.

```mermaid flowchart TD accTitle: Static Analysis Tracking Flow accDescr: Flowchart showing how the compiler statically tracks owners and pointer targets across function scopes without whole-program analysis.

subgraph Ownership ["Ownership & Pointers"] O["owner&lt;T*&gt; / Containers / Smart Pointers"] -->|"tracks resource lifetime"| L["Leak Freedom"] P["Pointer / Reference (T*, T&)"] -->|"tracks points-to set"| D["Dangle Freedom"] end

subgraph ScopeRules ["Local Per-Function Rules"] L --> C["Compile-time Verification"] D --> C C -->|"no runtime instrumentation"| Z["Zero Runtime Overhead"] end ```

The system is designed so that developers do not have to write heavy annotations throughout their code; intelligent defaults cover common idioms. Rather than tracking fine-grained inner members, a pointer to a class member or an array element simply tracks the lifetime of the outermost enclosing object or array. For heap-allocated objects, the system tracks the owner managing that object. When calling functions, the static analyzer applies sensible defaults: passed pointers are assumed to be independent and valid for the duration of the call, and returned pointers are assumed to derive from the input arguments. As Sutter summarizes the core objective: “The goal is leak freedom for the owners and tracking points to to avoid dangling for pointers.”

- `owner<T*>` acts as a readable marker for tools to enforce cleanup when modern types like `std::unique_ptr` cannot be used. - Static analysis is strictly modular: functions are checked in isolation, maintaining compiler scalability while delivering global guarantees. - The default assumption for functions returning a pointer is that the result originates from one of the parameters, covering roughly 99.99% of real-world cases.

---

[33:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=1980s) Warm-Up: Detecting Dangling Stack Pointers

To illustrate basic local checking, Sutter presents an example where three pointers declared in an outer scope point to stack-allocated variables inside an inner block: an ordinary integer, a member within a `struct`, and an element within an integer array. Inside that nested scope, all three pointers remain entirely valid because none of their pointees have been destroyed. However, the moment execution leaves the inner scope, all three target objects go out of scope simultaneously, invalidating every pointer that references them. Attempting to use any of these pointers afterward represents an immediate use of a dangling pointer.

``` +-------------------------------------------------------+ | Outer Scope: int *p1 = nullptr, *p2 = nullptr,... | | +-----------------------------------------------+ | | | Inner Scope: | | | | int i; | | | | MyStruct s; | | | | int arr[5]; | | | | p1 = &i; p2 = &s.member; p3 = &arr[2]; | | | | (p1, p2, p3 valid here) | | | +-----------------------------------------------+ <--- Line A: Destruction | Use of p1, p2, p3 here: ERROR (dangling pointers) | +-------------------------------------------------------+ ```

When an invalidated pointer is used after the block exit, the compiler or static analysis tool should flag it with clear diagnostic text, such as: `P was invalidated when I went out of scope at line A`. Sutter polls the audience of over 700 attendees to see how many already use tools delivering diagnostics that readable for this scenario. Only five or six hands go up—less than 1% of the room. He notes that while diagnosing stack escapes within a single function is straightforward enough that a human could do it, automated tools must consistently catch these errors and present them with crystalline clarity.

- Stack objects, struct members, and array elements are invalidated at their scope exit line. - The analysis abstracts members and array indices to the lifetime of the enclosing container or object. - Sutter observes of the room's current diagnostic tooling: “How many of you use such tools? I see about four hands. Got about over 700 people here, so about five or six hands. Maybe 1%.”

---

[35:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=2100s) Indirection, Pointers to Pointers, and Dereferencing Rules

Sutter explains how taking an address and tracking indirection works under the static analysis model. In C++, references and pointers share the same underlying indirection nature for lifetime tracking purposes; taking the address of `i` with a reference `ri` or a pointer `pi` means both have a static "points-to" list containing `{i}`. When creating a pointer to a pointer, such as `int** ppi = &pi;`, the analyzer only records direct, single-hop associations: `ppi` points to `pi`, and `pi` points to `i`. It does not need to store multi-level indirection tables explicitly, because pointer chains can simply be traversed step-by-step on demand.

```mermaid flowchart LR accTitle: Pointer Indirection and Dereference Tracking accDescr: Step-by-step traversal from pointer-to-pointer through intermediate pointer to the underlying integer variable.

PPI["ppi (&pi)"] -->|"points to"| PI["pi (&i)"] PI -->|"points to"| I["i"] PI2["pi2 (*ppi)"] -->|"copies points-to list"| I ```

Dereferencing is modeled by looking up what the source pointer refers to. Because `ppi` points to `pi`, evaluating `*ppi` statically resolves to `pi`. Therefore, writing `int* pi2 = *ppi;` is recognized by the analyzer as equivalent to `int* pi2 = pi;`. When this copy occurs, `pi2` inherits not just the runtime address value, but also the compile-time points-to set of `pi`, meaning `pi2` now points to `i`. If `pi` is subsequently reassigned to point to an integer `j`, `ppi` requires no updates at all because it still points to `pi`. Dereferencing `*ppi` again will dynamically track the updated target and assign `{j}` to any newly initialized pointer. As Sutter puts it: “These seem like very simple rules, but we have to baby talk to a computer, because it only understands little simple rules one at a time. But on this, we can build a lot.”

- References (`T&`) and raw pointers (`T*`) are treated identically as indirection types that track their target. - Taking the address of an owner, such as a `std::shared_ptr`, simply yields a pointer pointing to the smart pointer variable itself for its lifetime. - Dereferencing propagates the pointee tracking list directly from the target pointer without needing complex deep-graph updates.

---

[37:30](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=2250s) Owned Heap Objects and the `S'` Notation

Moving past the basic warm-ups, Sutter tackles what happens when a program creates a pointer to a value managed inside a heap owner, such as taking a raw pointer from a `std::shared_ptr<int> s` via `s.get()`. Unlike stack variables, heap-allocated entities do not possess a static variable name in the source code. To solve this, the static analysis rules introduce a synthetic concept: the owned heap entity is designated as $S'$ ("S prime"), representing whatever object is owned by the smart pointer $S$.

``` +--------------------------------------------------------+ | Stack: | | shared_ptr<int> s; <======ps (&s points to s) | | | | | | owns | | v | | Heap: [ int (value 2) ] <=== S' | | ^ | | +==================== p (s.get() points to S')| +--------------------------------------------------------+ ```

When code executes `int* p = s.get()`, the points-to set of `p` is statically recorded as pointing to $S'$. The analyzer then enforces a clear lifetime rule: $p$ remains valid only as long as its owner $S$ is neither destroyed nor modified. Determining whether the owner might be modified relies directly on the type system. Because `const` semantics distinguish mutating member operations from non-mutating ones, the analysis knows exactly which member function calls on `std::shared_ptr` can invalidate the underlying pointer. As Sutter notes regarding the role of constness: “How do I know if something's modified? Oh, Bjarne invented this wonderful thing called const that C took as well. We already have that information.”

- Heap allocations managed by an owner $S$ are designated using the synthetic identity $S'$ in the static analysis model. - Pointers extracted via `.get()` or operator dereferencing are tied directly to the lifetime and modification state of the owning object. - Existing language-level `const` annotations provide the analyzer with the exact boundary between safe non-modifying calls and potential pointer invalidations.

[40:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=2400s) Pointer Invalidation Rules and the First Live Tool Demo

Herb Sutter explains the core lifetime rules governing pointer invalidation when dealing with owners. When a raw pointer `p` points to something owned by an owner `s` (such as a `std::shared_ptr`), `p` remains valid until `s` is either destroyed or modified via a non-const operation, such as calling `.reset()` or swinging it to point somewhere else. Sutter acknowledges that strict static checking might occasionally produce false positives—for example, a self-assignment like `s = s;` that does not genuinely alter the target—yet he is entirely comfortable with that tradeoff:

> “Now you could know, yeah, but I didn't actually swing it. That's OK. I'm willing to take some false positives. We're going to work to drive those down. But I want soundness. And I know that if you modify that shared pointer, I can no longer trust that pointer.”

Sutter stresses that these rules are deliberately general. Nothing in the analysis engine is hard-coded for `std::shared_ptr`; the logic applies uniformly to any type identified as an owner, whether user-defined smart pointers or standard containers like `std::vector`.

```mermaid flowchart TD accTitle: Owner Invalidation Lifecycle accDescr: Diagram illustrating how an owner creates a pointer, which becomes invalid when the owner is modified or destroyed. subgraph Allocation ["Owner Lifecycle"] A["Owner s created"] --> B["p = s.get()"] B --> C["Use *p (Valid)"] end subgraph Invalidation ["Invalidation Event"] C --> D["Modify/Reassign s (e.g., s =...)"] D --> E["p invalidated"] end subgraph Dereference ["Illegal Access"] E --> F["*p dereferenced -> Static Analysis Error"] end ```

To demonstrate that these static checks are practical rather than purely theoretical, Sutter invites Neil MacIntosh to the stage. MacIntosh and his team built an experimental prototype inside Microsoft's existing static analysis framework (originally known for PREfast, PREfix, and SAL annotations) to enforce lifetime rules directly without manual annotations. MacIntosh opens Visual C++ with a clean example dereferencing a pointer obtained from a shared pointer. The analysis initially runs clean with an empty error list. MacIntosh then adds a line reassigning `s` before the dereference (`*p = 42;`) and reruns the analyzer. A pink warning immediately flags the line: the analyzer explains that `p` is an invalid pointer because `s` was reassigned via `operator=` at line 8.

- Raw pointer `p` is valid only until owner `s` is destroyed or modified through a non-const use. - The lifetime model targets full soundness over avoiding harmless edge-case false positives (e.g., self-assignment). - The rules apply generically to any smart pointer or container recognized as an owner, not just standard library types. - Neil MacIntosh's prototype integrates with Microsoft's existing C++ static analysis infrastructure.

---

[43:24](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=2604s) The Stack Overflow Bug: Returning a Unique Pointer by Value

Moving to a second scenario, Sutter recounts finding an actual bug report posted to Stack Overflow just hours before drafting this portion of his paper:

> “The exact quote was, 'This code compiles, but RA contains garbage. Can someone explain me why this code is invalid?'”

The user's code had a function, `my_fun()`, that returns a `std::unique_ptr<A>` by value. In the calling code, the author attempted to initialize a reference, `const A& ra = *my_fun();`, binding directly to the dereferenced result of the returned temporary. Sutter explains the underlying mechanics: `my_fun()` produces a temporary rvalue `unique_ptr` (`temp_up`). The dereference operator produces a reference to an object owned by that temporary. But as soon as the full expression finishes, the temporary `unique_ptr` destructs, releasing its owned memory and leaving `ra` dangling. Sutter argues that compilers ought to reject this construct immediately upon initialization rather than forcing developers to ask for explanations online:

> “Wouldn't it be nice if our compiler just told us that, and we could save ink on Stack Overflow?”

- The Stack Overflow code snippet dereferenced the return value of a function producing an rvalue `std::unique_ptr<A>`. - The temporary smart pointer was destroyed at the semicolon of the initialization statement, invalidating the bound reference `ra`. - Sutter advocates flagging an error at the exact point of initialization, as initializing a reference into an already-dead temporary object can never be legitimate.

---

[45:48](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=2748s) Live Demo: Detecting Dangling References and Fixing Lifetime Issues

Neil MacIntosh returns to the screen to verify this scenario in the analyzer. MacIntosh jokes about his outfit—wearing Seattle Seahawks team colors—which Sutter calls proof of being "loyal and optimists," qualities Sutter quips are indispensable when dealing with C++ pointer lifetimes. MacIntosh dryly echoes Marshawn Lynch:

> “Yeah, that's right. Just here so I don't get fined.”

MacIntosh runs analysis on the problematic Stack Overflow snippet. The tool immediately flags line 14 in pink, reporting that `ra` was dereferenced after being invalidated by the destructor of the temporary `unique_ptr`.

MacIntosh and Sutter demonstrate two ways to fix the problem: 1. **Retain the Owner:** Change the local variable to capture the returned smart pointer itself (`auto p = my_fun();`) and dereference it later. After this change, the analyzer runs and passes with zero warnings. 2. **C++ Temporary Lifetime Extension:** Sutter explores an advanced language subtlety: why the original `const A& ra = *my_fun();` failed to extend the lifetime of the temporary. Lifetime extension only applies when binding a reference directly to the temporary itself, not when binding to a sub-object or through an `operator*()` call of a distinct type (`A` versus `unique_ptr<A>`). Sutter shows that changing the declaration to bind directly to the returned temporary (`const auto& p = my_fun();` or `auto&&`) safely extends the lifetime of the temporary `unique_ptr` to match the scope of the reference, moving the dereference down to the use point. MacIntosh tests this live, and the analyzer correctly recognizes the lifetime extension and reports no errors.

- The static analyzer correctly identified that the temporary owner's destructor ran at the end of the full-expression, invalidating the reference. - Normal resolution: store the owner locally (`auto p = my_fun();`) to keep the underlying object alive. - Advanced resolution: binding directly to the temporary extends its lifetime, which the prototype analysis engine tracks accurately without producing false positives.

[50:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=3000s) Multi-Level Pointer Tracking and Container Invalidation

Herb Sutter walks through a complex ownership setup using a diagram showing multiple levels of indirection. At the root, there is an `sv`, a `std::shared_ptr<std::vector<int>>`, which owns an unnamed heap-allocated vector, which in turn owns a buffer of integers. From this hierarchy, three raw pointers are established: - `sv2`, a raw pointer directly pointing to the `shared_ptr` (`sv`). - `vec`, a raw pointer pointing to the unnamed vector (`*sv`). - `ptr`, a raw pointer pointing directly into the vector's internal integer buffer.

Sutter jokes, “Clear as mud? This is what we ask compilers to do all the time. We're so mean.” Under normal circumstances, reading or writing through `ptr` is completely valid because it was acquired through a legitimate path and points to live memory.

```mermaid flowchart LR accTitle: Multi-Level Ownership and Pointer Invalidation accDescr: Diagram tracing shared_ptr sv, its owned vector sv prime, its buffer sv prime prime, and their raw pointers.

subgraph Owners ["Owners & Heap"] SV["sv (shared_ptr)"] -->|"owns"| VecObj["*sv (Vector) [sv']"] VecObj -->|"owns"| IntBuf["Integer Buffer [sv'']"] end

subgraph Aliases ["Raw Pointers / Aliases"] SV2["sv2 (&sv)"] -.->|"points to"| SV VecPtr["vec (&*sv)"] -.->|"points to"| VecObj IntPtr["ptr (&(*sv)[0])"] -.->|"points to"| IntBuf end ```

To demonstrate how changes disturb this setup, Sutter considers what happens when mutating the vector via `vec->push_back(1)`. Although a push-back might not reallocate if capacity allows, the C++ standard specifies that it invalidates iterators and references into the vector. To turn this English specification into an automated compiler rule, Sutter introduces a formal concept of tracked ownership levels using "primes" ($'$): - `sv2` points to `sv`. - `vec` points to `sv'` (an entity directly owned by `sv`). - `ptr` points to `sv''` (an entity owned by something owned by `sv`).

The analysis tracks up to two levels of prime (`''`); deeper nesting falls back to conservative assumptions, trading occasional false positives for tractability while covering most practical bugs. Because `vec->` dereferences `vec`, it designates the expression `sv'`. Calling `.push_back()` is a non-`const` mutation on `sv'`, which automatically invalidates anything dependent on it—namely anything classified as `sv''`. Therefore, `ptr` is invalidated, while `vec` and `sv2` remain unaffected. Sutter expresses long-standing excitement for this capability: “I have been dying to get that kind of error message for many, many years.”

Alternatively, if one resets the outer owner via `(*sv2).reset()`, the target of `sv2` is `sv` itself. Mutating `sv` with a non-`const` operation invalidates all dependent children: both `sv'` and `sv''`. Consequently, both `vec` and `ptr` become invalidated, triggering compile-time diagnostics on any subsequent dereference.

---

[53:30](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=3210s) Live Tool Demonstration: Detecting Dangling Pointers via Aliasing

Putting the rules to the test, Sutter works with Neil to demonstrate the static analysis tool on live code: “Could a compiler really do this? What is there to lose? It's just public humiliation, right? Let's try it.”

1. **Baseline validation:** The code initializes the pointers and dereferences them without mutation; the compiler check succeeds cleanly. 2. **Push-back invalidation:** Neil inserts a vector push-back (`vec->push_back(42);`, prompting Sutter to joke that Douglas Adams would be proud). The tool immediately outputs an error identifying that the pointer was invalidated by `std::vector::push_back` on line 12, flagging the illegal dereference on line 14 where the program dereferences the invalid pointer `ptr`. 3. **Reset invalidation:** Neil comments out the push-back and instead calls `sv2->reset()`. Running the tool now produces two distinct warnings: both `vec` and `ptr` are flagged as invalidated by the `reset()` invocation.

Sutter highlights why this diagnostic is so powerful: `sv2` is an alias created completely separately from the expression path used to fetch `ptr`. Conventional syntactic checks fail here, but because the tool tracks what every pointer actually designates, it identifies that `*sv2` is an alias for `sv`. Modifying the object through the alias propagates the invalidation across all dependent references: “And even though I got these pointers from unrelated paths, the correct invalidation occurs because I know just at one step, he points to him, or he points to an object owned by this owner.”

---

[56:10](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=3370s) Flow-Sensitive Local Analysis Rules

Sutter summarizes the remaining rules for local, intra-procedural static analysis: - **Branches (`if`/`else`):** If a pointer `p` is assigned to object `x` along one branch and object `y` along another, the analysis merges the paths and models `p` as potentially pointing to either `x` or `y` (`p -> {x, y}`). - **Loops:** Handled with fixed-point merges analogous to branches. - **Nullability checks:** If a pointer is conditionally tested (`if (p!= nullptr)`), the null state is filtered out within that guarded block. - **Exceptions (`try`/`catch`):** The engine adopts pessimistic assumptions; upon entering a `catch` block, it assumes all potential invalidations within the `try` block occurred, and none of the revalidations succeeded.

---

[57:30](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=3450s) Inter-Procedural Analysis and Function Call Safety

Sutter transitions from local checks to inter-procedural reasoning. While local analysis alone would eliminate huge categories of bugs, real programs constantly pass pointers across function boundaries: “I mean, if we did nothing but this local stuff, we would catch boatloads of errors. And I'd be very happy with that. But people keep calling functions, so let's do something about that.”

When passing a pointer `p` to a function `f(p)`, two correctness conditions must be guaranteed: 1. The pointee must outlive the duration of the function call. 2. The function `f`, as a side effect (including via reentrancy or modifying shared/global state), cannot accidentally invalidate whatever is keeping `p` alive.

Sutter notes how insidious that second requirement is, asking the audience how many have suffered from it: “You have to guarantee that F, as a side effect, cannot accidentally invalidate, including through reentrancy, whatever is keeping P alive. Now, that was a long phrase. How many of you recognized it and say, oh, yeah, I've lived that? Way too many people.”

To make inter-procedural checking practical without full whole-program analysis, the model splits responsibilities between callee and caller: - **Callee contract:** When a function accepts a raw pointer parameter, it unconditionally assumes the pointer is valid for the duration of the call: “In the callee, now, if I'm inside a function and I get a pointer parameter, I'm just going to assume it's good for the lifetime of the call. Yeah, I don't care it fell off the end of your uncle's truck. Looks good to me. I'll trust you on this one.” This matches standard nested stack frame semantics. - **Caller contract:** The caller must verify that the argument passed cannot be invalidated during the call.

Sutter introduces a prime example of where this contract breaks down—the single most frequent correctness mistake developers make when working with smart pointers: passing the raw address of a managed object (`gsp.get()`) from a global or shared owner into a function that might directly or indirectly modify that owner.

[01:00:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=3600s) Preventing Smart Pointer Invalidation and Re-entrancy Bugs

Herb Sutter addresses a classic pitfall when passing raw pointers obtained from smart pointers down a call tree. Suppose a function `f` is given a raw pointer extracted via `.get()` from a global shared pointer `gsp`. Sutter emphasizes that `f` has no way of knowing that the pointer it received points to an object owned by `gsp`. If `f`, or any downstream function a hundred calls deep, executes a side effect that modifies `gsp`, the underlying object can be destroyed, leaving the raw pointer dangling. To fix this, Sutter explains the established guideline: take a single local copy of the `shared_ptr` at the top of the call tree before extracting the raw pointer with `.get()`. This ensures a reference count that cannot possibly drop to zero during downstream execution, completely immune to side effects on the global variable. While Sutter taught this rule as prose in his CppCon 2014 talk ("thou shalt and thou shalt not"), developers often forget it, highlighting the need for automated tooling.

```mermaid flowchart TD accTitle: Safe Pointer Extraction Pattern accDescr: Diagram showing how a local copy of a shared_ptr prevents raw pointer invalidation from global side effects.

subgraph Unsafe["Unsafe Call"] GSP1["Global gsp"] -->|"gsp.get()"| F1["f(raw_ptr)"] F1 -.->|"Side effect alters gsp\n(Dangling pointer!)"| GSP1 end

subgraph Safe["Safe Local Root Pattern"] GSP2["Global gsp"] -->|"Copy once"| SP["Local sp (refcount +1)"] SP -->|"sp.get()"| F2["f(raw_ptr)"] F2 -.->|"Side effect alters gsp\n(sp holds refcount alive!)"| GSP2 end ```

Sutter examines a second example involving a function `g` that takes a modifiable `shared_ptr&` and an `int*`. If a caller passes `sp` and `sp.get()` into `g`, the call is unsafe because `g` could modify `sp` through its non-const reference, thereby destroying the object while holding an alias to it. Sutter remarks: “Dollars to donuts, you don't know.” By contrast, passing `gsp` and a pointer derived from a local `sp.get()` remains safe: even if `gsp` and `sp` happen to alias the same underlying object, modifying `gsp` will not decrement the reference count to zero because the local `sp` retains ownership throughout the call. Calling this the number one correctness issue with smart pointers, Sutter notes that programmers frequently complain about re-entrancy and unexpected dangling when interfacing across language boundaries, which can all be avoided by acquiring a local root before dropping down to raw pointers.

- Obtaining a local `shared_ptr` root costs exactly one reference increment and decrement at the root, after which free raw pointer access can safely traverse a million-line call stack. - Herb Sutter: “This is the number one correctness issue using smart pointers.” - Re-entrancy bugs commonly surface when calling into other languages or deep libraries that invoke callbacks mutating the original smart pointer.

---

[01:05:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=3900s) Tool Enforcement and Parameter Lifetime Contracts

Herb invites Neil MacIntosh back to the live editor to verify these scenarios against the prototype analyzer in Visual Studio. Testing example four, the tool confirms the analysis by issuing a diagnostic warning that calling `.get()` on `gsp` yields a pointer that could be accessed and invalidated by `f`. Neil then edits the code to introduce a local `shared_ptr sp = gsp;` and passes `sp.get()`, causing the error diagnostic to disappear cleanly. When they deliberately test passing `sp` and `sp.get()` into `g(sp, sp.get())`, the analyzer again catches the violation. Sutter points out that this bug is actually caught by two distinct layers: first, by the rule prohibiting passing a pointer into a function that could invalidate it, and second, by the foundational rule that passing `sp` by non-const reference is itself a mutating use of `sp`, which invalidates dependent pointers prior to the call. Sutter observes that while lifetime rules fix the primary correctness flaw of smart pointers, the primary performance flaw—passing `shared_ptr` by value when a raw pointer or reference suffices—is addressed by separate parameter-passing guidelines.

Turning to container operations in the standard library, Sutter demonstrates that defaults sometimes need overriding via lifetime contracts. Functions such as container `insert` have overloads with fundamentally different pointer relationships:

| Method / Overload | Lifetime / Alias Requirement | Default Behavior vs. Contract Override | |:--- |:--- |:--- | | `insert(pos, value)` | `pos` iterator must point into `*this` container | Overrides default independence; requires iterator to match `lifetime(this)` | | `insert(first, last)` | `[first, last)` range must NOT point into `*this` container | Defaults to independent containers; requires `lifetime(last) == lifetime(first)` |

Compilers cannot enforce the English prose in the ISO Standard, but explicit lifetime annotations can. For a single-position hint `m.insert(m2.begin(), val)` on a map `m`, the tool flags an error because `m2.begin()` does not point into `m`. Conversely, attempting a range insertion `m.insert(m.begin(), m.end())` triggers two distinct violations: the iterators point into the container being mutated, and calling `insert` can invalidate those very iterators mid-execution.

- Herb Sutter: “Smart pointers are great. They're commonly misused. That is the number one correctness issue with smart pointers. Re-entrancy, the side effect of accidentally kicking the bucket that your pointer is standing on.” - Draft annotation syntax allows specifying that an iterator parameter shares the lifetime of `this` or that a pair of range iterators point to the exact same underlying collection. - In `m.insert(m.begin(), m.end())`, mutating `m` simultaneously violates the container-independence contract and causes active iterator invalidation.

[01:10:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=4200s) Catching Invalid Iterator Ranges and the Function Return Lifetime Problem

```mermaid flowchart TD accTitle: Compile-Time Verification of Return Lifetimes accDescr: Diagram illustrating how the compiler assumes output lifetimes match input owners and validates this when compiling the callee. subgraph CallerSide ["Caller Side (Default Assumption)"] InParams["Input Owners / Pointers: (s,...)"] --> RetVal["Returned Pointer (ret)"] RetVal -.->|"Assumed lifetime: points into"| InParams end subgraph CalleeSide ["Callee Side (Separate Compilation)"] CodeReturn["return s.c_str();"] --> MatchCheck{"Matches assumed lifetime?"} MatchCheck -->|"Yes (returns s')"| OK["Compile Success (Zero Annotation)"] MatchCheck -->|"No (returns static or other param)"| Err["Compile Error: Fix code or annotate [[lifetime]]"] end ```

Herb Sutter finishes demonstrating compile-time container safety by addressing invalid iterator ranges. When calling an operation like `m.insert(m2.begin(), m.end())`, two iterators from completely different containers are passed together. The static analysis catches this immediately, issuing a clear error message: parameter 1 points to `m2`, but parameter 2 points to `m`, whereas they are required to point to the same entity. Correct code passing two iterators from the same container compiles without complaint. Sutter emphasizes that this verification is performed strictly at compile time: “We believe that this will eliminate large classes of iterator range errors with zero debug instrumentation.” Because iterators are conceptually "capital-P Pointers"—values that indirect to an object without owning it—tracking their underlying target owners prevents long-standing bugs that developers have accidentally written for decades.

Turning to the next major design challenge, Sutter examines how to reason about pointers returned from functions. Pointing to an example slide, he notes audience members nodding along, specifically teasing Detlef Vollmann: “Is that Detlef nodding heavily there? If you were in Detlef’s talk yesterday about concurrency, you will appreciate why he said the head just kept going.”

To track returning pointers, a lifetime contract must exist between caller and callee. While in principle a programmer could explicitly state the lifetime of every returned pointer, requiring manual annotations everywhere would be onerous. Sutter points out that programming languages and program semantics are remarkably regular:

> “Ever since the dawn of computing, we know where most outputs of any function—forget about pointers, if you get an `int` out of a function that takes two `int`s, where do you suppose it came from? Dollars to donuts it came from those two `int`s you passed in.”

Just as integer outputs almost always originate from input arguments rather than global state, a returned pointer almost always refers to memory passed in via the input Owners and Pointers. By adopting that reality as the default rule, the common case requires zero annotations: - If a function takes owner or pointer parameters, the returned pointer is assumed to point into those inputs. - If a function takes no pointer/owner parameters (e.g., no arguments, or only primitives like `int`), its return value is assumed to point to `static` storage, eliminating the need to annotate singleton instances. - If a callee genuinely returns a different lifetime (such as returning a static buffer while taking pointers), the separate compilation of the callee fails. The compiler points out the discrepancy, and the programmer can copy and paste the suggested lifetime annotation directly into the function declaration.

Sutter contrasts this philosophy with other memory-safe languages: “And this is what I don’t like about Rust and Cyclone and C-Cured: there’s too heavy annotation. You should not need any annotation at all in most functions.” While Rust has incorporated lifetime elision defaults, Sutter emphasizes that idiomatic C++ should avoid explicit regions and named scopes wherever sensible defaults suffice. When calling a function taking two `int*` parameters and returning an `int*`, the compiler treats the return as branching: the returned pointer is tracked as pointing either to `*p` or `*q`. When passing an Owner (such as a string reference) and getting a pointer back, the compiler denotes the lifetime target as "Owner prime" ($Owner'$), signifying that it points into elements owned by that argument.

---

[01:15:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=4500s) R-Value Magnets and Validating Callees

Sutter addresses temporary objects in C++, highlighting how the standard's value categories (`lvalue`, `prvalue`, `xvalue`) integrate cleanly with lifetime tracking. To capture how parameters bind to values, Sutter coins practical terms: - **Weak R-value magnets:** A `const&` (such as `const string&`) binds to lvalues as well as temporaries. Because it frequently binds to durable objects, the analysis treats it as a weak magnet. When searching for what a returned pointer points to, the compiler prefers non-const owner references over `const&` owner parameters, avoiding assuming a return points into a temporary unless no better candidates exist. - **Strong R-value magnets:** An rvalue reference parameter (`Owner&&`) is explicitly designed to bind to temporaries (`prvalues`) or expiring objects (`xvalues`, such as variables passed through `std::move`).

Sutter warns that returning a pointer tied to an rvalue reference parameter is fundamentally dangerous:

> “The only thing that can really attach to, or should attach to conceptually, is either a temporary, in standardese terms, a PR value... and you don’t want to return a pointer into one of those, because he’s about to go away. You know that for sure.”

Even if an `xvalue` was not originally a temporary, the callee is expected to move from it, meaning its internal state is about to be emptied or terminated. Therefore, the lifetime analysis will never default to assuming a returned pointer refers to an `Owner&&` parameter.

Sutter illustrates this with a `find_match(string& s, const string& match)` function. Because `match` is a const reference (a weak r-value magnet) and `s` is an lvalue reference, the compiler defaults to assuming the returned `char*` points into `s` ($s'$). Consequently, callers can safely pass string literals, temporaries, or user-defined literals into `match` without triggering lifetime warnings, because the returned pointer's validity is governed solely by the lifetime of `s`.

Inside the callee implementation, the compiler enforces the contract: - If the implementation derives its return pointer from `s`, the conversion to $s'$ matches the assumed interface and succeeds cleanly. - If the body instead takes the address of something inside `match` (or a local temporary) and tries to return it as a pointer to $s'$, compilation fails with a type-lifetime error. The developer must either fix the buggy code or explicitly annotate the declaration with the non-default lifetime.

Sutter notes that standard container idioms fit this model naturally: - `std::vector::operator[]` returns a reference into `*this'`, treating the invisible `this` pointer as the owner. - `std::vector::begin()` returns an iterator pointing into `*this'`, meaning mutating operations that invalidate `*this` will automatically invalidate that iterator.

Closing the segment, Sutter checks the room for Gabriel Dos Reis ("Gabby") and pivots with a smile to an ongoing debate: “So I’ve been getting email from a brilliant friend of mine named Andrei Alexandrescu. And Andrei really is brilliant and he’s done a lot of good work for C++... and other languages. But he emails me about every year...” setting up a discussion of Alexandrescu's challenges regarding language safety and design.

[01:20:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=4800s) The Andrei Alexandrescu Problem: Dangling References in `std::min`

Herb Sutter brings up a longstanding complaint from Andrei Alexandrescu about `std::min`: when will the committee fix it? Pointing to Gabriel Dos Reis (Gaby) sitting in the aisle, Sutter notes that Andrei had been sending similar emails for years. Dos Reis confirms that he and Andrei first discussed the problem around 1998, well before Facebook or Google Groups existed. Dos Reis explains the core defect: `std::min` accepts its arguments by `const T&` and returns a reference. While passing two lvalues like `min(x, y)` works safely and efficiently by returning a reference to one of them, the design becomes dangerous the moment a temporary is introduced.

```mermaid flowchart TD accTitle: The std::min Temporary Lifetime Problem accDescr: Flowchart illustrating how passing a temporary to std::min leads to an invalid dangling reference under maintenance.

subgraph Inputs["Arguments to std::min"] LVal["Lvalue: x"] Temp["Temporary: y + 1"] end

subgraph MinCall["std::min(x, y + 1)"] RefArg["Binds to const int&"] RetRef["Returns reference to smallest"] end

subgraph Outcome["Scope Exit"] Destruct["Temporary destroyed at full-expression end"] Dangle["Returned reference dangles if (y + 1) chosen!"] end

LVal --> RefArg Temp --> RefArg RefArg --> RetRef RetRef --> Destruct Destruct --> Dangle ```

Sutter illustrates how easy it is to write a disaster during refactoring: a developer might change `min(x, y)` to `min(x, y + 1)`. The audience groans, and Sutter calls it a "sneeze"—a small edit with catastrophic, data-dependent behavior because the code dangles only when the temporary evaluates to the smaller value. Passing function call results introduces identical hazards depending on whether those functions return by value or by reference. Sutter quips that Andrei could not be roasted in person because his flight arrives the following morning, adding: “Andre is completely correct and has rightly been complaining about it for almost 20 years.”

Neil MacIntosh steps up to demonstrate the prototype lifetime profile analyzer on this exact case. He loads code containing an explicit implementation of `min` and two helper functions: `F2()` returning a reference to a static integer, and `F3()` returning an integer by value. - When calling `min(x, y)`, the standard compiler and the analyzer both accept it as safe because both variables outlive the expression. - When modified to `min(x, y + 1)`, every existing C++ compiler cleanly builds the code without a warning, but the lifetime tool flags it, warning that the result is invalidated by a temporary allocation (`allotemp`). - When tested with `min(x, F2())`, the tool accepts the code because an unparameterized function returning a reference is assumed to point to static storage. - When tested with `min(x, F3())`, the tool flags the temporary returned by value, proving it catches the bug automatically.

---

[01:23:45](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=5025s) Cracking a 40-Year-Old Nut Without Garbage Collection

Sutter reflects on the corridor skepticism following Bjarne Stroustrup’s opening keynote. Attendees had muttered that distinguishing owners from pointers and tracking pointer targets sounded like magic, though they trusted Stroustrup does not say random things. Sutter emphasizes that between Stroustrup's talk and this one, they have laid out a concrete framework to finally solve memory and lifetime bugs in C++ without resorting to heavy annotations, a garbage collector, or an entirely new programming language: “And I hope that this has been useful to you, because we are very hopeful this may crack a 40-year nut in solving lifetime problems safely, without heavy annotation, in C++, as it's largely written today, without relying on things like garbage collection, or heavy annotation, or a new language.”

```mermaid flowchart LR accTitle: Core Guidelines Lifetime Safety Approach accDescr: Pipeline contrasting traditional safety approaches with the C++ Core Guidelines static analysis profile.

subgraph Traditional["Traditional Approaches"] GC["Garbage Collection (Runtime Overhead)"] NewLang["Entirely New Languages"] HeavyAnnot["Pervasive Source Annotations"] end

subgraph CoreGuidelines["Core Guidelines Approach"] Types["Standard Types + GSL"] Ownership["Owner vs. Pointer Distinction"] StaticAnalysis["Compile-Time Profile Analysis"] end

CoreGuidelines --> Result["Safety by Default (Zero Runtime Overhead)"] ```

Sutter explains that while the 1.0 formal design specification is being polished with implementer rules, he had pushed the version 0.9.1 design paper earlier that day into the `CppCoreGuidelines` repository under the `/docs` directory. He then asks Neil MacIntosh about release timelines for the tooling inside Visual Studio: - Prototype releases for the **Bounds** and **Type** safety profile checkers will drop alongside the Visual Studio 2015 Update 1 timeframe (within one to two months). - A prototype of the **Lifetime** checker will follow shortly thereafter, targeting the coming winter.

---

[01:25:35](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=5135s) Simplifying C++: Why Smart Pointers Don't Implicitly Convert to `T*`

Sutter pivots to the broader design philosophy: safety does not just prevent crashes; it actively simplifies the language and libraries. To demonstrate this, he tackles a question he has answered at dozens of companies: why `std::unique_ptr` and `std::shared_ptr` require explicit `.get()` calls instead of providing an implicit `operator T*()`.

Every engineer initially tries to write an implicit conversion operator, only to run into two severe failure modes: 1. **Accidental Pointer Arithmetic**: An implicit `T*` conversion allows code like `p + 42` to silently compile, treating a single managed object as an array and creating a wild, out-of-bounds pointer. 2. **Dangling Raw Pointers**: Code can quietly extract a raw pointer without realizing it is tied to a temporary smart pointer whose lifetime ends immediately, leaving a dangling pointer behind.

Under a profile-enforced subset of C++ where bounds violations and dangling are statically prohibited, these failure modes vanish. Sutter shares an intriguing conjecture: “Well, if we do have a solution where we have a subset of C++ where there are no dangling, and no bounds violations. There should be no reason that smart pointers can't just convert to T-Star.” Safety allows library abstractions to dispense with protective boilerplate and expose cleaner, more natural interfaces.

---

[01:27:06](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=5226s) Call to Action and Related Talks

Sutter emphasizes that these guidelines, types, and analysis rules are anchored in standard ISO C++. The patterns discovered in the Guideline Support Library (GSL) will be fed back into the ISO C++ standards committee to be standardized or refined into even better language features. He highlights that the overarching achievement is providing type safety, bounds safety, and lifetime safety almost entirely at compile time with zero runtime overhead.

He directs attendees to upcoming CppCon 2015 sessions that continue these safety and modernization themes: - **Gabriel Dos Reis**: Modules (limiting macro leakage and header overhead) and Contracts. - **Neil MacIntosh**: Two sessions on static analysis, detailing `array_view` and `string_view`. - **Eric Niebler**: Plenary keynote on Ranges, which fundamentally addresses bounds safety by pairing data with its bounds rather than relying on unconstrained raw iterators.

Sutter closes by thanking the community for the explosive global interest in the GitHub repository, issuing an earnest plea for contributors: “Please help us make it better. We need help. We can't do it ourselves.”

## Notable quotes > [02:30](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=150s) Nobody tries to take anything away successfully from C++ programmers. — Herb Sutter

> [06:50](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=410s) Everybody, every program in any language eventually calls unsafe code. — Herb Sutter

> [27:50](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=1670s) Raw pointers and references are just fine, and you really want to use them on the stack, especially as locals and especially as parameters, for parameter passing by reference, by pointer, because you want the speed and the leanness of just an address. — Herb Sutter

## Mentioned Herb Sutter, Bjarne Stroustrup, Neil MacIntosh, Eric Niebler, Gabriel Dos Reis, Andrei Alexandrescu, Detlef Vollmann, Guideline Support Library (GSL), CppCoreGuidelines, Visual Studio, Visual C++, ISO C++ Committee, Rust, Cyclone, CCured, SAL (Source Code Annotation Language), PREfast, PREfix, Stack Overflow, GitHub, Qt

## Video description http://www.cppcon.org — Presentation Slides, PDFs, Source Code and other presenter materials are available at: https://github.com/isocpp/CppCoreGuidelines/blob/master/talks/Sutter%20-%20CppCon%202015%20day%202%20plenary%20.pdf -- Modern C++ is clean, safe, and fast. It continues to deliver better and simpler features than were previously available. How can we help most C++ programmers get the improved features by default, so that our code is better by upgrading to take full advantage of modern C++?

This talk continues from Bjarne Stroustrup’s Monday keynote to describe how the open C++ core guidelines project is the cornerstone of a broader effort to promote modern C++. Using the same cross-platform effort Stroustrup described, this talk shows how to enable programmers write production-quality C++ code that is, among other benefits, type-safe and memory-safe by default – free of most classes of type errors, bounds errors, and leak/dangling errors – and still exemplary, efficient, and fully modern C++.

Background reading: Bjarne Stroustrup’s 2005 “SELL” paper, “A rationale for semantically enhanced library languages," is important background for this talk. -- Videos Filmed & Edited by Bash Films: http://www.BashFilms.com

Work at Hudson River Trading (HRT): https://tinyurl.com/safxfctf

[Source](https://www.youtube.com/watch?v=hEx5DNLWGgA)

中文简报

Herb Sutter 推出 C++ 安全配置文件以消除内存错误 局部静态分析规则无需垃圾回收、运行时开销或类似 Rust 的注解,即可消除 C++ 代码中的内存与边界缺陷。 Herb Sutter 提出了一套实用蓝图,旨在无需垃圾回收或冗长源码注解的前提下,在现代 C++ 中实现完整的类型、边界及生命周期安全。通过结合现代语言特性与准则支持库(GSL)——引入诸如 `array_view`、`string_view` 和 `not_null` 等抽象——开发者可以在保留该语言零成本性能的同时强制执行安全默认设置。本次演讲介绍了局部、流敏感的静态分析规则,这些规则将资源所有者与非拥有型指针区分开来,并在各作用域及赋值操作中跟踪被指对象的失效情况。在 Neil MacIntosh 的现场工具演示支持下,Sutter 展示了生命周期安全配置文件如何捕获长期存在的 C++ 缺陷,包括类似 `std::min` 等函数中的悬挂临时引用问题。 开源的 C++ Core Guidelines 项目和准则支持库(GSL)旨在通过静态强制执行的配置文件,在无需垃圾回收或繁重注解的情况下,使 C++ 默认具备类型安全和内存安全。 (2:30) 完整的内存与类型安全被划分为三大支柱——类型安全、边界安全和生命周期安全——且选择退出机制严格限制在局部,以便在源代码中提供明确的审计点。 (5:00) 边界安全强制要求原始指针仅引用单个对象而非数组,使用如 array_view 和 string_view 等统一的跨度抽象来替代“指针+长度”的成对参数。 (13:30) 生命周期安全配置文件采用过程内静态分析和所有权跟踪,在每个函数局部消除释放后使用(use-after-free)和悬挂指针缺陷,从而在无需全程序编译时间的情况下实现全程序安全。 (30:00)

Reading summary

Herb Sutter argues that C++ can achieve full type, bounds, and lifetime safety by default without garbage collection, pervasive annotations, or breaking backward compatibility. This is accomplished by combining modern abstractions in the Guideline Support Library with compiler-enforced static analysis profiles that restrict unsafe operations while preserving explicit, granular escape hatches for low-level tasks. Language Evolution via Subsetting and Fine-Grained Suppression (5:00) Sutter characterizes the C++ Core Guidelines initiative as a strategy of "supersetting, then subsetting." C++ historically expanded its capabilities by adding safe modern idioms alongside legacy features such as manual memory management, raw loops, and unchecked casts. The Core Guidelines seek to complete this modern toolbox so that compilers and static analysis checkers can turn off unsafe constructs by default. Sutter emphasizes that code compiled within the safe profile subset is guaranteed never to be the root cause of type or memory safety failures, assuming precompiled external libraries are trusted. Enforcing Type and Bounds Safety with GSL (10:00) To establish type safety, the Guideline Support Library (GSL) introduces explicit vocabulary types like `gsl::byte`—preventing raw uninterpreted memory from being treated as text—and relies on `std::variant` to replace C-style unions, while checkers disallow uninitialized variables and unsafe casts. For bounds safety, disjoint raw pointers and length parameters are replaced with unified non-owning abstractions such as `gsl::array_view` and `gsl::string_view`. These view types eliminate pointer arithmetic, prevent array-to-pointer decay, and catch buffer overflows while enabling interoperability across diverse container and custom string implementations. Sutter notes that nearly all GSL rules operate strictly at compile time with zero runtime overhead, with only `array_view` dynamic bounds checks and `not_null` assertions introducing minor, minimal runtime verification. Local Tracking of Owners and Pointers (27:50) To address the longstanding challenge of lifetime and memory safety without resorting to unscalable whole-program static analysis, the Lifetime Safety profile categorizes all indirections into two conceptual roles: Owners (such as smart pointers and standard containers that manage resource destruction) and Pointers (non-owning observers like raw pointers, references, and iterators). Per-function static analysis tracks each pointer's compile-time "points-to" set to ensure it never outlives the object it references. For heap-allocated entities without source code names, the model introduces a synthetic designation ($S'$) that ties the heap object to its owner $S$. The analyzer relies on language-level `const` semantics to track modifications: any non-const operation or reassignment of an owner immediately invalidates all dependent pointers, enabling compilers to flag subsequent dereferences as errors at compile time. Inter-Procedural Contracts and Parameter Lifetime Semantics (57:30) Crossing function call boundaries introduces potential invalidation through side effects and reentrancy. The analysis resolves this by dividing responsibilities: callees assume that incoming pointer parameters remain valid for the duration of the call, while callers are required to prove that the pointee cannot be destroyed during the call. For instance, passing a raw pointer extracted from a global `shared_ptr` requires creating a local copy at the caller site to ensure downstream side effects cannot drop the reference count to zero. When functions return pointers or references, the analyzer relies on parameter value categories to deduce lifetimes without heavy annotations. It avoids assuming returns originate from rvalue reference parameters (`Owner&&`), which bind to expiring temporaries. It also catches defects in functions like `std::min`, which trigger subtle dangling reference bugs when refactored to take temporary arguments. Sutter concludes that compile-time profile checking offers a viable solution to a 40-year-old safety challenge in native systems programming by automating best practices already latent in modern C++. However, complete program guarantees remain bounded by practical limits: third-party binary libraries must be independently audited, and static rules accept occasional false positives to maintain soundness while ongoing work refines analyzer heuristics.

精炼阅读摘要

Herb Sutter在CppCon 2015的主题演讲中提出,C++ Core Guidelines与指导支持库(GSL)旨在通过“先超集扩展、再子集约束”的演进策略,在保留底层直接访问能力的同时,于编译期实现类型安全、边界安全和生命周期安全默认化。 演进哲学:专业工具与细粒度安全抑制 (2:30) Sutter将C++的底层特性比作专业厨师手中的锋利刀具:开发者在处理极端性能场景时确实需要打开“刀具抽屉”,但语言与工具的默认状态不应将不安全操作暴露在外。C++历史上一贯采用超集演进,即不断扩充高阶安全抽象而不破坏向后兼容;当现代替代物完备后,编译器与静态检查器便可默认关闭不安全特性。Sutter指出,在安全子集下编译的代码永远不会是类型或内存安全错误的根本原因,除非开发者显式启用底层操作。针对此类逃生通道,Sutter主张摒弃类似Rust或C#的宽泛unsafe块,因为粗粒度标记在放行某项必要操作时,会连带掩盖同一作用域内的联合体误用或类型双关等其他致命缺陷;Core Guidelines推行的是行级别的细粒度规则抑制(如[[gsl::suppress(bounds)]]),以保持严密且可自审计的安全边界。 三大安全支柱:类型、边界与GSL轻量抽象 (10:00) Core Guidelines确立了三大核心防护支柱:类型安全、边界安全与生命周期安全。在类型安全方面,体系通过引入显式语义类型(如以gsl::byte区分裸内存与字符文本、以std::variant取代易错的C风格union)并强制变量初始化,系统性封堵未定义行为。在边界安全方面,演讲强调消除原始指针算术以及由于“指针+长度”分离导致的偏移与越界漏洞。GSL通过array_view与string_view统一连续内存的抽象访问,不仅避免针对不同容器重复生成模板代码,还为各类自定义字符串类型提供了统一的转换接口。Sutter说明,除array_view跟踪边界和not_null断言有效性带来极小运行时开销外,GSL规则绝大多数均通过编译期静态检查实现零运行时开销,从而杜绝以性能为由拒绝采用安全类型的借口。 破解四十年难题:所有者与指针的局部静态分析 (24:25) 原生语言中的生命周期缺陷(内存泄漏、重复释放与悬垂指针)困扰了业界四十年,过往依赖垃圾回收、全程序分析或侵入式生命周期标注的方法均无法完全契合C++的零开销与规模化需求。Sutter指出,Core Guidelines并没有发明新语法,而是将现有间接访问归纳为“所有者(Owner)”与“指针(Pointer)”两大概念:所有者(如unique_ptr、容器或owner<T*>)负责确定性销毁;指针(如原始指针、引用、迭代器及view)仅作为借用观察者,其存活期绝不可超过目标对象。静态检查引擎依托按函数隔离的局部流敏感分析,在遇到堆对象时采用合成标识(如S')追踪其归属的所有者,并结合C++语言原生的const修饰符准确判定所有者是否发生变动。该机制无需昂贵的全程序图构建与运行时插桩,即能在局部编译阶段精确拦截栈变量逃逸、悬垂引用以及跨多层间接访问的失效问题。 跨函数契约、别名失效与调用安全边界 (57:30) 在跨函数交互中,体系通过明确的主调方与被调方契约保障生命周期延续。被调方默认假定传入的原始指针在调用期间合法有效,而主调方必须确保指针背后的对象在整个调用链执行过程中不会被意外销毁。Sutter指出,若直接将全局智能指针的裸指针(如gsp.get())传递给下游函数,一旦下游产生副作用篡改全局状态,将直接导致悬垂;安全模式要求在调用树顶端先按值复制一份局部shared_ptr以固定引用计数,防止重入或间接别名导致提前析构。此外,针对函数返回指针的场景,静态分析基于C++右值模型建立推导规则:区分弱右值磁铁(const引用,可绑定持久对象)与强右值磁铁(右值引用,专用于临时对象或移出状态),严禁默认假定返回值关联到即将消亡的强右值参数,从而防止类似std::min混用左值与临时值引发的数据依赖型悬垂缺陷。 Sutter总结指出,安全检查机制并非限制表达能力,反而在消除悬垂和越界顾虑后使库接口得以省去冗余样板代码(如探索智能指针安全隐式转换的可行性)。不过该体系的防护范围局限于开发者直接控制且经检查器重新编译的代码;未重新编译的第三方二进制库仍属于外部不安全信任域,需要独立审计与验证。

Detailed notes — zh

## Background 该视频由 CppCon 官方频道于 2015 年 9 月 24 日发布在 YouTube 平台。页面简介说明,本场讲座是 Herb Sutter 接续前一天 Bjarne Stroustrup 主题演讲的深度报告,旨在探讨如何依托开源的 C++ Core Guidelines 项目与 GSL 准则支持库,通过跨平台工具链在默认情况下使现代 C++ 达到类型安全与内存安全,在杜绝大部分类型、越界与悬垂/泄漏错误的同时保持极致性能。 ## TL;DR Herb Sutter 深入阐述了 C++ Core Guidelines 的落地策略,即通过“先超集、后子集”为不安全操作提供安全替代品,并通过三大安全 Profile(边界、类型与生命周期)在默认编译中消除未定义行为的根因。演讲重点介绍了用于替代裸指针加长度的 array_view 与 string_view 抽象,并通过细粒度规则压制取代粗糙的 unsafe 块。随后,Sutter 详解了困扰工业界 40 年的生命周期安全解法:将所有间接寻址抽象为“所有者(Owner)”与“指针(Pointer)”,在单函数局部静态分析中借助 points-to 列表与虚构别名追踪失效时机,实现零运行时开销、低注解负担的编译期悬垂指针与临时对象析构拦截,并由微软工程师 Neil Macintosh 现场进行了原型诊断演示。 ## Atmosphere & style 整场演讲氛围热烈且极具感染力,Herb Sutter 既有顶级语言设计者的宏大视野,又充满技术布道师的风趣与亲和力。他巧妙运用“大厨的锋利刀具抽屉”、“叔叔的皮卡后斗”等生动比喻化解深奥的概念,台下不时爆发心领神会的会心笑声与热烈掌声。与现场助手 Neil Macintosh 的实机演示配合默契,加上穿插与台下 Gabriel Dos Reis 等老友的幽默互动,让这场直面 C++ 数十年内存安全顽疾的深度技术剖析显得既严谨扎实又充满振奋人心的开拓感。 ## Bulletin - [00:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=0s) C++ 核心准则(C++ Core Guidelines)开源项目发布仅数天即登顶 GitHub 全球周趋势榜第一,关注度超越同期发布的 iOS 9 样例及 XcodeGhost 恶意软件项目。 - [08:30](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=510s) 核心准则确立了首发的三大安全 Profile(边界安全、类型安全、生命周期安全),通过“先超集、后子集”策略在完全保留 C++ 底层控制力的同时,使现代安全写法成为编译默认选项。 - [30:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=1800s) 生命周期安全分析体系完全基于函数级局部编译规则构建,零运行时开销、零垃圾回收(GC),通过抽象“所有者(Owner)”与“指针(Pointer)”模型静态消除悬垂指针和内存泄漏。 - [01:20:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=4800s) 微软展示了基于 Visual Studio 的静态分析原型,成功静态捕获困扰 C++ 近二十年的 std::min 临时对象悬垂引用、迭代器跨容器混用以及全局智能指针重入失效等典型缺陷。 ## Key takeaways - [05:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=300s) 安全策略采用“先超集,后子集”:语言先提供现代安全替代品形成超集,再借助静态检查规则在默认情况下关闭危险的旧特性,无需剥夺底层的直接内存操作能力。 - [05:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=300s) 安全保证具有明确的根本原因(Root Cause)边界:在开启安全子集后重编译的代码,自身绝不会成为类型或内存错误的根因,开发者只需将精力集中在显式退选的 unsafe 代码段上。 - [10:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=600s) 类型安全 Profile 旨在彻底杜绝非法类型双关与未定义转换,推行独立的 gsl::byte、以 std::variant 替代 raw union,并禁用裸 reinterpret_cast 与未初始化变量。 - [13:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=780s) 边界安全以 array_view 和 string_view 作为核心抽象替代“指针加长度”反模式,全面禁止裸指针算术与数组退化,在设计上追求零函数调用开销与零静态分析能力退化。 - [20:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=1200s) 摒弃粗粒度的整块 unsafe 关键字,改用细粒度的规则压制语法(如 [[suppress(bounds)]]),避免开发者因微小的底层操作而意外关闭对类型双关和生命周期错误的防护。 - [21:40](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=1300s) array_view 与 string_view 作为通用词汇类型,能够统一吸收不同连续内存容器和多厂商自定义字符串,彻底消除接口重载壁垒与不必要的跨库数据拷贝。 - [24:16](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=1456s) 分析体系拒绝了全程序静态分析、重度代码注解语法(如 Rust/Cyclone)与运行时垃圾回收,坚持利用 C++ 原生作用域、生命周期与 const 语义进行高效的局部静态推导。 - [27:32](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=1652s) 静态生命周期模型将所有间接寻址统一抽象为两大概念:负责资源生命周期且杜绝泄漏的“所有者(Owner)”,以及负责只读观察但可能失效的广义“指针(Pointer)”。 - [40:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=2400s) 静态分析器通过单步跳跃与虚拟符号(如 S')追踪所有者与借用指针的绑定关系,当所有者发生析构或非 const 变异(如 reset 或 vector 扩容)时,自动判定下游所有借用指针失效。 - [01:00:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=3600s) 智能指针在正确性维度的头号陷阱是因重入导致的对象提前销毁:在深层调用或多线程环境中,必须先在调用树顶层持有局部的 shared_ptr 副本锁定生命周期,再向下传递裸指针。 - [01:10:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=4200s) 静态分析模型能够将迭代器等同于非拥有型“指针”进行校验,在编译期精准拦截 std::map::insert 等跨不同容器混用迭代器范围的隐蔽逻辑 Bug。 - [01:12:15](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=4335s) 函数返回指针的生命周期采用零注解推导规则:默认假设返回值派生自输入参数集合,仅在无指针/拥有者参数时默认为静态生命周期,只有在契约冲突时才需显式标注。 - [01:20:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=4800s) 通过区分弱磁铁(const Owner&)与强磁铁(Owner&&),静态检查工具彻底解决了 Andrei Alexandrescu 提出的 std::min 传入临时对象导致数据依赖型悬垂引用的 20 年难题。 - [01:25:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=5100s) 安全性能够反哺语言本身的极简化:当静态规则彻底杜绝越界与悬垂后,智能指针隐式转换为裸指针等更自然的语法将不再具备破坏性,从而减轻代码冗余度。 ## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=0s) 开场:GitHub 趋势第一与让现代 C++ 成为默认 Herb Sutter 走上讲台,回顾了前一天 Bjarne Stroustrup 关于 C++ 核心准则(C++ Core Guidelines)的主题演讲,并现场调查有多少人听完后打算回公司付诸实践。看到台下举起的大片手臂,他非常振奋。Herb Sutter 展示了一张当天清晨约 6 点抓取的 GitHub 全球近一周趋势榜截图:这个刚发布数天的 C++ Core Guidelines 项目已经直接冲到了全球趋势排行榜的第 1 名。 ```mermaid flowchart TD accTitle: GitHub 热门代码库趋势排名对比 accDescr: 对比 CppCoreGuidelines 与同周热门的 XcodeGhost 以及 iOS 9 样例项目的热度排行 subgraph GitHubTrending["GitHub 近一周趋势排行"] Rank1["No.1: CppCoreGuidelines<br/>(C++ 核心准则与 GSL)"] Rank3["No.3: XcodeGhost<br/>(首个 iOS 恶意软件源码)"] Rank4["No.4: iOS 9 Sampler<br/>(苹果最新系统示例工程)"] end Rank1 -.->|"关注度超越同周两项热点总和"| Rank3 Rank1 -.->|"关注度超越同周两项热点总和"| Rank4 ``` 台下当时坐着大几百人、近千名开发者,但这种全球性的关注度显然来自整个技术社区。Herb Sutter 调侃道,同周科技媒体(如 CNN 等)铺天盖地报道的头条是史上首个 iOS 恶意软件“XcodeGhost”事件,同时苹果也刚发布了 iOS 9 正式版,因此相关项目分别排在趋势榜第 3 和第 4 位;然而这两大热门项目加起来的热度,都比不上开发者对 C++ 编码规范的热情。 回到 C++ 代码本身,Herb Sutter 指出,随着现代标准演进,右侧现代 C++ 的优秀写法(如范围 for、智能指针等)在工业界正被越来越多团队采纳;但左侧充斥着裸指针、`new`/`delete`、传统循环的旧代码依然能够完美编译运行。C++ 的向后兼容性固然无与伦比,但在旧特性依然存活的前提下,如何让优秀、现代、安全的写法在默认状态下生效,成了本次演讲的核心议题。正如 Herb Sutter 所言: > “Nobody's trying to take anything away from you. Nobody tries to take anything away successfully from C++ programmers. But you just open the hood and then you can use them.” 他用刀具抽屉打了个生动的比方:程序员就像大厨,使用锋利的刀具是本职工作,我们绝对需要刀具;但没人希望刀具抽屉默认是完全敞开的。最理想的状态是:日常默认使用安全、简洁的现代化工具,只有在真正需要触碰危险底层时,才手动去拉开那个危险抽屉。 - **GitHub 趋势榜**:C++ Core Guidelines 仓库上线仅数天便登顶全球周榜第一,超越同期热点 XcodeGhost(第 3 名)与 iOS 9 Sampler(第 4 名)。 - **演讲定位**:本场讲座是 Bjarne Stroustrup 主题演讲的“第二部分”(Part 2),侧重深入探讨那些在走廊和前夜讨论中被频繁问及的技术细节。 - **项目成熟度与生态**:Core Guidelines 当时处于 0.6 版本状态;配套的准则支持库(Guideline Support Library, GSL)已有跨平台/跨编译器的参考实现,微软及开源社区正全力开发静态检查工具(Checker tools)。 - **致谢贡献者**:Herb Sutter 特别邀请台下参与编写准则与工具的贡献者起立接受全场致意,强调该工程是一项庞大的跨团队协作成果。 --- ### [05:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=300s) 超集、安全子集与类型及内存安全保证 Bjarne Stroustrup 提出的演进策略是“先超集,后子集”(Supersetting then subsetting)。C++ 一直非常擅长做超集,语言通过引入新标准库、新特性来提供现代化工具;但如果想要彻底淘汰不安全的部分,就必须借助核心准则为每一个不安全特性提供安全替代品。当这套工具箱补齐之后,编译器和检查器便可以在默认情况下关闭不安全的旧特性。旧特性依然留在语言中,没有任何人强行剥夺它们,但开发者必须显式“揭开引擎盖”才能访问。 Herb Sutter 明确指出,本场演讲通篇聚焦于传统上被认为与 C++ 绝缘的领域:**类型与内存安全(Type and Memory Safety)**。 | 安全维度 | 核心定义与防护目标 | 常见违规场景 | |:--- |:--- |:--- | | **类型安全 (Type Safety)** | 不对类型进行非法双关(Type punning),绝不把类型 $T$ 强行当成毫无关联的类型 $U$ 使用 | 危险的 `reinterpret_cast`、C 风格强转、未受保护的联合体(Union) | | **边界安全 (Bounds Safety)** | 内存访问绝对不能超出分配缓冲区的合法范围 | 数组越界读写、缓冲区溢出 | | **生命周期安全 (Lifetime Safety)** | 绝不访问已经失效或被释放的内存分配区域 | 悬垂指针(Dangling pointer)、释放后使用(Use-after-free)、内存泄漏 | 关于保障边界,Herb Sutter 给出了严谨且坦诚的定义:启用安全规则后重新编译的 C++ 代码,自身绝不会成为类型或内存安全错误的**根本原因(Root Cause)**,除非开发者显式声明退选安全检查(例如标记 unsafe)。这项保证具有以下现实边界: ```mermaid flowchart LR accTitle: 代码安全边界与根因隔离 accDescr: 展示安全编译代码、显式退选代码与第三方闭源库之间的安全责任划分 subgraph UserCode["用户控制的代码(需重编译)"] direction TB SafeCode["安全子集编译代码<br/>(零类型/边界/生命周期根因错误)"] OptOutCode["显式退选代码 (Unsafe)<br/>(代码内明确标记审计点)"] end subgraph External["外部环境"] ThirdParty["第三方库 / 未重编译代码<br/>(基于信任假设,不提供自动化保证)"] end SafeCode -->|"调用"| OptOutCode SafeCode -->|"调用"| ThirdParty ThirdParty -.->|"产生错误可能波及"| SafeCode ``` - **重编译范围**:安全保证仅针对开发者重新编译的代码生效。对于未重编译的第三方库,假定用户对其充分信任;若第三方库内部破坏了内存,报错可能在安全代码区崩溃,但根本原因不在安全代码中。 - **免去全量安全审计**:只要代码符合准则且无报错通过编译,团队便无需花费精力对这些逻辑做逐行安全审计;开发者只需审查源代码中那些明确标记为“请信任我(Trust me)”的退选点。 - **Verifiable 全程序证明的现实真相**:针对某些语言宣称的“可验证(Verifiable)整程序保证”,Herb Sutter 指出在工业界这几乎是乌托邦。任何语言的程序最终都要调用底层系统或非安全代码,一旦非安全代码出错,整系统都会崩溃;C++ 核心准则提供的保障逻辑与所有所谓“安全语言”在本质上完全一致,但对待外部依赖的态度更加坦率透明。 --- ### [08:30](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=510s) 核心安全 Profile 与类型安全概览 为了让规则模块化落地,核心准则引入了 **Profile(配置文件/规则集)** 的概念。一个 Profile 即指一组紧密结合、共同实现某项特定安全保证的规则集合。Herb Sutter 明确了当前的实施路线图: - **当前首发的三大 Profile**: 1. **边界安全(Bounds Safety)** 2. **生命周期安全(Lifetime Safety)** 3. **类型安全(Type Safety)** - **未来规划中的 Profile**:安全算术运算(Safe Arithmetic)、并发安全(Concurrency Safety)、以及部分受限嵌入式/硬实时系统高度关注的 `noexcept` Profile 和禁止动态分配的 `noalloc` Profile。 关于类型安全 Profile,其核心出发点非常纯粹:禁止将内存中的某块存储位置错误地当作另一种不兼容的类型来访问。Herb Sutter 展示了类型安全总结路线图的开篇,并准备逐步拆解如何消除类型双关、裸类型转换与非法别名等遗留隐患。关于整个安全体系的承诺,Herb Sutter 掷地有声地总结道: > “C++ code compiled in the safe subset, so with these particular parts of guidelines turned on, is never the root cause of type or memory safety errors unless you opened the hood and explicitly said unsafe me, go and let me do something that I know is wrong.” ### [10:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=600s) 类型安全 Profile 与语言超集/子集策略 Herb Sutter 阐述了当前正在推行的核心策略:先构建“超集”(superset),再在超集之上划分“安全子集”(subset)。引入超集的目的绝非为了炫技,而是为了明确指出哪些不安全的部分必须被禁止,并为其补充尚未进入标准的现代安全替代方案。Sutter 指出,C++11 已经在内存和类型安全上完成了大约 60% 到 70% 的工作,但那并不是一次有组织的行动来消灭每一个未定义行为(Undefined Behavior)源头。当下的目标正是要系统性地为每个不安全行为提供安全的替代品。 在类型安全(Type Safety)方面,绝大多数 C++ 代码本身已是类型安全的,但仍需补齐关键构件: - **独立的 `byte` 类型**:必须将原始字节与字符类型区分开来。尽管它们在底层拥有相同的比特位、大小和内存布局,但在类型系统中字符与内存字节有着本质区别。 - **替代 `union` 的 `std::variant`**:委员会当时正密集推进 variant 的标准化(预计很快在科纳 Kona 会议上达成一致),因此 Guidelines Support Library(GSL)的初始实现未自行发明重复的 variant,而是等待委员会直接标准化。 - **彻底禁止类型双关与不安全转换**:禁用未受检的 static downcast、`reinterpret_cast`、以及移除了 const 属性的 `const_cast`(除非为了封装调用遗留非 const 安全的第三方库并进行单点隔离)。 - **严格限制 C 语言风格类型转换**:C 风格转换会在底层级联尝试多达四种底层转换,新规则在语法上仍然允许写 C 风格转换,但只要它等价于被禁止的危险转换,编译器或分析器就必须报错或警告。 - **变量初始化与规避变长参数**:禁止在初始化前使用局部变量或成员变量,严禁使用 `union` 与 C 风格可变参数(`varargs`),因为这两者是类型双关(type punning)的高发区。针对“安全数学”(Safe Math)由于涉及运行时范围检查与截断开销,被独立划分为单独的 profile,未合入当前的零开销类型安全规范中。 ```mermaid flowchart TD accTitle: 类型安全 Profile 的检查与替换机制 accDescr: 展示危险类型操作如何被 GSL 和现代 C++ 特性安全替代 subgraph UNSAFE["被禁止的危险操作"] U1["reinterpret_cast / 未检查的 static downcast"] U2["C-style 强制类型转换 (隐式级联)"] U3["union / varargs (类型双关)"] U4["未初始化变量直接使用"] end subgraph PROFILE["Type Safety Profile 过滤"] P1["静态分析规则拦截"] end subgraph SAFE["标准与 GSL 安全替代方案"] S1["独立 gsl::byte (区分 raw memory 与 char)"] S2["std::variant (替代 raw union)"] S3["强制就地初始化 / RAII"] end U1 --> P1 U2 --> P1 U3 --> P1 U4 --> P1 P1 --> S1 P1 --> S2 P1 --> S3 ``` - 关于消除未定义行为的目标,Herb Sutter 强调:“Now C++11 got us most of the way. I'd say it's roughly 60-70% of the way to type of memory safety compared to C++ and IDA. But it wasn't a coordinated effort to drive out and provide alternatives for every source of undefined behavior and unsafety to give a safe alternative to it. That's what we're attempting to do.” - 关于字符与原始内存的类型界定,Sutter 解释道:“I know it's the same size and layout, I know it's the same bits, but in the type system the character is not the same as a byte, and it's useful to distinguish those two things.” --- ### [13:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=780s) 边界安全与 ArrayView、StringView 的抽象设计 谈及边界安全(Bounds Safety),首要目标是在有内存分配的情况下彻底杜绝越界访问。GSL 引入了 `array_view` 和 `string_view` 作为指针加长度(`pointer + length`)这一反模式的现代替代品。Sutter 解释了 GSL 版本与当时 Technical Specification(TS)版本的两处核心差异: 1. **读写属性**:TS 版本的 string view 通常是只读的,而 GSL 中的视图支持可读可写,因为实际业务中确实存在修改原缓冲区的需求。 2. **类型正交性**:GSL 中并不存在两套分裂的机制。`array_view` 是唯一的底层模板类型,`string_view` 仅仅是针对一维场景对 `array_view` 的模板别名(Alias Template),并调整了部分默认参数。Sutter 开玩笑称团队确实想不出多维字符串(multidimensional strings)有什么存在的实际意义。 Sutter 坦承,在整个 GSL 库的设计中,绝大多数安全检查都是在编译期由静态类型系统完成的,唯有 `array_view` 与 `not_null` 这两个类型会带来微小的运行时开销(runtime overhead)。开发团队正在竭尽全力将这些开销压降到最低,以确保开发者不会因为性能顾虑而拒绝采用安全组件。 建立起这一层类型抽象后,就可以正式推行边界安全的铁律:彻底抛弃裸指针算术(Pointer Arithmetic),禁止数组向指针退化(Array-to-pointer decay),并禁用不安全的 C 运行库函数。 - 关于指针语义的重新定义,Herb Sutter 总结道:“If you look at rule number one, what's that really saying? Another way of stating rule one is a pointer only points to one object. That's what pointers should do. That's what the vast majority of actual pointers in our code 'do' is referred to a single object.” - 关于运行时代价的坦白,Sutter 直言:“And I want to point out that in the entire guideline support library, array view is the primary type. The only other one is not null that incur any runtime overhead. Everything else is done statically at compile time. But we do want full disclosure. ArrayView and then NotNull do incur some runtime overhead.” --- ### [15:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=900s) 从 SAL 注解到类型系统:ArrayView 的实战重构与调用统一 为了确保新规范立足于现实工业代码,GSL 并不是凭空构想的,而是从网络协议处理器、底层操作系统内核例程、字符串处理及传输模块等真实场景代码中反复提炼与迭代的结果。Sutter 承认团队在探索庞大的设计空间时走过许多弯路(false paths),最终才收敛到了这几个紧密协作的核心类型上。 Sutter 现场对比了真实代码的演进: - **旧模式与 SAL 注解**:在 C/C++ 遗留代码中,常见通过 Microsoft SAL(源码标注语言)标注 `things` 指针与 `count` 长度具有绑定关系。虽然传统编译器完全无法感知越界,但静态分析器可以通过 SAL 捕捉诸如 `<=` 错写为 `<`、或者 `memcpy` 时误把元素计数当作字节数传递等低级错误。 - **ArrayView 现代化重构**:右侧代码直接用 `array_view<T>` 替代裸指针与长度配对。`array_view` 支持与指针相同的多层 const 语义——`array_view<const T>` 代表可以通过它肆意读取但不可写入底层对象,而将视图本身标记为 `const` 则意味着不能将其重定向到其他内存切片。 - **调用侧的无缝兼容**:基于 `array_view`,代码可以直接配合基于范围的 for 循环(Range-based for loop),或直接传入像 Eric Niebler ranges 设计思想那样的现代 `std::copy`,即便对象不是 POD 也能安全处理。对于遗留调用方,C++11 的统一初始化(Uniform Initialization)提供了极佳的迁移路径:老代码传递 `(ptr, count)` 处只需外层加一对大括号 `{ptr, count}`,编译器就能直接就地构造出 `array_view`。 Sutter 强调,这次演进的一个底线目标是“绝不在静态分析能力上产生任何功能退化”。使用 SAL 等编译器扩展注解所能捕获的缺陷,在使用 `array_view` 后依然能够被全部保留。更关键的是,`array_view` 成为了被人类与编译器工具链共同理解的“通用词汇类型”(vocabulary type),大量的边界检验被直接下放给编译期类型系统,工具不仅无需在宏与注解堆中吃力推断,分析负担大为减轻,而且目标是实现“零调用开销”(zero call overhead)。 - 关于调用旧代码时利用统一初始化进行低侵入迁移,Herb Sutter 指出:“If you want to call the API explicitly the way you used to, like for instance, the old API you might have called with pointer, length, let's say all your call sites had that. Well, notice the second call at the bottom, generally, most of the time, especially if they were in that order, pointer comma length, just put braces around them. We have the nice uniform initialization rules in C++11.” - 针对消除静态分析退化并提升类型系统作用,Sutter 强调:“So we have no regressions from the analysis, and the analyzer has less work to do, because we're using the type system and the target is zero call overhead.” ### [20:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=1200s) 精细化压制规则与 `array_view` 的开销优化 Herb Sutter 谈到了 `array_view` 在索引使用时的边界检查机制。编译器会尝试将边界检查外提(hoist),但在将 `array_view` 作为函数参数传递时,其开销绝不能高于传统的“原始指针加长度”传递方式,团队正致力于从 ABI 和调用约定层面实现这一目标。针对确实需要编写不安全代码的场景,Herb Sutter 讨论了如何退出边界安全(bounds safety)检查。在许多其他语言中,人们习惯使用像 `unsafe` 这样的关键字来包裹整个代码块,但他指出自己在设计初期就收到了很多反馈,认为粗粒度的 `unsafe` 并不理想: > “And several people gave me very good feedback that, you know, that's not very satisfying. And the reason is, is 'cause it lets you do too much.” 如果仅用一个笼统的 `unsafe` 块,开发者原本可能只是想执行一个编译期无法验证但已知安全的边界操作,结果却可能不小心在块内引入了联合体(union)的错误使用或类型双关(type punning)等其他隐患。因此,C++ Core Guidelines 采取的方案是允许开发者显式压制特定的规则,例如通过属性指定 `suppress type.cast`、`suppress bounds` 或具体的规则编号。这种方式既清晰又具备自解释性,开发者明确告诉工具“在这行代码上请信任我”,即使压制了某类检查,其他潜在的编程错误依然能被工具捕捉。以调用底层的 `memcpy` 为例,开发者可以局部压制边界警告,同时借助 `array_view` 获取准确的元素字节大小,从而避免低级失误。 ```mermaid flowchart TD accTitle: 规则压制机制对比 accDescr: 粗粒度 unsafe 块与细粒度 GSL 规则压制的对比示意图 subgraph Coarse["粗粒度 unsafe(其他语言)"] U1["标记 unsafe 块"] --> U2["允许所有危险操作"] U2 --> U3["隐藏了意料之外的类型转换与未定义行为"] end subgraph Fine["细粒度规则压制(C++ Core Guidelines)"] F1["精细标注: [[suppress(bounds)]]"] --> F2["仅豁免特定的边界检查"] F2 --> F3["持续检查联合体、生命周期与类型转换错误"] end ``` ### [21:40](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=1300s) 消除容器重载壁垒:`array_view` 与 `string_view` 除了安全性,`array_view` 和 `string_view` 还带来了极大的编码便利。在日常开发中,若想编写一个能处理连续内存(无论存放在 `std::vector`、原生数组还是指针加长度中)的函数,以往要么编写函数模板并为每种容器实例化,要么提供多个重载。而现代 C++ 的做法是直接让函数接受一个 `array_view<int>`,便能统一接收 `std::vector`、原生数组或自定义连续容器。 对于字符串类型,这一抽象的优势更为突出。Herb Sutter 在现场询问有多少观众在自己公司维护着自制的字符串类型,并笑称“别撒谎,没举手的只是害羞而已”。微软内部以及业界存在大量不同实现的字符串类型,幻灯片根本无法列全。如果每个接口都采用 `string_view`(或宽字符版本的 `wstring_view`),自定义字符串类型只需提供一个到 `string_view` 的类型转换操作符,就能无缝接入所有接受该视图的现代 C++ 接口,彻底摆脱跨库调用时字符串转换的泥潭。 - **边界安全配置文件(Bounds Safety Profile)核心规则**: - 核心目标是防止访问超出所分配内存的边界。 - 主要替代工具为 `array_view` 及其字符串特化别名 `string_view`。 - 禁止裸指针算术运算(pointer arithmetic)。 - 禁止越界的数组访问。 - 编译器与库设计正在解决调用约定(calling convention)与 ABI 问题,使参数传递开销与裸指针加长度完全持平,并消除冗余检查以降低采纳门槛。 ### [24:16](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=1456s) 进军生命周期安全:40 年难题与现有方案的缺陷 在总结完边界检查后,Herb Sutter 引出了生命周期配置文件(Lifetime Profile)。他先是用极简的口吻打趣说,生命周期安全其实只需要做好三件事:堆对象只释放一次以防内存泄漏;释放且仅释放一次以防堆损坏;永远不解引用指向已释放对象的指针以杜绝悬垂指针。台下随即爆发笑声与掌声,Sutter 笑着回应说: > “It's really easy to say what the requirements are. It's way harder to do it.” 原生语言(Native Languages)的内存安全是一个困扰了软件工程界 40 年的严峻问题。Herb Sutter 特别向 Bjarne Stroustrup 和 Neil McIntosh 致谢,表示自己与团队在过去一年中对此发起了极其认真的攻坚。回顾历史,许多先驱项目试图解决 C/C++ 的内存泄漏与悬垂问题,但它们选择的路径在现代高性能 C++ 中都存在局限: 1. **专注 C 语言的尝试(如 Cyclone、CCured)**:这些项目认为 C 语言更简单,但 Sutter 指出这只说对了一半——C 语言的语法虽小,但 C++ 代码比 C 代码更简单,因为 C++ 拥有更多丰富的语义信息。 2. **运行时垃圾回收(GC)**:许多语言引入 GC 或运行时开销,但这违背了 C++ 追求的零运行时开销(Zero Overhead)原则。 3. **全程序静态分析(Whole Program Analysis)**:以微软内部的工具为例,静态分析工具 Prefix 虽然能精确捕获全局错误,但面对超大型代码库时扩展性极差,Sutter 调侃其运行耗时“快要等到宇宙热寂(universe heat death)”;实际工程中只能退而求其次运行 Prefast 这种带有猜测和误报的轻量工具。 4. **繁重的注解语法(Extensive Annotation)**:包括 Rust 和 Cyclone 在内的语言依赖大量注解。Sutter 认为注解越多,越说明源代码本身缺失了信息;反之,注解越少,说明代码语义越完整。 5. **发明全新语言**:脱离了现有海量 C++ 代码库的生态。 ### [27:32](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=1652s) C++ 的内在语义优势与所有权抽象 Herb Sutter 指出,Core Guidelines 的生命周期方案之所以在概念上能够保持简洁,是因为充分利用了现代 C++ 语言本身内含的高价值信息:强作用域(Scope)、强生命周期(Lifetime)、强边界(Bounds)以及代码中既有的所有权(Ownership)结构。 针对 Reddit 社区在 Bjarne Stroustrup 演讲后出现的“指针是邪恶的”论调,Herb Sutter 明确予以反驳。指针本身并不是坏事,间接寻址(indirection)是必不可少的: > “Smart pointers are good. They encapsulate ownership. Of course, don't use them everywhere. Raw pointers and references are just fine, and you really want to use them on the stack, especially as locals and especially as parameters, for parameter passing by reference, by pointer, because you want the speed and the leanness of just an address.” 智能指针负责封装所有权,因此无需处处滥用;在栈上局部变量和函数传参时,裸指针与引用能提供直接传递内存地址的轻量与极致速度。关键在于如何在享受裸指针极致性能的同时,确保其不会发生悬垂。 生命周期安全配置文件的核心不是具体类型,而是抽象概念(Concepts)。Sutter 将所有能够产生间接寻址的机制划分为两大阵营:所有者(Owner)与指针(Pointer)。 ```mermaid flowchart TD accTitle: 生命周期核心概念划分 accDescr: 间接寻址分为拥有所有权的 Owner 与仅作观察的 Pointer Indirection["间接寻址 (Indirection)"] --> Owner["所有者 (Owner)"] Indirection --> Pointer["指针 (Pointer / 借用观察)"] subgraph OwnerGroup["所有者 Owner"] Owner --> O1["代表: unique_ptr, 容器等"] Owner --> O2["责任: 拥有资源,绝不发生悬垂"] Owner --> O3["保证: 正好释放资源一次,杜绝泄漏"] end subgraph PointerGroup["指针 Pointer (大写 P)"] Pointer --> P1["代表: 裸指针, 引用, iterator, range, array_view"] Pointer --> P2["角色: 观察者,不拥有生命周期"] Pointer --> P3["风险: 引用的对象可能被提前销毁导致悬垂"] end ``` - **所有者(Owner)**:如 `std::unique_ptr` 或拥有资源的容器。所有者掌管资源的分配与释放,它本身绝不会产生悬垂,其职责是确保对象在生命周期结束时被正确且唯一地清理一次。 - **指针(Pointer,广义概念)**:包括原始裸指针、C++ 引用、迭代器(iterator)、新的 Ranges 提议中的范围对以及 `array_view`。它们只是资源的借用者或观察者,不拥有生命周期。分析系统的核心任务就是跟踪这些“指针”,确保程序绝不会在它们所指的对象失效后继续解引用。 ### [30:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=1800s) 局部静态分析原则与所有权模型 Herb Sutter 阐明了本次讲解的核心目标:“确保你的指针不会悬垂(Make sure you don't dangle. This is the goal)。”在当前的 C++ 语义中,原始指针 `T*` 是模棱两可的,它无法表达自己到底是否拥有所指向的对象。正如 Bjarne Stroustrup 在前一天的演讲中所强调的,如果必须使用原始指针且无法改用 `std::unique_ptr` 等现代智能指针,开发者应该为其套上 `owner<T*>` 包装。`owner` 本质上只是一个类型别名(typedef/alias),但静态分析工具和检查规则能够识别它,从而强制要求开发者正确执行释放与析构逻辑。 在实现策略上,整个分析体系坚持采用**局部规则(Local rules)**,彻底放弃在大型项目中无法扩展的全程序分析(Whole-program analysis)。Herb Sutter 强调编译器必须保持“每次只编译一个函数”的经典模式。与此同时,除了具有极低运行时开销的 `array_view`(用于范围越界安全)和空指针检查包装器之外,这里提出的所有生命周期跟踪技术完全运行在编译期,没有任何运行时开销(Zero runtime overhead),不增加任何指令埋点、运行时分析或额外记账开销。 整个检查模型由两个基础动作构成: 1. **识别所有者(Owners)**:确保每个堆对象或资源都有明确的所有者,以实现“无泄漏(Leak freedom)”。 2. **跟踪指针的指向(Tracking points-to)**:对每一个非拥有型指针,在编译期静态推导其所指向的目标对象,以防止出现悬垂指针(Dangling pointers)。 为了避免给开发者带来沉重的代码注解负担,规则最大化地利用了语言已有语义,推导符合直觉的默认值。其核心追踪原则如下: - 指针跟踪其所指向的最外层对象(Enclosing object)。如果指针指向一个类内部的 `int` 成员或数组中的某个元素,静态分析不需要精细跟踪到该成员或下标,只需跟踪它所属的宿主对象或数组本身,因为两者的生命周期是完全等同的。 - 若指向堆对象,则跟踪该堆对象的所有者(Owner)。 - 在调用函数时,默认假设传入的所有指针参数相互独立,且在被调用函数的整个生命周期内均有效;当函数返回指针时,默认假定该指针派生自输入的某个参数(“99.99% 的情况下事实都是如此”)。 ```mermaid flowchart TD accTitle: 静态生命周期分析工作流 accDescr: 展示从函数级静态规则到所有者追踪与悬垂指针防护的推导结构 subgraph Inputs["输入与编译模型"] Code["函数源代码 (每次仅分析单个函数)"] Annotation["基本类型别名 (例如 owner<T*>)"] end subgraph Rules["编译期静态规则 (零运行时开销)"] TrackOwner["识别所有者对象 (实现无内存泄漏)"] TrackPointees["跟踪指针指向集合 (Points-to List)"] end subgraph ScopeLifetime["最外层生命周期绑定"] Outer["外层宿主对象 / 堆所有者"] Member["成员变量 / 数组元素"] Outer --- Member end subgraph Diagnostics["诊断目标"] Safe["安全解引用"] Error["精准报错: 变量已离开作用域/失效"] end Code --> TrackOwner Annotation --> TrackOwner Code --> TrackPointees TrackPointees --> Outer TrackOwner --> Diagnostics TrackPointees --> Diagnostics ``` - 编译期保证理念:Herb Sutter 指出,“只要每次编译一个函数并对整个程序的所有函数依次编译,就能得到全程序级别的安全保证(You get whole program guarantees by compiling the whole program, a function at a time)。” - 免除重度注解:大多数生命周期信息已内嵌于代码的变量作用域和类型系统中,分析器直接采用合理的默认行为。 --- ### [32:38](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=1958s) 作用域与悬垂指针热身示例 Herb Sutter 带领观众从函数内部的局部变量生命周期展开“第一幕热身”。他给出了一个刻意构造的作用域逃逸错误示例:在外部作用域声明了三个初始为空的指针 `p1`、`p2`、`p3`;接着进入一个局部内部作用域,在栈上分配了一个独立整型 `i`、一个包含整型成员的结构体 `s`,以及一个整型数组 `a`。外部的三个指针分别指向栈上的 `i`、结构体内部的整型成员以及数组中的某一元素。 只要执行流还在该内部作用域中,这三个指针都是绝对安全且有效的,因为栈变量尚未被销毁。然而,一旦代码执行越过内部作用域的大括号(标记为 Line A),变量 `a`、`s` 和 `i` 立即被析构,从而使得 `p1`、`p2` 和 `p3` 瞬间失效。如果后续代码尝试解引用或使用 `p1`,编译器或分析工具必须立即指出类似“`p1` 在 Line A 离开作用域时已被置为失效(`P was invalidated when I went out of scope at line A`)”的错误。 Herb 现场调查了观众席:“在场的 700 多人中,目前有多少人使用的工具能给出如此清晰易懂的报错?”台下只有大约 5 到 6 个人举手,比例仅占约 1%。Herb Sutter 表示,这类栈变量逃逸问题即便是人工审查也很容易识别,部分现有工具已能做到,但核心 guidelines 的目标是将这种诊断清晰度普及到每一个开发者的日常构建中。 --- ### [35:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=2100s) 取地址与指针层级追踪 在进入更复杂的场景前,Herb Sutter 解析了取地址操作在静态分析中的抽象方式。当定义一个整型 `int i` 并分别取得其引用 `int& ri = i;` 和指针 `int* pi = &i;` 时,尽管在 C++ 语言语义层面上引用有着别名与间接访问的双重特性,但在生命周期安全分析体系中,两者被统一抽象为大写的“指针(Indirection/Pointer)”。静态分析器直接在分析表中记录:`ri` 指向 `i`,`pi` 指向 `i`。 当出现多级指针时,分析系统并不需要维护深层的全局链路图,而是采用**单步跳跃(One hop)**模型: - 当执行 `int** ppi = &pi;` 时,分析器仅需静态记录 `ppi` 直接指向 `pi`。 - 如果需要查询 `ppi` 最终指向什么,只需按图索骥去分别查询 `pi` 的记录即可,分析器在记录 `ppi` 的属性时只需关注单层关系。 - 若出现智能指针如 `std::shared_ptr<int> s`(Herb 在幻灯片中用深色标记其为所有者),再执行 `auto ps = &s;`,分析器同样只需记录 `ps` 指向 `s`。虽然 `s` 本身是堆对象的所有者,但这一取指针操作的生命周期规则依然简单——`ps` 的有效性仅仅绑定在变量 `s` 本身的存活期上。 ```mermaid flowchart LR accTitle: 多级指针单步映射关系 accDescr: 展示多级指针如何通过局部的一级映射组合解析最终目标 subgraph PointerChain["单步指向链"] PPI["ppi (指向 pi)"] -->|"单步追踪"| PI["pi (指向 i)"] PI -->|"单步追踪"| I["栈对象 i"] end subgraph Reassignment["重定向追踪"] PI2["pi2 = *ppi"] -.->|"解引用展开: *ppi 即 pi"| PI PI2 -->|"拷贝 points-to 列表"| I end ``` --- ### [36:30](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=2190s) 解引用机制与别名 S-Prime 的诞生 接着,分析推进到解引用操作的规则推导。若在已有 `ppi -> pi -> i` 的前提下,代码执行 `int* pi2 = *ppi;`,分析器该如何推导 `pi2` 的生命周期?Herb 解释道:“因为我们静态地知道 `ppi` 指向 `pi`,所以表达式 `*ppi` 无非就是 `pi` 的另一个别名。”因此这行代码在静态语义上等价于 `int* pi2 = pi;`。分析器执行两个动作: 1. 复制存储的地址值; 2. **复制其指向列表(Points-to list)**:因为 `pi` 指向 `i`,所以 `pi2` 获得相同的指向列表,即 `pi2` 同样静态指向 `i`。 这种基于规则的简单推导展现了强大的局部推导能力。例如,当引入另一个变量 `j`,并通过赋值将 `pi` 重新指向 `j` 时,分析器完全不需要去修改 `ppi` 的记录,因为 `ppi` 依然只记住自己指向 `pi`。如果再次通过 `*ppi` 给 `pi2` 赋值,分析器动态沿着 `ppi -> pi` 读取此时 `pi` 的最新指向列表(即指向 `j`),并将其赋予 `pi2`。Herb 调侃道:“计算机非常笨,我们必须用婴儿学步的方式跟它沟通,因为它一次只能理解一条简单的规则。但在这些规则之上,我们可以构建宏伟的大厦。” 热身结束后,讨论切入到真正困难的领域:**从智能指针中取出非拥有型指针**。 当拥有一个 `std::shared_ptr<int> s` 时,堆上的那个整型对象在源码里并没有变量名。Herb Sutter 抛出一个问题:“共享指针 `s` 在堆上拥有的那个 `int` 叫什么名字?静态代码里它根本没有名字,对吧?”为了解决这一抽象难题,静态分析规则引入了虚拟符号名: - 将该堆对象命名为 **$S'$(S-Prime)**,专门作为“由共享指针 `S` 所拥有的堆对象”的简写标记。 - 当开发者调用 `int* p = s.get();` 时,分析器推导出 `p` 指向虚拟对象 $S'$。 关于该指针 `p` 究竟能合法存活多久,Herb 向全场提问:“这个指针的有效期应该是多久?谁来猜一下?”答案是:**直到 `s` 被销毁,或者 `s` 被修改为止**。那么分析器又如何知道 `s` 何时被修改?Herb 笑着感叹:“Bjarne 发明了 `const` 这个美妙的机制,后来 C 语言也借鉴了它。我们手头早就已经拥有了这些信息。”编译器对于 `std::shared_ptr` 的哪些成员函数是 `const`(只读)以及哪些会产生修改具有完全的静态感知,从而为跟踪非拥有指针的失效时机提供了坚实的理论依据。 ### [40:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=2400s) 所有者生命周期规则与原生指针失效机制 Herb Sutter 深入阐述了静态生命周期跟踪的核心原则:指针或引用的有效性严格依赖于其所有者(Owner)的状态。若一个原生指针 $P$ 指向某个由智能指针 $S$ 所拥有的对象,那么只要对 $S$ 调用析构函数,或者执行非 const 的修改操作(例如 `reset()` 或重新赋值使其指向其他对象),$P$ 就会立即失效。Herb 直言不讳地指出,这种保守的静态分析模型可能会带来假正例(False Positives),比如执行了没有实质改变所有权的自我赋值 `s = s;`,分析器仍然会把随后对指针的解引用标记为错误。但他明确表态,为了保证健全性(Soundness),宁可承担少量的假正例,后续团队会逐步降低误报率,但首要前提是杜绝漏报:“And I know that if you modify that shared_ptr, I can no longer trust that pointer.” ```mermaid flowchart TD accTitle: Pointer Invalidation Flow accDescr: Demonstrates how modifying an owner invalidates raw pointers derived from it. subgraph OwnerState["所有者状态与指针派生"] S["std::shared_ptr s"] -->|"派生出借用指针"| P["原生指针 p = s.get()"] P -->|"s 未被修改"| Valid["解引用 *p:安全合法"] end subgraph MutateAction["非 const 修改操作"] Reset["调用 s.reset() 或赋值 s = other"] end MutateAction -.->|"使指针失效"| P P -->|"在修改后解引用"| Error["静态分析报错:解引用无效指针"] ``` Herb Sutter 强调,这套生命周期检测规则完全不依赖于 `std::shared_ptr` 的硬编码。分析器并不关心具体使用的是哪种智能指针,它识别的是抽象的“所有者(Owner)”概念。不论是标准库智能指针、用户自定义的拥有型智能指针,还是 `std::vector`、`std::string` 等各类标准容器,只要被标记为拥有者,其非 const 修改都会使此前流出的指针和引用失效。 随后,Herb Sutter 邀请微软团队的 Neil 登台演示原型工具。Neil 带领的团队基于微软已有的静态分析框架(该框架曾支撑 Prefast、Prefix 以及 SAL 注解)构建了生命周期规则分析原型。Neil 在现场调出一段示例代码: - 代码中首先由 `shared_ptr` 获取裸指针并执行解引用,代码虽然无用但语法合法,分析器显示错误列表为空。 - Neil 随后在代码第 8 行插入一条重定向赋值操作(使用 `operator=` 给该 `shared_ptr` 赋新值)。 - 再次运行代码分析时,IDE 立即弹出了粉红色高亮报错,精准指出第 8 行对所有者的重新赋值导致后续解引用访问了失效指针。 ### [45:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=2700s) 临时对象的销毁陷阱与实机诊断演示 Herb Sutter 展示了一个源自 Stack Overflow 的真实提问案例。该提问者发帖求助称:“This code compiles, but ra contains garbage. Can someone explain to me why this code is invalid?” 问题代码包含一个名为 `my_fun` 的函数,该函数按值返回一个 `std::unique_ptr<A>`。调用方在调用时试图通过解引用将其绑定到一个常量引用 `const A& ra = *my_fun();` 上。 Sutter 分析道,按值返回的 `unique_ptr` 移动出来后是一个右值临时对象(Temp-UP)。当该语句结束时,临时拥有者对象被立即析构,而绑定的引用直接指向了一个已被销毁的死对象。分析器会在初始化引用的这一行直接拦截并报错,因为初始化一个在同一行便注定失效的引用没有任何实际价值。如果编译器能直接给出错误提示,就能在社区论坛上“节省大量的墨水”。 ```mermaid flowchart LR accTitle: Temporary Unique Pointer Lifetime accDescr: How binding a reference to a temporary unique_ptr dereference causes a dangling reference. subgraph CallSite["表达式执行与析构"] Call["my_fun() 返回临时 unique_ptr"] --> Deref["解引用 operator*()"] Deref --> Ref["初始化引用 ra"] Call -->|"语句分号结束"| Destruct["临时 unique_ptr 析构并释放 A"] end Ref -.->|"悬垂指向"| Destruct Destruct ==> InvalidRef["ra 变为失效悬垂引用"] ``` Neil 在 Visual C++ 的分析工具中当场加载了这一错误示例。工具成功定位到问题:在第 14 行,由于临时 `unique_ptr` 的析构函数被调用,变量 `ra` 已被判定为无效。针对该问题,Neil 现场演示了两种修复策略: - **方案一(标准修正):** 将函数返回值直接接管到本地拥有者变量中,即 `auto p = my_fun();`,然后再对其进行解引用。Neil 在编码时开玩笑调侃输入法按键映射变了(“Devorak keyboard”),并在调整为安全代码后顺利通过分析,零警告无错误。 - **方案二(高级生命周期扩展):** 原始代码之所以未触发 C++ 的临时变量生命周期延长(Lifetime Extension),是因为引用绑定的是内部类型 `A`(`const A&`),而非外层包装的临时 `unique_ptr` 本身。如果将接收变量声明为右值引用的常量绑定或直接持有临时包装对象,延长规则才能生效。Neil 将类型调整后,临时对象成功活到了作用域结束,静态分析器同样正确识别到了这一点,顺畅通过验证。 - Neil 登台时身着代表西雅图海鹰队(Seattle Seahawks)的球迷服,Herb Sutter 调侃这是设计会议期间的常客着装,象征着从事 C++ 复杂指针生命周期研究必须具备的两大品质:“忠诚”与“乐观”。 - Neil 也随即接梗引用了著名的体育采访名梗:“Just here so I don't get fined.” 逗乐了全场观众。 - 演示完毕后,Sutter 示意 Neil 暂留台上,准备进入下一步更复杂、更“奇特”的多所有者共享生命周期场景。 ### [50:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=3000s) 多级所有权别名与指针失效追踪 Herb Sutter 介绍了一个由复杂所有权层级构成的示例,演示编译器与静态分析工具如何确定性地追踪指针失效。在这个场景中,底层存在一个 `std::shared_ptr<std::vector<int>>`(命名为 `sv`),它指向一个无名的 `vector`;该 `vector` 作为所有者,又持有一块 `int` 缓冲区。在此基础上派生出多个指针别名: - `sv2` 是指向智能指针 `sv` 本身的原始指针; - `vec` 是指向底层无名 `vector` 的原始指针; - `ptr` 是指向 `vector` 所拥有 `int` 元素的原始指针。 面对这种交错复杂的指针网,Herb Sutter 调侃道:“像泥巴一样清楚,对吧?这就是我们一直以来要求编译器做的事情,我们太残忍了。” ```mermaid flowchart TD accTitle: 所有权与指针指向层级 accDescr: 展示从 shared_ptr 到 vector 再到 int 缓冲区的两级所有权结构及对应的原始指针别名 subgraph Owners["所有权关系 (Owners)"] SV["sv: shared_ptr"] -->|"owns (SV')"| V["vector&lt;int&gt;"] V -->|"owns (SV'')"| I["int 缓冲区"] end subgraph Pointers["指针与别名 (Pointers)"] SV2["sv2: raw ptr"] -.->|"points to (SV)"| SV VEC["vec: raw ptr"] -.->|"points to (SV')"| V PTR["ptr: raw ptr"] -.->|"points to (SV'')"| I end ``` Herb Sutter 解释了用于追踪这种深层间接关系的符号系统——撇号计数(`prime` 记号)。静态分析模型限制只跟踪两级 `prime`: - `sv2` 指向 `sv`(第 0 级所有者别名); - `vec` 指向 `sv'`(由 `sv` 拥有的第一级对象); - `ptr` 指向 `sv''`(由 `sv` 拥有的对象再拥有的第二级对象)。 虽然只保留到 `prime prime`(两层所有权)可能会产生少量假阳性(false positives),但已足以精确覆盖绝大多数典型场景。接着 Herb Sutter 讨论了两种干扰该系统的修改操作: 1. **调用 `vec->push_back(1)`**:`vec->` 解引用等价于访问表达式 `sv'`。对 `sv'` 执行非 `const` 变异操作会使其下一级拥有的所有内容(即 `sv''`)全部失效。因此,仅有指向内部元素的 `ptr` 变为悬垂指针(即使底层内存可能尚未重新分配,但在 C++ 标准规范中已定义为失效)。Herb Sutter 激动地感慨:“很多很多年来,我一直梦寐以求能得到这种错误提示。” 2. **重置智能指针 `(*sv2).reset()`**:`sv2` 是 `sv` 的别名,调用 `reset()` 是对根对象 `sv` 的非 `const` 变异。这会导致 `sv'`(整个 `vector`)以及级联的 `sv''`(内部 `int` 存储)统统失效。因此,`vec` 和 `ptr` 会在同一时刻被双双判定为非法指针。 --- ### [52:45](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=3165s) 动态实机测试:编译器诊断指针失效 为了证明这套规则并非空中楼阁,Herb Sutter 与助手 Neil 进行了实机演示,当场验证静态分析工具的原型实现。在控制台演示中,Neil 首先建立起三层指针结构并直接使用,工具验证基线通过,未报任何错误。 随后 Neil 按照讨论先后引入变异操作: - 首先在第 12 行插入向 `vector` 添加元素的调用(Neil 将插入的值写成了 42,Sutter 调侃道:“你喜欢 42。Douglas Adams 泉下有知也会感到欣慰”)。重新分析后,工具在第 12 行标出失效触发点,并在第 14 行准确报错:解引用了一个名为 `ptr` 的失效指针。 - 随后 Neil 撤销 `push_back`,改为通过别名指针执行 `sv2->reset()`。工具立即弹出两条诊断警告:它不仅识别出指向内部整数的 `ptr` 已失效,还精确抓住了原本指向无名 `vector` 的 `vec` 同样已经失效。 Herb Sutter 强调,这一诊断能力之所以极其强大甚至“显得有些神奇”,是因为分析器完全通过别名推理工作:指针 `ptr` 最初是通过直接访问 `sv` 获取的,而破坏它的操作却来自于一个完全看似无关的独立别名 `sv2`。只要编译器在局部分析中掌握了“谁指向谁”,便能穿透间接路径,计算出精确的作用域与失效依赖链。 --- ### [55:50](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=3350s) 跨函数调用与生命周期保证 在总结函数局部作用域的控制流分析(分支合并 `P` 的可能指向、循环、空指针检查以及在 `try-catch` 中对异常路径采取悲观失效假设)之后,Herb Sutter 将重心转向跨函数边界(跨过程)调用的生命周期安全。 针对向外部函数传递指针参数的情况,Herb Sutter 指出被调用方(callee)与调用方(caller)之间必须达成契约平衡: ```mermaid flowchart LR accTitle: 跨函数调用指针契约 accDescr: 展示调用方需履行的指针生命周期保障与被调用方在函数内的有效性假定 subgraph Caller["调用方 (Caller) 责任"] C1["保证指针在调用期间存活"] C2["确保被调函数无副作用<br/>导致维持指针存活的对象失效"] end subgraph Callee["被调用方 (Callee) 假定"] E1["默认假定入参指针<br/>在整个调用期间合法"] end Caller -->|"安全传递"| Callee ``` 被调用方的行为规则相对简单直接: - 进入函数时,被调用方默认假定传入的原始指针在其整个调用生命周期内始终有效。Herb Sutter 幽默地形容被调用方的态度:“在被调用方内部,如果我收到一个指针参数,我直接假定它在整个调用期间都是好的。对啊,我才不管它是从你叔叔的皮卡车后斗里掉出来的,看起来没问题,这把我就信你了。” 然而,所有的防线压力都落在了调用方身上。调用方必须严格保证两点: 1. 指针指向的对象在函数调用的整个生命周期内不被销毁。 2. 被调用的函数不能通过潜在的副作用(包括通过全局状态或回调导致的**重入性 reentrancy**)意外破坏维持该指针生命周期的所有者。 Herb Sutter 向现场观众抛出这个微妙却极易踩坑的场景时,发现台下有大量听众心领神会地举手,他感叹道:“太多人举手了。这种话你第一眼看到时本不该秒懂,只有当你被它反复坑过很多次之后,你才会深有体会。”接着,他展示了智能指针使用过程中最致命、最隐蔽的“头号正确性问题”代码原型:一个全局智能指针 `gsp`,调用方直接通过 `f(gsp.get())` 将裸指针交给了潜在危险的外部函数。 - 关键考量:被调用函数内部如果直接或间接访问了全局智能指针 `gsp` 并重置了它,传入 `f` 的裸指针参数将在函数执行途中瞬间变为悬垂指针。 ### [01:00:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=3600s) 智能指针的头号正确性问题:悬空与重入失效 在 C++ 中,直接将全局智能指针(例如 `GSP`)通过 `.get()` 获取到的裸指针传递给某个函数 `f`,隐藏着极其危险的缺陷。调用者往往没有意识到:被调用的函数 `f` 根本不知道传入的指针指向由全局变量 `GSP` 所拥有的对象;既然 `GSP` 是全局变量,那么 `f` 本身,或者 `f` 在调用栈深处(甚至是一百层调用之后)调用的任何函数,都有可能作为副作用修改 `GSP`。一旦全局智能指针被重新赋值或重置,它所指向的对象就会立即析构,导致传递给函数的裸指针瞬间变成悬空指针(dangling pointer)。Herb Sutter 直言:“We should be ashamed of ourselves. We gave it a pointer it could invalidate.” 解决这个问题的正确范式是在调用树的顶端先创建一个局部的强引用。对于 `std::shared_pointer`,应在调用前在本地拷贝一份该智能指针,这样仅需付出一次引用计数递增的代价。随后从这个本地智能指针中提取裸指针并沿调用树向下传递。由于本地智能指针持有独立的引用计数,深层被调用的函数无论产生何种副作用,都绝不可能将引用计数降为零,从而完全杜绝了指针失效的问题。Herb Sutter 指出,许多程序员虽然极具规范意识地使用智能指针,却常在涉及跨语言互操作或复杂重入调用时遭遇难以排查的悬空指针,而“获取本地根(local root)”正是解决这种重入导致指针被破坏的核心法则。 ```mermaid flowchart TD accTitle: 避免智能指针裸指针悬空的正确机制 accDescr: 展示全局智能指针直接获取裸指针导致失效,与先拷贝到局部变量再获取裸指针的安全对比 subgraph UnsafePath["危险做法:直接传递全局智能指针的裸指针"] G1["全局 GSP"] -->|"GSP.get()"| P1["裸指针 ptr"] P1 --> F1["调用函数 f(ptr)"] F1 -.->|"深层调用副作用修改 GSP"| G1 G1 -.->|"引用计数清零,内存释放"| Dangling["ptr 变为悬空指针(Crash / 漏洞)"] end subgraph SafePath["安全做法:在调用树顶层创建局部拷贝"] G2["全局 GSP"] -->|"拷贝构造"| L2["局部 sp 副本(Ref Count + 1)"] L2 -->|"sp.get()"| P2["裸指针 ptr"] P2 --> F2["调用函数 f(ptr)"] F2 -.->|"即便深层副作用修改了全局 GSP"| G2 L2 ==>|"局部 sp 依然锁住生命周期"| Safe["内存不会释放,ptr 绝对安全"] end ``` 针对将智能指针与裸指针同时作为参数传递的情况(例如函数 `g(shared_ptr<int>&, int*)`),分析器同样能进行严密判定: - 若将同一个智能指针的非常量引用与其内部数据的裸指针同时传入(例如 `g(sp, sp.get())`),因为函数 `g` 可能会通过引用修改该智能指针,它就有可能使得传入的裸指针失效;即便某种情况下碰巧安全,静态分析工具也会将其标记为非法。正如 Herb Sutter 所强调:“Dollars to donuts, you don't know. And we're going to say that that's invalid.” - 相反,如果传入的是一个智能指针,以及另一个指向独立生命周期对象的裸指针(例如基于局部安全副本 `sp` 产生的 `sp.get()`,配合全局的 `GSP`),由于本地副本保证了生命周期不会归零,即便二者指向相同对象也是完全安全的。 --- ### [01:05:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=3900s) 实时静态分析演示与智能指针规则防线 在现场实时工具演示中,Neil 协助演示了静态生命周期检查工具在检测此类错误时的表现。当代码尝试调用 `f(GSP.get())` 时,编译器分析工具立刻报错,准确提示调用 `GSP.get()` 的结果可能被函数 `f` 自身访问并非法失效。而修复代码的方式极为自然:引入一个局部拷贝 `auto sp = GSP;`,然后传递 `sp.get()`。保存重新编译后,静态分析器立即通过并消除了警告。Herb Sutter 补充说明,目前工具正在持续演进其共享所有权分析模型,未来即便编写 `sp = GSP; f(GSP.get());`,工具也能够推理出本地已存在保护该对象的生命周期根节点,从而不将其误报为错误。 当现场测试将非常量引用参数与裸指针混合传递的边缘场景(如调用 `g(sp, sp.get())`)时,编译器再次成功拦截并报错。Herb Sutter 剖析指出,这种调用实际上触犯了生命周期系统的两道独立防线: 1. **防止潜在失效防线**:函数接受非常量引用,意味着函数内部可能会重置该智能指针,从而使伴随传入的内部裸指针失效。 2. **基础非常量使用防线**:在调用发生之前,将智能指针以非常量引用传递的行为本身,就被生命周期系统视为对 `sp` 的非常量使用(non-const use),而此操作会直接导致先前基于 `sp` 派生出的所有借用裸指针全部失效。 要想放过这种代码,用户必须在编译器中同时关掉两条安全规则。Herb Sutter 总结了智能指针在实践中的两大约束维度: - **正确性层面的头号问题**是由于重入和意外副作用导致的“生命周期踢翻支架(kicking the bucket that your pointer is standing on)”; - **性能层面的头号问题**则是滥用智能指针传参。程序员经常在仅需裸指针或引用的地方盲目传递 `std::shared_ptr`,引发巨大的无谓原子增减开销。关于传参的准则是:仅在需要表达所有权转移或所有权共享时才传递智能指针,其余情况一律传递裸引用或裸指针。 --- ### [01:08:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=4080s) 契约与生命周期覆盖:以 STL 容器的 insert 方法为例 在标准库(STL)中,默认假设传入函数的不同指针具有彼此独立的生命周期,但在许多真实接口中,这种默认假设并不成立,此时需要显式的契约注解来覆盖默认生命周期语义。Herb Sutter 以 `std::map::insert` 为例,深入剖析了如何通过生命周期规则静态捕捉由于迭代器失效引发的内存 Bug: 对于带位置提示(hint)的插入重载 `insert(hint_iterator, value)`,标准库要求传入的迭代器必须指向当前的同一个容器。过去,这种要求仅以英文文档的形式存在,而“compilers can't do much with English text”。在核心指导方针的拟定语法中,可以通过注解将传入迭代器的生命周期直接绑定到当前对象的生命周期(即 `this` 或 `*this` 的生命周期)。如果程序员试图执行 `m.insert(m2.begin(), val)`,静态分析器就能立即捕获错误,因为 `m2.begin()` 指向的是容器 `m2` 而非 `m`。 对于范围插入重载 `insert(first, last)`,生命周期契约的要求则截然相反且更为苛刻: - 传入的迭代器区间 `[first, last)` 必须指向同一个底层序列,因此第二个参数必须声明为具有与第一个参数相同的生命周期,从而杜绝将不同容器的迭代器混搭成无效区间的低级错误(例如一个来自 `vector1`,另一个来自 `vector2`); - 该迭代器区间**绝不能**指向被插入的目标容器 `this`。因为 `insert` 是一个会修改当前容器的非 `const` 操作,如果传入源指向自身容器(例如 `m.insert(m.begin(), m.end())`),在插入过程中极易引发内部重分配或结构改变,导致源迭代器当场失效。工具能够在此类错误调用发生前就静态捕获两项致命冲突:参数非法指向了被修改对象,以及在调用发生瞬间迭代器便已遭遇非法破坏。 | `insert` 重载签名 | 迭代器所属容器要求 | 预期生命周期关系约束 | 错误调用示例 | 工具捕获的错误原因 | |:--- |:--- |:--- |:--- |:--- | | `insert(hint, value)` | 必须属于当前容器 `this` | `hint` 的生命周期与 `*this` 绑定一致 | `m.insert(m2.begin(), val)` | 传入迭代器属于 `m2`,违反当前容器生命周期要求 | | `insert(first, last)` | 必须**不属于**当前容器 `this`,且 `first` 与 `last` 属于同一容器 | `last` 寿命等同于 `first`;且 `[first, last)` 独立于 `this` | `m.insert(m.begin(), m.end())` | 1. 迭代器非法指向目标容器自身;2. 容器被自身非 const 操作修改导致迭代器即刻失效 | ### [01:10:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=4200s) 编译期捕获无效迭代器范围与大写“Pointer”模型 Herb Sutter 继续通过具体的代码用例展示静态检查器的威力。如果代码中写下类似 `m.insert(m2.begin(), m.end());` 的调用,实际上是在传入一个根本不能成对的迭代器范围——它们分别来自两个完全不同的容器。静态检查器会立即报错指出:参数 1 指向 `m2`,而参数 2 却指向 `m`,两者必须指向同一对象;只有当两端来自同一个容器时代码才能顺利通过。Herb 强调,这一切全都在编译期完成,无需任何运行时调试插桩,就能彻底杜绝一整类困扰 C++ 程序员多年的迭代器范围错误。 ```mermaid flowchart TD accTitle: 迭代器范围静态检查机制 accDescr: 展示编译器如何在编译期核验两个迭代器是否来自相同容器对象 subgraph ErrorCase["非法范围匹配"] A1["参数 1: m2.begin()"] -->|"points to"| C1["容器 m2"] A2["参数 2: m.end()"] -->|"points to"| C2["容器 m"] C1 -.->|"容器不一致 -> 编译期拦截"| Err["报错: param 1 与 param 2 指向不同容器"] end subgraph ValidCase["合法范围匹配"] B1["参数 1: m.begin()"] -->|"points to"| C3["容器 m"] B2["参数 2: m.end()"] -->|"points to"| C3 C3 -->|"指向相同容器"| OK["编译通过"] end ``` 这种机制之所以可行,是因为在新的生命周期心智模型中,迭代器在概念上就是大写的“指针(Pointer)”:它们提供间接访问,但并不拥有所间接引用的对象。紧接着,Herb 将讨论重点转向核心议题——函数返回的指针生命周期。现场听众对此反响热烈,Herb 注意到台下重重点头的 Detlef 并打趣道,如果有听众听过 Detlef 前一天关于并发的演讲,就会明白他点头点得停不下来有多么让人高兴。 - 迭代器本质上属于非拥有型间接访问,即概念上的指针(Pointer),涵盖 `iterator`、原生指针以及 `array_view` 等抽象。 - 编译期静态分析可以在零运行时开销、零调试插桩的前提下,直接查验并拦截跨容器混用 `begin()` 与 `end()` 的严重逻辑错误。 --- ### [01:12:15](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=4335s) 函数返回值生命周期的默认规则与零注解哲学 Herb 澄清了一个关键原则:生命周期静态检查的正确性并不依附于默认规则,引入默认约定纯粹是为了最大限度减少人工注解(annotation)。在理论层面,任何返回指针的函数都必须明确其指向对象的生命周期:调用方据此假定其有效范围,而编译器在单独编译被调用函数体(callee)时验证其是否符合约定。但这并不意味着程序员需要面对繁重的类型标注,因为从计算机工程的共识来看,函数的行为具备极高的一致性。Herb 直言: > “If I get a pointer out of a function, its dollars to donuts comes from the inputs, from the owners and the pointers that were passed into the function.” > (“如果我从一个函数里拿回一个指针,十有八九它来自输入参数——来自传进函数的拥有者和指针参数。”) 因此系统确立了简洁的默认推导规则: 1. **优先源自输入参数**:返回指针的生命周期默认与传入的拥有者(Owner)或指针(Pointer)参数绑定; 2. **无指针/拥有者入参时默认为静态**:当函数无入参或仅接收 `int` 等纯值类型时,默认假设返回指针指向具有 `static` 静态生命周期的对象(例如单例实例接口 `Singleton::instance()` 即可享受零注解待遇); 3. **推导冲突时才需显式标注**:仅当被调函数的实际实现违背上述默认推导时,单独编译该函数才会报错,提示开发者必须显式标注其真实生命周期契约。编译器甚至会在错误信息中直接给出可复制粘贴的生命周期标注语法。 Herb 对比指出,这正是他不喜欢 Rust、Cyclone 以及 C-Cured 的一点——它们的标注负担过重。尽管 Rust 也有不错的默认推导策略,但 C++ 应当力求在绝大多数日常函数中完全无需开发者书写区域(regions)或作用域(scopes)注解。在具体实现上,如果函数接受两个 `int*` 指针 `p` 与 `q` 并返回 `int*`,其推导逻辑类似于分支结构:默认假定返回值集合属于 `*p` 或 `*q`;而若传入的是对象拥有者(如 `std::string`),则假定返回指针指向该拥有者内部管理的数据(即 `owner'`)。 --- ### [01:15:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=4500s) 右值弱磁铁、强磁铁与被调函数契约检验 当涉及到临时对象时,C++ 精细的值类别(Lvalue、PRvalue、Xvalue)恰好能与生命周期追踪完美契合。为了直观阐述,Herb 提出了两个自创的比喻术语——“右值弱磁铁(Weak Magnet)”与“右值强磁铁(Strong Magnet)”: | 参数类别 | 概念定义 | 右值磁力特性 | 默认生命周期绑定考量 | |:--- |:--- |:--- |:--- | | `const Owner&`(常量引用) | 弱磁铁(Weak Magnet) | 既可绑定左值,也能附着在临时对象(PRvalue)上 | 默认排在普通拥有者之后;若有其他正常拥有者存在,推导时会优先忽略弱磁铁 | | `Owner&&`(右值引用) | 强磁铁(Strong Magnet) | 专门绑定临时对象(PRvalue)或即将消亡的将亡值(Xvalue,如 `std::move`) | **绝不**将返回指针与此类参数绑定,因为其指向的目标即将被移走或销毁 | 以接口 `find_match(string& s, const string& match)` 为例,返回值 `char*` 在默认情况下只会与第一参数 `s` 绑定,而自动忽略作为弱磁铁的第二参数 `match`。这样一来,无论调用点传入的是左值、字面量还是临时对象,编译器都能安全认定返回指针的存活时间与主字符串 `s` 一致。 ```mermaid flowchart LR accTitle: 被调函数的生命周期推导与安全验证 accDescr: 展示 callee 函数体内部如何校验返回值与默认契约 s' 的兼容性 subgraph Callee["被调函数内部验证 find_match(s, sub)"] direction TB P1["从 s 提取指针 -> 标记为 s'"] -->|"与返回值默认契约 s' 匹配"| Ret1["允许返回 char*"] P2["从 sub 取地址 -> 标记为 sub'"] -->|"与默认契约 s' 冲突"| Ret2["编译拦截: 阻止非法返回"] end ``` 在独立编译被调用方(callee)函数体时,编译器会施加极其严格的类型与生命周期核验: - 若函数体返回从 `s` 获取的指针,由于其内部标记为 `s'`,与对外承诺的生命周期兼容,编译直接放行; - 若函数试图返回取自局部参数 `sub` 的地址(生命周期标记为 `sub'`),编译器会直接报错拦截,防止悬垂指针逃逸。开发者若确有特殊意图,必须在函数签名上手动标注 `lifetime(sub)`; - 这一规则直接适用于标准库容器方法:例如 `std::vector::operator[]` 返回的是对 `*this'` 的内部引用,而 `vector::begin()` 则是返回指向 `*this` 内部元素的迭代器,一旦宿主容器被修改或销毁,这些借用都会在编译期追踪中失效。 在讲完这套严密的推导体系后,Herb 看了看时间,转向台下寻找好友 Gaby(Gabriel Dos Reis),并幽默地提到自己每年都会收到来自另一位极富才华的挚友 Andrei Alexandrescu 的邮件,以此引出关于语言设计演进的后续讨论。 ### [01:20:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=4800s) `std::min` 的悬垂引用隐患与 Andrei Alexandrescu 的 20 年困扰 Herb Sutter 在现场把坐在走道旁的 Gabriel Dos Reis(Gaby)请入讨论,开玩笑说 Gaby 事先完全不知道自己会被点名。Sutter 借机引出了著名 C++ 专家 Andrei Alexandrescu 长期以来的一个执念:“标准委员会到底什么时候才会修复 `std::min`?”Gaby 回忆道,Andrei 最早在 1998 年左右就和他讨论过这个问题,那还是在 Facebook 和 Google Groups 出现之前的年代。 ```mermaid flowchart TD accTitle: std::min 的临时对象悬垂引用问题 accDescr: 展示传入临时对象给 std::min 导致返回的引用失效的机制 subgraph Inputs ["参数传递阶段"] A["变量 x (持久左值)"] -->|"绑定为 const T&"| C["std::min(a, b)"] B["临时对象 y + 1 (右值)"] -->|"绑定为 const T&"| C end subgraph Evaluation ["求值与返回"] C -->|"返回引用"| D["const int& ref"] end subgraph Danger ["生命周期结束"] B -.->|"完整表达式结束被销毁"| E["悬垂引用 (Dangling Reference)"] D -.->|"指向已销毁的内存"| E end ``` Gaby 解释了 `std::min` 潜在的危险机制:`std::min` 接受的参数是 `const T&`,并且为了效率直接返回其中一个引用的别名。如果调用者传入的是普通左值变量(例如两个 `int` 类型的 `x` 和 `y`),`min(x, y)` 毫无问题且非常高效;但如果在后续维护中,有人将调用写成 `min(x, y + 1)`,灾难就发生了。表达式 `y + 1` 会生成一个临时对象,虽然它能合法绑定到形参 `const T&` 上,但该临时对象的生命周期只延续到包含它的完整表达式结束。一旦 `min` 返回了这个临时对象的引用并被外部引用捕获,就会产生一个极难排查的悬垂引用(Dangling Reference)。更可怕的是,这种崩溃往往是“数据依赖”的——只有当 `y + 1` 恰好小于 `x` 时才会返回该临时对象的引用并崩溃,如果 `x` 较小代码便看似正常运行。同理,若传入函数调用,危险性完全取决于该函数返回的是左值引用还是纯右值。Sutter 感叹:“Andrei is completely correct and has rightly been complaining about it for almost 20 years.” - Andrei Alexandrescu 早在 1998 年就指出了 `std::min` 返回 `const T&` 时绑定临时对象的悬垂隐患。 - 这一缺陷极其隐蔽,因为传入临时对象不仅完全符合编译规则,而且是否引发未定义行为取决于运行期的比较结果(数据依赖)。 ### [01:22:15](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=4935s) 现场工具演示:捕获临时对象与区分静态生命周期 为了展示静态生命周期 Profile(Lifetime Profile)的威力,Sutter 请 Neil Macintosh 在 Visual Studio 实验工具上进行现场验证。Neil 首先展示了标准调用 `min(x, y)` 以及自身编写的 `min` 实现,以证明背后没有任何作弊手段。编译器顺利编译,分析工具也判定代码完全安全。 接着,Neil 将代码修改为传入临时对象的版本 `min(x, y + 1)`。此时常规编译器依然毫无警告地编译通过,但底层的代码分析工具立刻报出错误,提示 `min` 的返回值正被某个临时变量名(底层生成的内部名称)所使失效(invalidated)。随后,Sutter 让 Neil 测试调用自定义函数的情况: 1. **测试 `F2()`**:该函数不接受任何参数,返回一个静态变量的引用(`static`)。分析工具推断无参函数返回的指针/引用必定指向静态存储期对象,因此判定其生命周期安全,成功通过检查。 2. **测试 `F3()`**:该函数按值返回对象(By Value)。分析工具敏锐地将其识别为临时对象,再次准确地报错拦截。 Sutter 回顾前一天听众在走廊里的议论:当 Bjarne Stroustrup 宣称“我们认为通过区分所有者(Owner)和指针、并追踪指针指向的目标,就能消除生命周期错误”时,不少人觉得这听起来像是在变魔术。但 Sutter 强调,Bjarne 绝不会信口开河,这套分析模型有望在不引入垃圾回收(GC)、不发明新语言、不对既有 C++ 代码进行沉重注解的前提下,“解开困扰了计算机界 40 年的难题(crack a 40-year nut)”。 - 静态生命周期分析能够区分返回临时右值的函数与返回 `static` 左值引用的无参函数,避免误报。 - 分析工具在开发阶段会将临时变量报告为生成的内部临时符号,后续版本会进一步优化错误提示信息的人类可读性。 - Sutter 刚刚在 CppCoreGuidelines 官方 GitHub 仓库的 `/docs` 目录下提交了 0.9.1 设计规范文档,除部分面向编译器实现者的形式化规则仍在完善外,主体模型已全部公开。 ### [01:25:00](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=5100s) 交付路线图、安全对语言简化的反哺与闭幕致谢 对于开发者最关心的可用时间,Neil Macintosh 和 Sutter 明确了 Visual Studio 团队的发布路线图: - **边界(Bounds)与类型(Type)检查器**:作为首个原型随 Visual Studio 2015 Update 1 同步发布(预计一到两个月内推出 CTP 测试版)。 - **生命周期(Lifetime)检查器**:作为后续原型在随后的几个月内(下一个冬季前后)推出。 Sutter 进一步探讨了“安全性反哺语言简化”的深层哲学。他提出了一个常见的困惑:为什么智能指针(如 `unique_ptr`、`shared_ptr`)不能隐式转换为原始裸指针 `T*`?为什么开发者必须到处繁琐地写 `.get()`?这是因为在传统 C++ 中,一旦允许隐式转换为 `T*`,指针算术(如 `ptr + 42`)就可能合法化并引发越界野指针;或者开发者未意识到拿到了裸指针,未顾及智能指针的析构时机从而导致指针悬垂。Sutter 指出:“Well, if we do have a solution where we have a subset of C++ where there are no dangling, and no bounds violations. There should be no reason that smart pointers can't just convert to T-Star.” 只要静态保证了没有悬垂和越界,库的设计就能变得极其自然直观。 最后,Sutter 梳理了本次大会后续的相关专题讲座,并呼吁开源社区共同参与核心指南的建设: - **Gabriel Dos Reis (Gaby)**:午餐后分享关于 C++ Modules 的进展(通过阻断宏泄露提升安全性),后续还将带来关于合约编程(Contracts)的演讲。 - **Neil Macintosh**:次日带来两场静态分析实战讲座,深度解析 `array_view` 与 `string_view` 的运作机制。 - **Eric Niebler**:周五的主题演讲/结幕演讲将深入 Ranges(范围库),从抽象层面彻底解决裸迭代器丢失边界的安全问题。 Sutter 表达了对全球开发者积极参与 GitHub 仓库的感激之情,并呼吁大家继续贡献:“We need help. We can't do it ourselves.” 整场演讲在热烈的掌声中圆满结束。 - 核心指南倡导的方法论几乎完全在编译期静态执行,零运行时开销(zero runtime overhead)。 - 微软 Visual Studio 将率先集成这些 Profiles 的分析检查,同时也期待 Clang、GCC 等其他编译器与工具链厂商跟进实现。 ## Notable quotes > [08:30](https://www.youtube.com/watch?v=hEx5DNLWGgA&t=510s) C++ code compiled in the safe subset, so with these particular parts of guidelines turned on, is never the root cause of type or memory safety errors unless you opened the hood and explicitly said unsafe me, go and let me do something that I know is wrong. — Herb Sutter ## Mentioned Herb Sutter, Bjarne Stroustrup, Neil McIntosh, Gabriel Dos Reis, Andrei Alexandrescu, Eric Niebler, Detlef, C++ Core Guidelines, GSL (Guideline Support Library), GitHub, XcodeGhost, iOS 9, Microsoft SAL, Prefast, Prefix, Visual Studio, Visual Studio 2015 Update 1, Rust, Cyclone, CCured, array_view, string_view, std::unique_ptr, std::shared_ptr, std::variant, std::min, std::vector, std::map ## Video description http://www.cppcon.org — Presentation Slides, PDFs, Source Code and other presenter materials are available at: https://github.com/isocpp/CppCoreGuidelines/blob/master/talks/Sutter%20-%20CppCon%202015%20day%202%20plenary%20.pdf -- Modern C++ is clean, safe, and fast. It continues to deliver better and simpler features than were previously available. How can we help most C++ programmers get the improved features by default, so that our code is better by upgrading to take full advantage of modern C++? This talk continues from Bjarne Stroustrup’s Monday keynote to describe how the open C++ core guidelines project is the cornerstone of a broader effort to promote modern C++. Using the same cross-platform effort Stroustrup described, this talk shows how to enable programmers write production-quality C++ code that is, among other benefits, type-safe and memory-safe by default – free of most classes of type errors, bounds errors, and leak/dangling errors – and still exemplary, efficient, and fully modern C++. Background reading: Bjarne Stroustrup’s 2005 “SELL” paper, “A rationale for semantically enhanced library languages," is important background for this talk. -- Videos Filmed & Edited by Bash Films: http://www.BashFilms.com Work at Hudson River Trading (HRT): https://tinyurl.com/safxfctf [Source](https://www.youtube.com/watch?v=hEx5DNLWGgA)

Source quotes

[8:30] Herb Sutter: C++ code compiled in the safe subset, so with these particular parts of guidelines turned on, is never the root cause of type or memory safety errors unless you opened the hood and explicitly said unsafe me, go and let me do something that I know is wrong.

Source quotes

[2:30] Herb Sutter: Nobody tries to take anything away successfully from C++ programmers. [6:50] Herb Sutter: Everybody, every program in any language eventually calls unsafe code. [27:50] Herb Sutter: Raw pointers and references are just fine, and you really want to use them on the stack, especially as locals and especially as parameters, for parameter passing by reference, by pointer, because you want the speed and the leanness of just an address.

Original transcript

[0:00] So no further announcements. I'm going to bring up our headliner right now, Herb Sutter. Well, did you all enjoy Bjarn's keynote yesterday? Now, you already told John that. but I have a more specific question for you. In Björn's keynote, how many of you, just by show of hands, how many of you found at least one thing you want to go and try and apply back at work? Lots of hands. That's great. because These guidelines are meant to be used that Bjarne announced yesterday. And I know that you've been paying attention to it, because we noticed it. This is a snapshot as of around six o'clock this morning of the top repos trending worldwide over the past week, and this has only been available for a few days. over the past week, and you'll notice who's the number one spot. Now, we do have many hundreds of people in the room, in the high hundreds, so the better part of a thousand people are here, But-- This is clearly more than just you. So those who are in the room, those who are watching this on demand afterwards, Thank you very much for your support, because you make this possible. You guys, you rock. Thank you very much for this. Now, in case you're wondering, What other repos are hot this week? You might have noticed if you tune into CNN and other news outlets, that you see things like Xcode Ghost, the very first ever malware for iOS. There was also a major new version of iOS released. The Xcode Ghost and iOS 9 sampler projects are trending this week as well. They're number three and number four. And combined, they don't match your interest in C++ coding standards. Thank you. for your interest. Very exciting times. When we think about C++, as Bjarne said yesterday, We already have so much goodness. How many of you are regularly writing... code more like the right-hand side in production today. That is great to see. The number of hands going up increases every year, because you've got to adopt tools, you've got to be have management buy-in to be able to use them. And yet the code on the left-hand side of the screen-- Still works. It's a great compatibility story. This talk is very much part two of Bjarne Strusserf's keynote, yesterday. We're going to go into more detail on some specific things that he said. And for completeness, to make it a standalone talk, especially for those who are watching this on demand, may not have been here or may be watching these out of order, I'll mention a few highlights, but we'll go through the first few slides pretty quickly because a lot of it is just sort of rounding out a little bit of what Yarnas said yesterday. And then we're going to take a deep dive onto a topic that you have already been asking about in the corridors that I've been hearing and the buzz overnight, and that we'll talk about today. So we have… The old... new delete, non-range 4C++, and we have the new better tools. Can we ever make those be the default? I mean, compatibility is great. We never want to just remove the old features, at least not for decades, because compatibility is really important. But can we get to a mode where we actually have the new stuff only on by default, and if you try to write the older code, you actually get a warning or an error and you can still access the old code. Nobody's trying to take anything away from you. Nobody tries to take anything away successfully from C++ programmers. but you just open the hood and then you can use them. It's like a drawer of knives. to use a popular expression, we want the knives. We are chefs. It's our business to use knives. But the knife drawer might not be one we want open by default. We want it to open it when we need it, but have the good and safe and simple parts be the ones that are armed by default most of the time. - Thank you. So that's what we're going to talk about today. And first to start with acknowledgments, as Bjarne announced, the C++ Core guidelines are all about getting these good new parts by default. And we need your help with that. They're sort of at an 0.6 state right now. And that's the repo that we just talked about a minute ago. There's also an initial reference implementation of the Guideline Support Library that works on various platforms and compilers, but we encourage you, contribute to it, send bug reports to it, but also make new ones. That shouldn't be the only one available in the world. And then checker tools, some of which we'll see this morning, The first one of those will also be available, and we're actually going to see in action in a few minutes. Thank you. The people on the bottom of the slide have contributed greatly already. If the people on the bottom of the slide, if you're in the room, or anybody who works directly with one of the people on the bottom of the slide, would you please stand up just for a moment? Please stand up if your name is on the bottom of the slide or you work directly with one of those people. [5:00] Thank you for everything you've already done to get us to this point. This is the work of many people already, and we hope many, many more. So thank you very much. Bjarne talked about a superset and then a subset. The idea is that C++ is very good at supersetting. This is progress. We get new standard libraries, we get new features, but we still have some of the more dangerous or older features available. Great compatibility story. What we would like to do is through core guidelines, extend that to have safe replacements for all the unsafe parts, Once you have the safe replacements, you have a complete system again, a complete toolbox, replacements for all the unsafe parts, then you can turn those unsafe parts off by default. They're still there. Nobody's taking them away. but you actively open the box when you want to make use of them. This helps us to have the goodness of compatibility but also start to evolve the language more than just by adding things. I'm going to talk specifically about type and memory safety. A lot of people think that C++ and type of memory safety shouldn't be in the same talk. This talk is about nothing but that. Thank you. The traditional definition of type of memory safety is really, if you boil it down, there's three factors. parts that go together. One is type safety, so I'm not going to pun a type and use a T as a U when they're unrelated. Then memory safety has two components. Bound safety, I'm not going to go outside the bounds of an allocation, and lifetime safety, I'm not going to access an invalid allocation. Put those three things together, and then you have what we commonly refer to in the industry as type of memory safety. There's a few variations in the definitions, but that's the crux of it. Now, those are the things we're going to focus on initially that I'm going to talk about today. We are also interested in concurrency safety. But that's not part of the initial target. Absolutely do want to get there too. And security and other kinds of profiles. But here's the guarantee, just to be very clear about what we are and we are not aiming to do. Thank you. C++ code compiled in the safe subset, so with these particular parts of guidelines turned on, ... is never the root cause of type or memory safety errors unless you opened the hood and explicitly said... unsafe me, go and let me do something that I know is wrong. In which case, you've documented the point you need to audit in your code. So these three parts, it applies to code you recompile. It does not apply to a third-party library you don't recompile. We assume you trust your libraries. If they have type unsafe or memory unsafe problems, you know where to look. But in the code that you control, you know you won't have root causes in the code that you recompile. You never need to audit that code for safety, that compiles cleanly with these rules. unless you opted out, and then you know, have it documented in source exactly the places where you said, trust me, that you need to go back and check if something does happen. This means we are not aiming to provide a guarantee of verifiable whole program guarantee. It is a whole program guarantee, but this idea of verifiable code that you see that some languages... talk about as their aim. None of the safe languages actually achieve that. Everybody, every program in any language eventually calls unsafe code. When you call unsafe code, an error in that unsafe code can cause an error anywhere in the code. including you can fall over in your safe code. But the root cause isn't there. The root cause is in the unsafe part. So really, this guarantee is very much like what any safe language guarantees, but we're being very explicit about it and candid about it. If you don't recompile your library, you say, yeah, you trust it. We're not going to check it for you. You need to independently use other tools to verify that it's correct. But hey, you have its source code? Compile it and we'll help you. Thank you. So I talked about profiles, and we'll talk about three specific ones. I use the term profile just to talk about a group of rules that hang together and achieve a specific guarantee. So we're going to talk about bound safety, lifetime safety, and type safety. And I'm going to go through a couple of them fairly quickly, because we're probably familiar with most of what's in them. But we do want to extend in the future, like I said, to additional forms of safety-- safe arithmetic, concurrency, possibly no acceptor, no alloc profiles, which are of interest to some people, but those are not ones that we've worked on so far. Let's talk about type safety. I'm only going to show you one slide about it. And this is sort of the summary slide we'll come back to and fill in. It's not complete, but it's to give us a roadmap and remember what we've talked about. So don't use a location as a [10:00] location actually contains an object of an unrelated type U. Now remember what we're doing. We're supersetting, then subsetting. The superset isn't just for fun. It's specifically to say, what are the unsafe parts that we want to ban that do not yet have standard safe replacements? Now C++11 got us most of the way. I'd say it's roughly 60-70% of the way. two type of memory safety compared to C++ and IDA. But it wasn't a coordinated effort to drive out and provide alternatives for every source of undefined behavior and unsafety to give a safe alternative to it. That's what we're attempting to do. So what do we need to add? Well, we already have much type safety. Most of C++ is already type safe. But it's useful to talk about a separate byte type because a raw byte, raw memory, is not a character. I know it's the same size and layout, I know it's the same bits, but in the type system the character is not the same as a byte, and it's useful to distinguish those two things. And a variant which is undergoing standardization right now And once you say that you can do things like say don use unions So here is type safety in the single slide I talk about because most of you are familiar with it The rules the first column are all about don do bad casting including static down casting that are unchecked, reinterpret casting, const casting away const, just don't do that, unless you really need to to call const on safe libraries, but like wrap them and do the cast in one place then. And number four, by the way, don't do C-style casts that would do one of those. Because remember, C-style casts can do any of those. They're specified as a, oh, try this cast. And if that doesn't work, try this cast. And then if that doesn't work, there's a cascade of four alternatives. Those same ones need to be turned off for C-style casts. So we still allow C-style casts syntactically. But the ones that would invoke those unsafe casts would become errors or warnings. The right-hand side, don't use a variable before it's been initialized, whether it's a member or a local, and don't use unions, don't use var args. Those are candidates for type punning. Now we were divided on whether to put safe math into this profile because you could view it as a form of type safety but as I mentioned before it really fits to be its own profile in particular because it has its own overheads which type safety does not but that one may because of the checking and narrowing So we also be interested in safe math but that not included in this particular profile. The summary is what's shown here. And there are still things we're working on for this particular profile. One of the things you will not see in the guideline support library that Bjarne talked about in the coding guidelines, the core guidelines, and you won't see in the GSL implementation, that first reference implementation, you won't see variant. And that is because the committee is very close to agreeing, we hope, possibly as soon as next month in Kona, on what variant should be. So since they're so close, let that continue. Why invent another one? On the other hand, you'll see with bounds, with array view and string view, we have taken the opportunity to take something that's undergoing standardization and say, ah, with minor tweaks, it could help us achieve this as well, which we can feedback as contributions for the Standards Committee to consider. So speaking of that, let's talk about bound safety. I've got an allocation, but I want to make sure that I don't access memory out of bounds. Here, the replacement types are array view and string view, which I said are already in a TS form In this model there are two main differences to the TS versions if you familiar with those The first is StringView isn read You can have a read-only one, but it's also read-write, because sometimes you want that. Furthermore, ArrayView and StringView are not two different types. ArrayView is the only type. StringView is just a templated alias to an ArrayView, which changes a couple of defaults. says, by the way, you really only want one dimension. We could not think of a reason to have multidimensional strings. If you think of one, let us know. Our imaginations were hampered there. And I want to point out that in the entire guideline support library, array view is the primary type. The only other one is not null that incur any runtime overhead. Everything else is done statically at compile time. But we do want full disclosure. ArrayView and then NotNull do incur some runtime overhead. And so we're working to make sure that we drive those down so that that's not a reason not to adopt these safe replacements. Once you have that, you say, don't use pointer arithmetic. If you look at rule number one, what's that really saying? Another way of stating rule one is a pointer only points to one object. [15:00] That's what pointers should do. That's what the vast majority of actual pointers in our code "do" is referred to a single object. Thank you. Don't use a rate of pointer decay. Don't use the unsafe functions. So how do we use a ray view instead? Let's talk about that. Here's an example that's adapted from actual code. The guideline support library was create--was iterated on and derived from actual code example. So we would take code from network protocol handlers, from low-level OS kernel routines, string manipulation, network transport, all sorts of different places. and actually work on the code examples and say, "How should these look with array view with not null, that kind of thing. And we went down many false paths as part of exploring a design space and backed up and came up with just a few types that hang together well. One of these examples and one of these types is what you see here. On the left-hand side, you'll see code, which if you're familiar with Microsoft Sal, the static annotation language, which are extensions primarily for C, in this case as far as they go for bounds C, safety. It says, hey, look, that pointer you've got there, things, and that unsigned count. Oh, by the way, those go together. Oh, OK. Static analyzer uses that information. And the static analyzer can catch some errors, such as, did you notice that you said not less than or equal to, instead of less than there? Don't want to do that. Now, a traditional compiler will compile that just fine, but many static analysis tools already will catch that error on the left-hand side. And things like passing the wrong size to memcopy, because we passed a count of things, not a count of bytes. Did you catch that? Or at the end, passing the wrong size, we say, here are 30 things, but really there's only 10. On the other side of the screen, you see we use an array view. The array view simply is a replacement for pointer comma length. Array view of t lets you say const t. If it's an array view of const t, that means you could read them all day long. You just can't write two of the t's. So there's a distinction, just like with pointers, of the array view itself being const, which means you can't point to something else. and the things that points to being cons, meaning you can't write through the array view. And of course, you can have both as well. You can have a const, a radio of const t, if that makes sense. just like pointers. It turns out that in many cases now, this makes our code simpler. You can use range four directly with this instead of iterating through the loop. You can call std.copy, and here I'm taking the liberty of stealing ideas from and anticipating Eric's Ranges talk on a Friday, because that is a rangified version, conceptually, of std.copy. which will do the right thing even if Thing is not a pod. So And then down at the bottom, notice that we can often deduce the sizes of things. We know the sizes of arrays and std arrays. We can deduce those for you and create arrayviews directly from that. That's implicit. It requires no extra code. But if you want to call the API explicitly the way you used to, like for instance, the old API you might have called with pointer, length, Let's say all your call sites had that. Well, notice the second call at the bottom, Generally, most of the time, especially if they were in that order, point or common length, just put braces around them. We have the nice uniform initialization rules in C++11. That just says, oh, by the way, I know you're trying to use an array view. Here, let me make one for you. without repeating the type. So that's very convenient for compatibility. And of course, if you lie and say the wrong number, Now we can enforce it because we know that what you said isn't true, and we can check that as a combination of statically and dynamically. Now, in all of this, a key goal was not to have any serious regressions from what static analysis could already do. If the after lost valuable information that you had with sal annotations, for example, or the similar annotations you have on other platforms with other compilers, then we've lost something serious. That rarely happens. We preserve nearly all the static analysis information because the static analyzer the compiler knows ArrayView. This is a popular type, a vocabulary type, that not only all programmers can agree on, but the compiler, the tools can know about and know the semantics of, like any standard or hopefully standard type. and And so it gets all the same information out of that, that it got out of the cell you see, plus many of the checks are now done at compile time using the type system. There's also a fixed length array view where you can just do the computation at compile time in code, no analysis required. So we have no regressions from the analysis, and the analyzer has less work to do, because we're using the type system and the target is zero call overhead. [20:00] So as you do the actual use of the array view to index, you may get balance checks there. We want to hoist those. But just saying that an array view is the type of your parameter, should not cause overhead in calling the function compared to just raw pointer and length. And so we're working very hard to make that be true. Now, what if you really did want to use something unsafe? Well, here's an example of how you might opt out of bound safety. Most of the time, I don't want you to do that, so this is the only slide I'm going to show the syntax. But you might have noticed in other languages, often you see the keyword unsafe on a block. which basically says, okay, now you can do unsafe stuff. And that's where I started with the design as I was working on this. And several people gave me very good feedback that, you know, that's not very satisfying. And the reason is, is 'cause it lets you do too much. It's A bit. It's like, oh, is there something unsafe in here? Maybe all you meant to do is a bounds unsafe operation that couldn't be verified at compile time by the tool, but that you knew was safe. But because you said unsafe with no ability to distinguish what was unsafe, you might do that and an accidental union use, or type punning, or something else. Instead, we want you to be able to suppress specific core guidelines rules including profile rules and say exactly what it is you're saying trust me on in this line of code. Suppress type.cast. bounds if you want to just escape the bounds profile. And you can suppress a specific guideline rule number in the guidelines that we're publishing. This is the model that we're following, so you can be very crisp and self-documenting. And that way, if you suppress one thing, you don't accidentally suppress warnings on other errors you might make. You still catch those. Thank you. And here's an example. If you do want to use it here, you can say, "Suppress bounds. Darn it, I do want to call memcopy. Just trust me." And even there, you're still getting some other benefit, because by using an array view, you can say, "Hey, give me the number of bytes." the number of things, instead of the number of things, and it solves other kinds of errors, even when you do use the unsafe APIs. Another benefit of these, which isn't related to safety, but it's a convenience thing, is let's say that you wanted to write a piece of code that could take the storage that's contiguous that might be stored in a vector or an array or a pointer length, and you wanted to write a single function that could accept any of those, well, one option is to write a template. that just gets stamped out for every contiguous container type that it might be passed. Better, just say, I want an array view of ins. And then you can call that with whether the storage is owned by a std vector, pointer length, an array, or your own container. This is even more compelling for strings. How many of you have your own homegrown string type at your companies? Don't lie. Right. And those of you who didn't put up your hands and are just bashful, we know we have them. A bunch of these are Microsoft ones. There's cute ones up there, I-I-C-E ones up there. And this is certainly not a complete list, because we just don't have enough slide space in the world to show them all. But if you write just the function takes a string view or a W string view, You can call it with all of these. Now, the nice thing is, because StringView is a well-known type And you get the wide character alias one as well with a W, and the narrow character one. That means if you have your custom string type, All you need to do is write a conversion operator from your custom string type to string view. And now you can just pass it to everything that takes a string view, which we hope is going to be much more and more code, because that's the way to specify, I want to see... string data. So that's the Bound Safety Profile. It's about not going beyond the bounds of an allocation. The main replacement tool is array view and string view, which is... an alias for array view. We ban pointer arithmetic. We ban out-of-bounds array accesses. And what we're working on now is to make sure those things are as cheap to pass as parameters, as a naked pointer length. So that affects calling convention and ABI. So that's one thing that we're investigating. We believe we can do. but I'm documenting where we are and what the next pieces are. And we want to make sure that we eliminate redundant checks so that the the checking overhead for doing the balance checking is... easily adoptable, is not a barrier to adopting this, that you don't want to turn it off. in Safeco. And that brings us finally to the lifetime profile. And this one's easy. We can talk about this in a slide, because really only you need to do two, three things. Delete every heap object once. That way you don't have any leaks. delete it only once, that means you have no corruption, and never deref a pointer to a deleted object, and then you have no dangling. So how many of you knew that already? Good. Okay. So that's safety in C++. [25:00] Thank you. Thank you. Okay, you're not really going to let me get away with that. Amen. It's really easy to say what the requirements are. It's way harder to do it. Thank you. Thank you. A lot of people have spent large fractions of their careers on this problem. Thank you. And I'm very grateful especially to Bjarne Struistrup and also to Neil McIntosh and others who are in this room. For a few years I've been talking about taking a run at this, and just over the past year, have taken a serious run at this. It's a lot of work. We believe it works, but we are also... going to give you a snapshot of where we are with the design. There's a paper that's now published on the CPP coding, the core guidelines repo. that talks in more detail about what I'm going to show you, so I refer you to that paper for details. But this is a hard, hard problem that people have tried to solve for native languages for 40 years. There are many attempts that either failed or blossomed, but took us in other directions. For example, Many attempts, like Cyclone, C-Cured, focused on memory safety for seeds. eliminating dangling and leaks for C, on the grounds that C is a simpler language. That's only half true. C is a simpler language. C++ code is simpler than C code because it contains more information. It is simpler to deal with C++. That's one observation. The second is they may incur runtime overheads, such as default on or expectation of a garbage collection. and many languages have gone down that road. But that doesn't meet our requirements, because we want zero overhead. This is C++. Others rely on whole program analysis. I mean, even at Microsoft there are two tools that you might have heard of, Prefix and Prefast. Prefix is a wonderful, static analysis tool that works on a whole program basis and catches lots of good stuff and errors deterministically. And Prefast is the one that's actually run, because this one can't scale to large programs and can't run until you get to universe heat death kind of time frames. you need pre- which is mostly right and makes some guesses and has more false positives. The whole program analysis doesn't get us there. It doesn't scale. Extensive annotation. Many languages have gone down this route. Rust very recently, and also Cyclone and others, rely on extensive annotation, which are difficult for the programmer to understand. and show that there's really something missing in the source code. The less we have to annotate, the more we realize the information is already there. in the source code. Or they invent a new language, or one or more of the above. So we believe we have something conceptually simple. I'm going to walk you through it, and it's going to rely on the fact that C++ contains really useful information. It has a very strong notion of scope. It has a very strong notion of lifetime. It has a very strong notion of bounds. And when you put those together, and look at the ownership information already in the code, it turns out that you can do a really good job with what you find already there. So thank you again to Bjarne and Neil in particular. But now let's dive in. We're going to say we don't want any invalid or deallocated allocations. No using dangling pointers, no leaking memory. I added the slide last night because on the Reddit thread on Vjarna's talk, somebody said something along the lines of, yeah, because pointers are evil, and no, they're not. Of course you want indirection. Smart pointers are good. They encapsulate ownership. Of course, don't use them everywhere. use them where they encapsulate ownership. Raw pointers and references are just Fine. and you really want to use them on the stack, especially as locals and especially as parameters, for parameter passing by reference, by pointer, because you want the speed and the leanness of just an address. Now, having said that, you want that without dangling. Let's talk. Here is the overview of the lifetime safety profile. The main concepts, notice these aren't types, they're concepts. An indirection, something that points to something else. And there's really two kinds. You either own the thing you point to, or you don't. You're observing it. If you own it, like a unique Pitter, you can't bangle. If you're just observing to it, recall that capital P pointer, because it could be anything. It could be an iterator. It could be a raw pointer. It could be a range in the new ranges proposal, pair of iterators. Anything like that could be an array view, could dangle. We want to make sure that you don't use that reference to something it doesn't own longer than it's good for. So for the owners, we'll make sure you delete everything exactly once, proper cleanup. For the pointers, [30:00] Make sure you don't dangle. This is the goal. So Because right now, T* is ambiguous as to whether it owns what it points to or not, as Bjarne pointed out yesterday, there's an owner of T* wrapper that you usually use. If you must use a T-star, you can't use a unique putter or something better, that's fine. But put owner around it. It's just a typedef. It's just an alias. but it's known to the tools and to the rules, so we can enforce that you do delete it properly. So now, let's dive in. The approach is that we have local rules, so we do not want to rely on whole program analysis which doesn't scale. A compiler just compiles a function at a time. That's the model we want. And we want to enforce it statically at compile time. That way you have zero runtime overhead. So I said a ray view for bound safety has some runtime overhead. Apart from the not null wrapper, which fits into here, nothing that I'm going to show you has any runtime overhead. There is exactly zero instrumentation. There is zero analysis or bookkeeping. These are all static rules run at compile time. Could be in the compiler, could be in another tool, in the build-to-set-for-static analyzer. either could implement these things. You get whole program guarantees by compiling the whole program, a function at a time. Thank you. We're going to identify who are the owners, so every object better have an owner, and we're going to track the pointers. If you're a pointer, we're going to track statically what do you point to. The goal is leak freedom for the owners and tracking points to to avoid dangling for pointers. Amen. And we don't want you to have to go annotate heavily throughout your code. Because that's a lot of work. Most of the time, the information's already there, and we can select defaults that are correct and are what you would have expected. You'll see them in a moment. Thank you. The principles. A pointer tracks its pointees. We track the outermost object. If you've got an int that's a member of a class, and you've got a pointer to that int, I'm not going to worry that it's a member. I'm just going to track the enclosing object, because I know the lifetime of that int is the same as the lifetime of the enclosing object. That's just simpler. Same for array elements in the array. For a heap object, I'm going to track the owner of the thing I point to. Thank you. When I call functions, I'm going to assume that any pointers I pass in are independent, by default, and are going to be valid for the lifetime of the function. And if I return a pointer back out, I'm going to assume by default that you got it from the inputs. which is 99.99% of the time what happens. That's pretty much it. Those principles are pretty much the static analysis rules. However, we need the next level of detail. So let's start digging into that. First I'll talk about local analysis. We'll look at the rules, warm up a little bit just inside a function body. Thank you. Then in Acts 2 and 3, we'll talk about calling other functions. And I'll show a few function calls in Act 1 just as an aside, but we'll really dig into the function calls in Acts 2 and 3. um So here's a warm up. Let's say I have three pointers which are outside of scope, so I can make them outlive something. This is the kind of error I'm trying to construct. Initially, they point to null. Then I go into a scope. I create a standalone int. I create another int that's a member of a structure. And just because I can, I make a certain array of integers. And I'm going to point at the stack int, at the int member, and into the array of int, at one of the ints. Before I leave the scope, all those pointers are good, right? Why are they good? Yeah, they're stack variables. And you haven't left their scope yet. They're not destroyed yet. As soon as you pass line A, what happens? Yes. That's where we destroy A, S, and I. We invalidate P3, P2. to P1. If I try to use P1, I should get an error. P was invalidated when I went out of scope at line A. Just curious, I know some tools already give you this error. How many of you are using tools today that give you errors that are roughly as readable for that second last line as, "P was invalidated when I went out of scope at line A?" How many of you use such tools? I see about four hands. Got about over 700 people here, so about five or six hands. Maybe 1%. Thank you. And then the second line, the line at the bottom, of course, we're trying to use two that have been invalidated. Those are dangling pointers as well. And we would like similarly good error messages for those. I was thinking of calling Neil up to demo those errors, but, you know, some tools do those today. These are pretty easy. A human could do it. Let's move on. But those are the errors we want diagnosed with that clarity of [35:00] Now let's see how taking an address works. I have an int, and I take a reference to it and a pointer to it. Remember, a reference is basically a pointer with different semantics. Sometimes in the language, it's interpreted as an alias instead of an indirection, so it's got this dual nature in language semantic terms. But for our purposes here, each is an indirection, each is a pointer, capital P. ri points to i, pi points to i. If I then take int star star ppi equals address of pi, we don't need any special kind of information here. For the first couple, we said ri points to i. We just track for ri, what does it point to? Remember, we track this statically. We say pi points to i. Well, what does ppi point to? It points to pi. And what does pi point to? Well, you can look that up separately, but we only need to remember one level. So what do I directly point to? But then you can follow pointer to pointer to chains once you've got that information. If I've got a shared putter, for example, at the bottom, I've got a shared putter S which owns. That's why I'm using the darker color for an owner and an owning pointer There an owning indirection It owns that with the value 2 that on the heap And in the final line I take the address of the shared putter He's an owner, but that's okay. So this ps points to s. It just happens to be that s is an owner, but that doesn't matter. It's ps points to s and is valid for as long as s exists. Make sense so far? These are baby steps, because then we're going to start running. One last warm-up. Dereferencing. So let's go back to a condensed version of the previous example where we have PPI points to PI points to I. Okay? Now I create a new pointer, PI2 equals star PPI. How do I know what that is, and how do I get the right lifetime for PI2? Well, that's easy, because before I do that, PPI points to PI, PI points to I. So when I say that expression, star ppi, I dereference ppi. Well, I know ppi points to pi. Therefore, star ppi is another name for pi. So really, that line is the same as in star pi 2 equals pi. I know this statically clearly which means not only do I take his value the address stored in him but I copy his points to list because I going to point to the same thing he points to I go through star PPI. PPI points to PI, so star PPI is the same as PI. When I copy that pointer, I copy not just its contents, but also its points to list. And so PI2 points to I. These seem like very simple rules, but we have to baby talk to a computer, because it only understands little simple rules one at a time. But on this, we can build a lot. Now, if I have another variable, j, and I change that middle pointer, that first pointer, pi, to swing him to point to j, this is kind of cool because now if I dereference ppi twice, I now point to something else. But I didn't have to modify ppi's points to list. I just know that ppi points to pi. That's all I need to know is one hop. And then if I now reset pi to, I have that same line of code I had in the middle of the screen, and I do it again. This time, PI2 correctly points to J, and I copy the points to list that says I'm pointing to J. Enough warmups. We are at EOW and BOF That was the easy stuff I showed you taking a pointer to a shared pointer. Now let's talk about taking a pointer to the thing owned by a shared pointer. Now the thing about this is, the shared pointer's got a name here, it's S, right? What's the name of that int on the heap that the shared pointer owns? Ain't got one statically, right? It's okay. We'll invent one. His name is S prime. That's shorthand notation for what is owned by shared pointer S. So then when I say instarp equals S dot get, we'll get to function calls in a moment, that gives you a pointer that points to S prime. Now the rules are for that is that pointer is valid. How long should it be valid? Anyone want to take a guess? Until when? until S is destroyed or modified. Hmm. How do I know if something's modified? Oh, Bjarne invented this wonderful thing called const that C took as well. We already have that information. I know exactly the set of functions on shared putter [40:00] That destroy it and modify it. Until one of those is called-- P is just fine and dandy, but as soon as you call one of those, like reset it or swing it to something else, I know P has been reset. Again, how do I know? Because P points to something owned by S, and that means that For S prime, we say P is valid until S is destroyed or modified with a non-const use. because that could point it to something else. Now, it's true, there could be false positives here. S equals S. OK, it wouldn't actually move it, and we would still make that line-- call that line an error, star p equals 43, Now you could know, yeah, but I didn't actually swing it. That's OK. I'm willing to take some false positives. We're going to work to drive those down. but I want soundness. And I know that if you modify that shared putter I can no longer trust that pointer. that depends on, that's the way to think about it, that depends on the current value of that owner. That's Sherrod Putter. Make sense? How many of you believe right now, after seeing one slide, in your heart of hearts, "Yes, I believe a tool could easily detect that"? laughter Thank you. The rules I just said I want to emphasize are not specific to standard smart pointers. There is nothing hard-coded in these rules I just described that say, do this for shared-putter. It's just, look, there's an owner. It happens to be shared-putter. If you modify it, you invalidate the pointers you got from it that point to things it owns. It is not specific to that smart pointer. It will work for your smart pointers if you identify them as owners. And it's not specific to SmartPoin, it works just fine for containers. But let's see, could a compiler really do this? Neil? Where are you? Could you come up, please? Neil has been busily in his team, busily writing a prototype to actually make sure that these rules will actually work. Based on the static analysis framework that Microsoft was already using, that works with, we talked about with prefast and prefix, but that also works with sal annotations. Well, we want to use array views instead. We want to use these lifetime rules instead. So can you show us this example and what it looks like? Is your mic on? I think so. Is it on? Yes. I am on. Okay, so I think this, at a much larger font size, is approximately equivalent to what you just had on the screen, correct? Right, so we have a shared pointer. We get a pointer from it. We dereference it, this program looks correct if essentially useless. I don't see any errors. There are no errors. The error list is empty. Let's see what happens. Thank you. if we repoint it. Thank you. Thank you. You used 2, correct? Sure. Any value. 42 is pretty good. Thank you. with the eyes. So then what we can do is we can take this pre-production version of Visual C, and we can run code analysis on that example. and we can see there's a little icon that says it's working and oh now we have an error let's see what it says Thank you. It's pink because pink's pretty. And it says don't dereference the invalid pointer that we just created because at line 8 we reassigned S. And in fact it tells us here at line 8 we used operator equals S. to break that point up. Thank you very much. Now, don't go too far because we'll want you back in just a moment. Sure. Thank you, Neil. So that's one example. But again, you didn't hard code anything specific to shared printer in the tool. Nothing at all. It's you've modified an owner. We know the pointer points to the thing owned by. S. You modified it. you invalidate the pointer. Next example. Here is an actual bug from Stack Overflow a few months ago. When I was writing up this part of my paper, I was looking for an example, and in my morning mail, before I could think of an example, an hour before, this one came across the transom. And I thought, oh, I'll just cut and paste that. The exact quote was, "This code compiles, but RA contains garbage. Can someone explain me?" why this code is invalid. So what's he doing? He's got my fun, which returns a unique printer by value. That's OK. It moves it out. Then he's calling it, and he's taking a reference to the returned that's owned by the Unique Pitter, And so what's wrong with that? like there's Yes, it returns by value, which is fine because it moves. Yes, it's an R value, it's a temporary, which is then destroyed just as you're storing the reference to that destroyed object. Well, that's not good. So, if we call that returned object temp up, what's really happening is that we're making a reference [45:00] Think of it as a temporary reference. The result of my fun call-- That gives you a reference which conceptually points to something owned by Temp-UP, which is that temporary that came back. as soon as, then you store it, but then as soon as you destroy Tempupi, You invalidate just as if you've modified it, you invalidate that reference. And so we say, hey, look, in fact, I would give an error right on making the reference, because it can never be useful. If you initialize a reference as invalid, that's just useless. And if you try to use the reference, you'll get the error saying, in both cases, R-A is invalid. It was initialized as invalid on line A. Now, the user said, "Can someone explain to me why this code is valid?" And some good Samaritan did. Several did. Wouldn't it be nice if our compiler just told us that, and we could save ink on Stack Overflow? That would be so much better. And this is not the first time. Somebody has asked this question. Could a compiler really do this, Neil? Well, I hope so. Otherwise, we're going to feel very silly. But by the way, you'll have noticed that Neil is wearing the local colors. Turn around. Go, Hawks. This is a noun, not a verb. And you wear the design meetings this time of year quite a lot. And I thought, hey, it would be great if you wore this on stage, because it shows that we're loyal and we're optimists. And those are two good things when you're dealing with C++ and pointer lifetimes. Go ahead, Neil. Yeah, that's right. Just here so I don't get fined. So... Should we start with the error-based example this time? OK, so let's see what happens if we run analysis on example 2. It goes away, it has a little bit of a thing. Oh, there's an error. Turns out we're dereferencing an invalid pointer again. And this time, it's trying to tell us what Herb went through on the slide, which is that RA was invalidated, if you can read the text and the message. At line 14, the light pink line, was invalidated by a destructor of unique pointer being called. And why would that be being called? - Because you returned to temporary. - Right. So, now, how would we fix this? Well, there's a couple of ways to fix it. One way would be just to store the unique putter. In fact, that's what the Stack Overflow answerer said. Okay, so we should probably change what we're putting it into, correct? Well, what you can do is have a... Auto P equals my fun. Okay. Let's leave. That there then. Is that capital A? Yes. So you mean auto-P? Whoa. Somebody changed my keyboard. Devorak keyboard. Yes. Auto-P was my favorite. How about that? - And then that one can just DRFP. That should be safe, right? Because now we're actually keeping the unique pointer, which is helpful. Okay, should we try? Thank you. The worst that can happen is we find a bug in the prototype. True. or our brains. thinking, oh, no errors. Looks like we fixed the bug. - Okay, so that's one way to do it. Now, that's what you normally tell people. That's what we were told on, on Stack Overflow, if you just Ctrl+Z back to the original. Now, some of you might have noticed, you do not need to know the following. I am going to stick with simple examples, because they're plenty enough. But many of you know that there's advanced parts of C++. you notice there's a const reference local variable. Why did that not do lifetime extension? for the temporary. because it was two different types. It was a reference to A, not a reference to the unique putter. If you don't know about lifetime extension for const reference locals, good. You shouldn't need to. The only reason I mention it here is because we're going to stick with simple examples But if you do and know and want to use the advanced stuff, it works too. And I'll just use this as a case in point and then not mention the advanced stuff as much. But in this case, the simplest thing you could do to change this is to change A to auto. Thank you. And then you don't need to deref. You can move the deref down to the use line. And this should do the temporary lifetime extension. If you don't know what that is, don't worry. Just ignore this part. But this ought to work, because now the temporary lifetime ought to be extended. And so if we rebuild this, Really pushing our limits here, let's see. Thinking, thinking, no errors. We succeeded. Thank you very much, Leo. Thank you. Oh, we're going to need Neil in a minute here. This one's hard. Let's go a little bit more exotic. Let's say you have a shared point or two of [50:00] And I'm trying to help with diagrams off to the side. And we take pointers to every single level. So I have a shared pointer to vector, that's sv down at the bottom. And it points to a vector which has no name. It's just the vector owned by the shared pointer. And it could... It owns a buffer of INTs. And I'm going to have sv2 be a pointer, a raw pointer to sv, which is an owner. I'm going to have vec be a raw pointer to the to the vector, which is an owner, and I'm going to have putter be a raw pointer to the integer that's owned by the vector that's owned by the shared putter. Clear as mud? This is what we ask compilers to do all the time. We're so mean. Now, if I use that putter right now, it's fine, because it's pointing to a valid integer, and I got it through a valid path. Now, what could I do to disturb this world and upset this apple cart? Oh, there's lots of places to kick. Let's kick at Vecca. So the first thing we'll try is we'll say VEC pushback one. Now this might not actually move memory. So the pointer might still be valid-ish, but according to the standard, it's invalidated. Now, the standard says that in English text. We'd like to say it... with a deterministic rule that a compiler and a tool can enforce. So we say, that pushback one invalidates PUTR. Now why? Now here's where I'm going to show there's a second level of prime. Now there's only two. If you say, sd prime prime prime, we're only going to track two levels. So we're going to give you a few false positives, but it turns out that covers many of the interesting cases. So here, what we're saying is that SV2 points to SV, Beck points to SV prime. I'm reading that blue bar on the N. and Pudder points to SV prime prime, that is something owned by something owned by SV. Thank you. When I say vec arrow, that's the same as dereferencing it. That's the same as star vec. So well, what does vec point to? Vec points to, star vec, is sv prime. So vec arrow, think that means sv prime. That is an expression which is the name of that unnamed vector. I then call pushback. That is a non-const operation. Therefore, it invalidates anything with another prime on that, anything that prime, which means it invalidates sv prime prime which if you look at the three inputs, it invalidates only the pointer. because it depends on something that depends on the shared footer. So we can say error. PTR was invalidated by the pushback on line A. I have been dying to get that kind of error message for many, many years. But now let's go back and do something else. So we'll reset to the original state, so same as before. But let's instead kick at another place. We'll kick at the shared putter. And let's just reset it. So that's going to blow away the vector. It's going to blow away the memory owned by the vector. But when I say star sv2.reset, what does sv2 point to? Well, if you read the table, It says, "SV2 points to SV." So star sv2 is another name for sv. I'm going to do a non-const operation on sv. That means I invalidate SV prime, Ah, well, Vec, points to sv prime, so he's invalidated. And putter points to sv prime prime. Well, sv prime is part of that, so he gets invalidated also. So I invalidate both of those pointers. And if I try to use them, vec or putter, I say, error. They were invalidated by the reset. on line B. My one question is, Could a compiler really do this? What is there to lose? It's just public humiliation, right? Let's try it. We're so used to that. Thank you. I think we're up to example three. So this one's going to take a little longer because a lot of stars, a lot of ampersands, and brackets, and they all cost more. Let's see. But initially that should be fine. Let's just make sure the baseline works. Because right now I set up the pointers and all I do is use them. I use the pointer to the vector and the pointer to the int. Does it agree? It all succeeded. Okay. So the first thing we wanted to do was modify the Vector. And there was a more complex expression In fact, I can't remember what the acronym was. Is it called Vec Pushback? Is that what you did? Yeah, that's probably it. Vec Pushback. You like 42. Thank you. at the time. Douglas Adams will be proud. Let's see what the tool thinks now. And so now we've modified the vector, which could move its memory, so we should invalidate only the pointer. What does it say? Oh, it's got an error. Let's see what it says. Ah, so the error is on line? It says the error starts at line 12 where you hold standard vector pushback. And the actual dereference, the problem is? Line 14, where we're derefing an invalid pointer called [55:00] ETR. Okay, so now let's go back and undo that, and instead just modify-- SV2, reset SV2. That's the other thing we tried to do. So just SV2 error reset, I think, is what we did. Thank you. Thank you. Thank you. like so. So now the share pointer to the vector, we're resetting it, so the vector and the storage it contains should go away. which means the pointer to the vector is also invalidated, as well as the pointer to the integer inside it. Ooh, now we have two warnings. What's happening here? Okay, VEC is still being invalidated. It's being invalidated by the call to reset. And that was not invalidated before. No, sorry, it was pointer. And there's pointer being invalidated as well. So we've caught both invalidations. Thank you very much, Neil. We'll give you a pause. By the way, If you were looking closely at this, this might seem somewhat magical, Because I'm using unrelated aliases unrelated to the path I got the pointer from. If you notice where I got at the top of the screen, where I got the pointer from. So I got the pointer from SV. through the path of SV, right? But look where sv2 is. It's off to the side. It's just a pointer to sv. It's unrelated to anything else. I never used sv2 for anything before that last line in the function. So that's just an alias. But because I know sv points to sv, when you know what things point to, you can do a lot. I know sv points to sv. Therefore, it is an alias for sv. Modifying star sv2 is the same as modifying sv. And even though I got these pointers from unrelated paths, The correct invalidation occurs because I know just at one step, he points to him. or he points to an object owned by this owner. Thank you. So let's just summarize the rest of what happens locally in functions. If you have a branch, you might point to more than one thing. You might assign p to one object, or to another object on different branches, that's OK. We just merge them and say P might point to X or Y. and follow that through. Loops are very much like branches. If you have points to null, you can remove that with a null check. Try-catch is handled. We make pessimistic assumptions about all the invalidations, and none of the revalidations in the try, if you get to the catch. For more details of these and examples and walkthrough, see the paper. But now let's talk about calling other functions. Because everything so far has been local. I mean, if we did nothing but this local stuff, we would catch boatloads of errors. And I'd be very happy with that. But people keep calling functions, so let's do something about that. Thank you. Thank you. What do you need to do to make that true? You need to guarantee that he lives long enough, for the lifetime of the function call, and you need to guarantee And this one's subtle, and if you've been bitten by it, you will appreciate this right away. You have to guarantee that F, as a side effect, cannot accidentally invalidate, including through reentrancy, whatever is keeping P alive. Now, that was a long phrase. How many of you recognized it and say, oh, yeah, I've lived that? Way too many people. That's the kind of phrase you shouldn't get at first blush, and you only get when you've lived it many times. Let's talk about the rules. In the callee, now, if I'm inside a function and I get a pointer parameter, I'm just going to assume it's good for the lifetime of the call. Yeah, I don't care it fell off the end of your uncle's truck. Looks good to me. I'll trust you on this one. Now, That's initially, if I then reset it, if it's a non-const pointer, then I know it's lifetime and I've done something else with it. But by default, with the value it came in, I assume it's good for the lifetime of my call. And in fact, that's nearly always true, because that's what nice stack nested scope semantics gives you. Now, how do we make that true? We enforce that you don't pass anything that you know that the colleague could invalidate. This is the number one problem people come across, number one correctness problem with smart pointers, because they're not using them correctly, but it's a subtle thing, and it's easy to make this mistake, so let's diagnose it. Uh, You've got two functions, f that just takes a pointer, and g that takes a shared pointer and a pointer. Okay. We start with a global shared pointer, GSP. Now, in my function, I'm going to call f with gsp.get. Now, if you just look at that first line, That's in red. Call f with gsp.get. What's wrong with that? even without reading the [60:00] And I'm like, what's wrong with that? F could invalidate that. It doesn't know the pointer you passed happens to be pointing to something owned by the global variable GSP, which hint is a global variable, F can change it. So f could, or something f calls, 100 function calls down, could, as a side effect, write to GSP. And now what do you say about that pointer that you handed that poor function? We should be ashamed of ourselves. We gave it a pointer it could invalidate. What's the solution? Now, in my CPPCon talk last year, we covered this material, but we covered it as English text saying, thou shalt and thou shalt not. So when you pass this, At the top of your call tree, where you're going to convert a shared pointer or any smart pointer to a pointer, take a copy once, so you're not taking a whole bunch of increment decrements, you take one local copy, of that shared pointer in this case. And then you get a raw pointer from that and you pass it down. That gives you a reference count that the quality cannot possibly change. And it completely prevents that pointer from being invalidated. But you have to remember to do that. Now, how many of you knew that that's what you're supposed to do? Take a copy of a shared putter before you turn it into a raw putter and pass it down a call tree? Good. Wouldn't you like help for when you forget? And the majority of us who didn't know it or don't remember it all the time, wouldn't we like help, too? Now, when we call it G, notice that if we call G with a shared pointer and .get a pointer we got from that same shared pointer G takes... a shared pointer in ref. G could modify the shared putter. Now, G has no idea, whether the int star you gave it points to an int that's owned by that shared putter, If he modifies it, he could invalidate the pointer. So we're going to say, even though this could be safe sometimes, we don't know. Furthermore... Dollars to donuts, you don't know. And we're going to say that that's invalid. However, it's perfectly valid to call G with one smart pointer and a pointer that doesn't point into it, and we know. because we know what pointers point into. sp.get gives you a pointer that points to sp prime, points to the thing owned by sp. GSP? Perfectly fine. By the way, even if they alias, that's still OK, because modifying GSP-- won't bump the ref count down to zero. and it won't invalidate the other pointer because you've got the local one. So even if they point it to the same thing, that last line is fine. Thank you. This is the number one correctness issue using smart pointers. People say, "I use smart pointers religiously. "I use them faithfully. "And then I call down, and I call into some other language, "and I get re-entrancy, and just sometimes my pointers "get modified, and things dangle, and I don't know "when the side effect happens." And this is the kind of advice we give them, is to get those local roots before you turn it into a pointer. It only costs you one ink, and then you get this free raw pointer all the way down your million-line call tree. But wouldn't it be nice if a tool could help us with this? Let's see. Maybe we can be smarter about smart pointers, right? Let's have a look. Must be up to example number four, is that right? Yes, oh there it is. Okay, so you said this was bad code, right? Yeah. Let's see if it is. Thank you. Thank you. Thinking, oh, looks like the tool agrees. So what does it say? It's telling us that the result of calling get on GSP could be accessed and invalidated by F itself. Pretty much what you said. Okay, well the way we tell people if they get this, you don't want to just give people errors on their code. You want to say, here's what you do instead. Well here, what would you do instead? You take a local copy of the pointer, right? Before you pass it down that call tree. The smart pointer, that is. So the easiest way to do that, there you go, And then remember to call Remember to scroll off the screen. And then we pass sp.gat. Right. We save. Bye. I see, does the software agree with the people? And yes, it does. Cool. Now, I believe in our current tool that if you pass gsp.get, you would still get the error, I think. continue working on the shared ownership model, and make that be a non-error. Because as long as you've got some local route, We might be able to reason about that there is somebody keeping it alive. Right now, we don't go to those efforts. We just say, We just know who you point to, and it's to somebody else. And that's good enough. So now, try calling G Let's say we called g instead with, what was it? sp, sp.g. [65:00] Yeah, that's one way to try to freak it out. See what it thinks. Thank you. Thank you. So now g is getting a reference, modifiable reference to sp, and a pointer derived from sp, although it doesn't know that. And we did the wrong thing again. So this time, result.get is invalidated again by G. Ah, now this is interesting because this is not quite the error I told you about. because there are two rules that catch this error. The one is you don't pass something, a pointer, to a function that it could invalidate, and it could invalidate SP because it gets a non-const reference to it. But this is caught by the more basic rule we already covered, which was, hey, look, that's a non-constitutive SP. Even before you try to make the call, you invalidated sp.get. because the act of attempting to pass SP to something that takes it by non-const, we assume it's going to modify. That is a non-const use of sp. So either one of those rules, even if you turn one of them off, to make that code compile, you'd have to turn two rules off. to make that code compile. Thank you very much, Neo. Pleasure. That's cool. But don't go too far, because we still need number 24. Smart pointers are great. They're commonly misused. That is the number one correctness issue with smart pointers. Re-entrancy, the side effect of accidentally kicking the bucket. that your pointer is standing on. Don't want that. The number one performance issue with smart pointers, that's not about lifetime, but I covered that last year with the guidance of how to pass parameters, and people pass shared putters way too often when you should just pass raw pointers. Only pass the smart pointer when you want to say something about ownership, ownership transfer, ownership sharing, that kind of thing. So other guidelines deal with that. With these two sets of guidelines, we can remove most of the reasons and misconceptions people have about using smart pointers incorrectly. I won't talk about that guideline, because it's not about lifetime, but this is two sides. The two number one problems of smart pointers, we believe, are solvable and solved. Now, sometimes you want to override the defaults. In STL, there are a bunch of functions, let's pick on insert, There's insert with hint that takes an iterator. Now that iterator had better be into the same container. The English text says so, and we all know we can rely on the compiler to respect English text. Yes, there's an uncomfortable mutter there. And so we want to say, hey, those two are the same. Now the default I said for pointers passed in is that they are independent, So we would have to override that and say, no, the this pointer, which is an owner, is related to the iterator passed in. So we could say that with the first overload of insert. The draft syntax for that-- we're still figuring out the syntax-- but the draft syntax would say, hey, make it be the same time as lifetime of this, or lifetime of this prime. And then they're related. Thank you. Now, Then there's another overload of insert. I pick on insert because it's a handy function that has an overload, with different lifetime rules, because the other overloaded insert takes a range. This is the range insert we've probably all used. That had better be pointing to somebody else, not into me. Because I'm taking... values I'm seeing over there and inserting them into my own representation. In fact, the standard helpfully says, in English, text that compilers-- can't do much with. that that iterator pair had better be not into my container. Furthermore, that's okay, that part's the default, is that the pointers point to different things, are independent. But In this case, we also want them to point to the same thing, so we don't accidentally... pass an iterator into my vector and an iterator into my other vector. which is not a range. So here we want to say, as shown in the last line, that the second parameter has the same lifetime as the first. It points to the same thing. as the first. And you know what? We can do that. So if I have a map with a couple of entries, and I have a second map, M2. So I've got two maps, M and M2. If I say M dot insert M2 begin, some value. Well, I'm going to get an error under these rules because m2 begin points to m2. not to M. In this call to insert, I should be passing an iterator into the same container, and I'm not. So I get an error. But if I say m.insert mbegin, the green line, everything's OK. So that's how I fix it. Now if I call the second one, if I call m.insert, m.begin, m.end, that middle line. Well, there's two errors. The parameters point to m, which they shouldn't, and m is modifiable by m.insert, so I actually invalidate those iterators right away. Badness. [70:00] So we don't want that to compile and we don't. But what if I say the third red line, m.insert m2begin m.end. So I'm passing an iterator pair that isn't really a pair. It's the two different containers. Error, param 1 points to m2, but param 2 points to m. And the error message can be that clear. They should point to the same thing. If they do point to the same thing, last line, That works. Remember I said all this is done at compile time? Thank you. We believe that this will eliminate large classes of iterator range errors with zero debug instrumentation. purely at compile time. We have all passed invalid iterator ranges, where we've passed begin of M1 and begin of M2, and the wrong thing happened. We aim to catch those, because iterators are just pointers. They're capital P pointers. They indirect to something and they don't own the thing, that they indirect to. So now finally let's talk about pointers you get out of functions. And again, when I say pointers, I mean capital P. It could be an iterator, it could be an array view, it could be anything. Here's the example. Thank you. Thank you. Have you ever asked yourself that question? I'm seeing nods. Is that Detlef nodding heavily there? If you were in Detlef's talk yesterday about concurrence, you will appreciate why he said, the head just kept going. It was great to see. Let's talk about it. Thank you. Now, I want to point out that we're going to talk about defaults, but the correctness does not depend on the defaults. The defaults are only to minimize annotation. In principle, think of the top part only. In principle, you always have to state the lifetime of a return pointer. The caller assumes the lifetime, and we ensure that it's correct when we compile the callee, because we can tell whether whether he lied or not when we compile him separately. Now, as soon as you say that, you have to state, you think, "Oh, great, here comes all the heavy annotation. Yeah. Turns out that languages are pretty regular. Code is pretty regular. Ever since the dawn of computing, we know where most outputs have any function. Forget about pointers. If you get an int out of a function, that takes two ints. Where do you suppose it came from? $1 to donuts that came from those two ins you passed in. Yes, they could have gone to a static. Yes, they could have done some side effect. But nearly always your function outputs come from the inputs. You want a way to override that default. In the cases it's not true, that's fine, like a singleton, but that's fine. You want But that should be the default. And if it's true for ants, why on earth isn't it true for pointers? If I get a pointer out of a function, Its dollars to donuts comes from the inputs, from the owners and the pointers that were passed into the function. And if we make that the default, and we do, you need zero annotation for the vastly common case. The next most common case is when there are no parameters at all, or just a parameter like an int, there's no owner or pointer parameters, Then we assume the lifetime is static. We assume you're pointing to something static. Turns out then you don't have to annotate singleton instance functions either. and things like that. Only if it's something else, which we catch when you separately compile the callee and the default is wrong, We say, oh, you can't compile the callee in this lifetime model until you say what its lifetime actually is. Because you said it's this, and you say you're returning this, Pick one. So you can actually just cut and paste from the error message and say, OK, it's lifetime this. Put it on your declaration. And then we're good. The best way to state a lifetime is like this. And this is what I don't like about Rust and Cyclone and C-Cured, There's too heavy annotation. You should not need any annotation at all in most functions. And Rust actually has some nice defaults. They do some of these same things. But you shouldn't really have to talk about regions or scopes. at all. Thank you. So let's call some functions. The vast majority of cases, as I said, were derived from the owner and pointer arguments. So if I call a function that takes two int stars, and that returns an int star. I'm going to assume by default rep points to star p or star q. This is like a branch. I treat it basically like a branch. Just like an if condition, ret equals star p, else ret equals star q. I basically do the same thing. Now, what if I pass an owner, or I pass a string ref, and I get a char star? We are going to assume, not because it's a char star, it could have been an int star. It's a very dumb rule. But we just say, hey, you know what, that pointer you returned, betcha it points into its parameter, into the owner that you passed as an argument. And because it's an owner, [75:00] We say owner prime. We assume it points to something owned by the owner you passed in. Remember, there's just owners and pointers. It's simple. If it's a pointer, You copy the points to set. If it's an owner, you take the owner's name, prime. It's something he owns. That's the default. Now, it turns out temporary objects in C++ are kind of interesting. Not in a bad way, But in fact, C++ has a very clear model about lifetime values, about L values and R values are now a bit finer grained than that, PR values, X values, And that works very, very elegantly and nicely with this. Thank you. I'm going to describe it. I'm just coining this term. So it's just not a term you'll find in the standards. It's one I made up because it was handy. I'm going to talk about... owner R-value weak magnets and strong magnets. So what is a weak magnet for an R value? Well, a const reference parameter can attach to L values, but it can also attach to R values. You can bind that to a temporary object. But it's not the only thing it mostly binds to. Often it binds to regular objects. So I'm going to call that a weak magnet for an R value. And when that's a reference to an owner, like say you could be passing in a temporary vector. By default, I'm not going to assume you're passing a pointer that you got from that. It could have been a temporary. I'm generally going to ignore those Unless there were no real owners that I know are not likely to be temporaries. If there aren't other candidates, then I'll consider const string ref, const owner ref parameters. Thank you. What I will never consider... is owner ref ref parameters, because those are just begging to be attached to temporary objects. The only thing that can really attach to or should attach to conceptually is either a temporary, in standardese terms, a PR value, a PR value, And you don't want to return a pointer into one of those, because he's about to go away. You know that for sure. Thank you. The other thing is what's called an X value, or an expiring R-value, that's something you call std.move-on of the call site, essentially, or something very like that. Now, why don't we want to return a pointer to something like that? You don't-- the function body doesn't have to move from it, but it's supposed to. It's expected to. So you're passing either a temporary or something that's not a temporary but you're still expected to move from, Yeah, I don't think you want to pass back a pointer into that. Because I know its lifetime is about to end. So here's the example of find_match. It takes a string ref and a const string ref. By default, I'm only going to consider the first owner because the second is a weak owner. our value magnet, So I'm going to assume that char star points into the string, not into the match. And then at all my call sites, whether I pass an L value, a literal, a temporary object, a user-defined literal, It's all the same. I just assume the pointer is good for as long as the string is alive, regardless of what the second parameter is. And you can see some of these call sites, if I did return a pointer to the second argument, that would be bad. because some of those are about to go away. In the callee, now you might say, well, that's great, but how do you know it's correct? The callee could actually do something else. Well, not if you compile it. If you compile it and say it returns, A pointer it got from s, that's fine, because it's getting a pointer that has s prime, using the internal function parameter name, and it's trying to convert that to a pointer, Return that as a pointer that points to S prime. That's compatible. So you're allowed to do that. But if you take the address of something in sub, and you get a pointer that points into sub, I'm not going to let you by default return that As a pointer, that points to S, that's not the same thing. So you'll get an error there if you try to compile that code, and either fix the code--it's probably a bug--if you tried to do this, it's probably a bug--or if you really, really meant it, then advertise it and annotate the char star you're returning. to say that you're returning lifetime son. instead. And this is true even if you try to mix it up through temporary objects and local values and different control paths. Thank you. And this is exactly what VectorSubscriptOperator does. It returns a reference, just like a pointer, into *this prime. It just happens to be this pointer The owner is the invisible this object. The same thing of begin. It happens to return an iterator into this. So *this* is what it points to. It can be invalidated if you modify this vector. Thank you. Now... We have just a minute. Let me see. Is there a Gabby? Is Gabby in the room somewhere? Oh, there you are. Okay. So I've been getting email from a brilliant friend of mine named Andre Alexanderscu. And Andre really is brilliant and he's done a lot of good work for C++. Where'd he go? Oh, there you are. And other languages. But he emails me about every year [80:00] When's the committee going to fix stood min? And I was talking with you about this. By the way, Gabby did not know I was going to do this to him. Amen. So this is why I asked you to sit near an aisle. So you were telling me, Gaby, that you got similar emails. So how long has Andre been asking you about Studmin? GABY KALA: Yeah, so the first time we talked about this was around 1998. That was before Facebook and Google groups. GABY KALA: OK, so now looking at this, what's wrong with Studmin? Or what's the potential danger? GABY KALA: So Andre's point was that the the parameters are const T refs. And if you call mean on temporary as arguments, then you get a reference back. Like, so what about this call here? I have x and y ints and-- Min of x and y, that's OK, isn't it? I mean, that's efficient. That's why min returns a reference. Fantastic. Oh, but under maintenance, what if I make it that? Oh, God. God bless you. It's a sneeze. OK, we'll call it a sneeze. And it's data dependent, because this might or might not fall over, depending on the value of x and y. OK, what if I make it some function call? Are those OK? Oh, God. Thank you, Gabby. Thank you, Gabby, very much. It depends on what the functions return. So Andre's going to be here, and I would have done this to him. But he's not flying in until tomorrow morning. It's OK. We'll all tell him. But he's already seen this, in fact. And so it's a very good observation. Andre is completely correct and has rightly been complaining about it for almost 20 years. that this could happen. Now, true, you have to bind a reference and so forth. but it's still a trap for the unwary. Wouldn't it be nice if we could get error messages that say these things, Neil, could you show this example? Yes, indeed. This is for you, Andre. laughter Okay, so here we have the same old example as you had. I've written up an F2 and F3 and Even implemented min here so everybody can see we're not cheating under the covers. So this looks like good code, right? Returning things that live long enough. And yes, we think it's good code. OK, but now try to change that to min x comma y plus 1, because then we're passing a temporary in, right? Okay. All right. Compiler still thinks it's valid. Let's see. And that's because this compiles, that code compiles on every compiler. Ooh, but the tool doesn't. Ah, what's it saying? It's telling us now that the result of min is being invalidated by some allotemp variable name, which looks horrible. So we'll fix up these error messages. Yes. But that's another way of saying... But that's another way of saying the temporary object y plus 1. Exactly right. Now, what if you change that to a call to F? F2, try F2 because that returns a reference to a static. Because that one really ought to be okay. If you say so, let's see. Because notice F2 takes no parameters. So we assume that the pointer you get back, in this case, spelled ampersand. is a pointer to something static, which should be OK. Analysis agrees. OK, we'll try F3, because F3 returns by value, That should be like any other temporary object. Okay, run analysis and see what it says. Oh, it agrees with you again. Thank you very much. Pleasure. And thank you, Andre. So that's the lifetime profile. I heard a bit of buzz in the hallway yesterday after Bjarne said, "We think by identifying the owners, distinguishing owners and pointers and tracking what pointers point to, we can eliminate lifetime errors." And I heard some grumbling and stuff like, That sounds like magic, but it's Bjorn saying it. He doesn't say stuff that's random. There must be more to this. And there was more than fit into one keynote, so we had two talks. And I hope that this has been useful to you, because we are very hopeful this may crack A 40-year nut. in solving lifetime problems safely, without heavy annotation, in C++, as it's largely written today, without relying on things like garbage collection, or heavy annotation, or a new language. Now having said that, we're still working on some things. We're still finalizing the 1.0 design paper. Earlier today, I pushed the 0.9.1 design paper. which contains everything except most of the formal rules that a compiler implementer would Those are still being refined, but put mostly [85:00] and some of the formal rules And that's in the CPP core guidelines repository right now, under /docs. So you can see that after the talk. And we aim to share this prototype next winter. In fact, When-- now, I know people are going to ask, when can we get this? And the answer is not right away. It's a prototype. So, Neil, what expectations should we have as to when, at least from this one, and we hope others will implement it too, But at least from this tool in Visual Studio, when should people expect to be able to see that? Right, well we are sharing prototype releases of the bounds and types checkers. in the same timeframe as Visual Studio Update 1. for 2015. So that's very soon. And then shortly afterwards, you should see a prototype for the lifetime's checker drop as well. Yeah, so the lifetime, hopefully in a few months, but in the next month or two, that's when you should see the CTPs and the update one of the other two, the bounds and the type profiles as well. Well, So can safety make C++ simpler? Yes, we can eliminate classes of errors. That's the easy answer. But it also makes our coding style simpler. We already saw min and max. But why do smart pointers not convert to raw pointers implicitly? Why do we have to dot and get all the time? I have answered this question in many companies because everybody initially writes the conversion to T-Star. Then they realized, oh darn, that means some code I didn't mean to compile compiles because of pointer arithmetic. I convert to T* and I add 42. Oh, that's helpful. Worse, I might dereference that, then it's a wild pointer. Or I might dangle a pointer. I might get a valid pointer, but I didn't quite realize it was a raw pointer, and so I wasn't paying attention to the lifetime of the smart pointer. So it dangles. Well, if we do have a solution where we have a subset of C++ where there are no dangling, and no bounds violations. There should be no reason that smart pointers can't just convert to T-Star. I don't know of anything that can go wrong. That's a conjecture, but it's an example of how safety simplifies library design. So this is all about ISO C++. It continues to evolve. The things we've learned in this handful of types, we hope to feed back into standardization, who I'm sure will make them even better, or find something different, a better replacement. That's something the committee does very well. But here is a set of tools that we've found, types and rules, that can give you type and bounds and even lifetime safety for leaks and dangling. and nearly all of it statically at compile time, with no runtime overhead. Again, acknowledgments to many people who have already given their input to it, and many more of you who are working on this core guidelines project. So far, you've seen Vjarna's talk and my talk about it. Here are some related talks to watch for. After lunch, Gabby is going to talk about modules. which is an area of interest that overlaps with safety, because it helps us to put limits on macro leakage and things like that. Neil, who we saw here on stage, is going to give two talks tomorrow on static analysis. about more than just what we've talked about here. There'll be other topics as well, and more about array view and string view and how they work. Gabby then follows up with contracts, and please come on Friday to the plenary session, the keynote, the end note, by Eric Kneibler, of ranges, because those, too, are about bound safety. Instead of a raw iterator and doesn't know its bounds, I know the bounds of what I'm talking about. They are also about bound safety. Please enjoy those talks. Thank you again for all of your interest already in the repository. The way it's trending worldwide is really a gratifying thing to see because it's something that's clearly useful to people. Please help us make it better. We need help. We can't do it ourselves. And we hope you've enjoyed this material. and the rest of the conference. Thank you very much for coming. Thank you.