Template- and Header-Heavy Libraries¶
What a template library exports¶
A class template exports nothing. Buffer<T> is a recipe; a symbol exists
only when someone instantiates it, and an implicit instantiation is
compiled into the consumer's binary, where the library never sees it. The
only symbols the library owns are its explicit instantiations — the
template class Buffer<int>; lines its build emits — and those carry the
same hazard every other symbol does, plus one of their own: adding a field
keeps the mangled name identical while sizeof(Buffer<int>) grows, and a
consumer that stack-allocated the old size is smashed with no header-level
signal. Part 4 § Templates and inline
is the mechanism; this page is what to do about a library that is mostly
this.
The contract is the instantiation matrix¶
For a template library the public contract is not a header and not an
export table. It is the matrix of explicit instantiations the build
system enumerates — every (Float, Method, Task) triple the release
promises to have compiled — and a comparison that does not know the matrix
cannot tell a promised instantiation from an incidental one. The
instantiation manifest states it directly:
version: 1
provides:
- template: oneapi::dal::train_ops
instantiations:
- {Float: float, Method: "method::dense", Task: "task::train"}
- {Float: float, Method: "method::sparse", Task: "task::train"}
- {Float: double, Method: "method::dense", Task: "task::train"}
- {Float: double, Method: "method::sparse", Task: "task::train"}
library: libonedal_core.so.1
optional_provider: false
Dozens of entries describe thousands of mangled symbols, and each one the
new release stops exporting is a broken promise rather than one more
removal in a long list. Two cases show the two ways the matrix breaks: the
instantiation is still exported but its layout changed
(case17), and the
instantiation is simply missing from the shipped binary because the build
dropped a line
(case79).
Parameter order in each entry must match the template's own; the shapes,
matching rules and the bootstrap script that produces a first manifest
from a release are owned by
Multi-binary § --instantiation-manifest,
and the manifest is applied on a product comparison
(Products, Not Libraries):
What the header side can and cannot see¶
The header tier sees a template through the compile context it was given.
The default castxml backend emits template instantiations only, never
the uninstantiated pattern; the clang backend records the pattern as well
(Header Backend Capabilities).
Neither backend detects a change to an uninstantiated template's
signature: nothing was instantiated, so nothing was compared. That is the
source tier's job — L4 replays the declaration itself
(case122,
the same case How a Break Shows Up cites for
"the source changed, no binary did"). Three neighbours are visible at the
header tier and worth knowing by name: a default template argument
changed, which silently changes what every consumer instantiates
(case87);
an internal template's signature changed while public code depends on it
(case85);
and a detail:: templated base class whose layout change propagates into
every public derived class
(case77).
Which tier each break needs is defined by
Evidence & Detectability.
The cost cliff¶
The one cost cliff in the evidence ladder is at the source tier, and it tracks template depth: a single template-heavy translation unit's AST dump can reach gigabytes, so a full-target replay of a large tree costs hours and memory, while a replay seeded by the changed translation units costs minutes. The replay caps its worker count by available memory, including a container's cgroup limit, rather than by CPU count alone. Ask before you spend:
--since is not wired to --no-baseline's one-sided audit path yet (it
exits 64 there today) — changed-path seeding is inherently a two-sided
concept anyway (narrowing to what a revision range touched relative to
a baseline), so this is a two-sided compare:
That seeds the L4/L5 replay to the changed translation units instead of the
full target, which is where the minutes-versus-hours saving above actually
comes from. --dry-run's own cost preview does not reflect this seeding
yet — verified live: adding --dry-run to the command above still prints
source scope: target on each side (compare has no PR change seed) and
leaves the "Cost preview" TU counts unchanged, because --since's
changed-path resolution runs after --dry-run has already emitted and
exited (a documented gap:
known-gaps.md, "compare --dry-run's cost
preview does not reflect --since's changed-path seeding" entry). So a dry
run here only ever previews the unseeded upper-bound cost; run the
command above for real (without --dry-run) to get the actual seeded,
cheaper replay. In CI, --budget
fails loudly on overflow rather than shrinking scope, so a scan that
finished is a scan that did what it claims. Numbers, knobs and the
reasoning behind the memory cap are owned by
Performance § L4 source-replay performance;
the moments to run the seeded versus the unseeded scan by
Where in the Pipeline.
Multi-TU surfaces and comparability¶
A template library's public surface is rarely one header. --dump-manifest
describes the translation units that together form one side's snapshot —
each with its own compile context — and is side-scoped, so old and new can
carry different manifests. That is also where the comparability gate
earns its keep: two snapshots extracted under different manifests, flags
or scope settings differ in thousands of instantiations that are not
contract changes, and the gate refuses the pair with no verdict rather
than producing a page of phantom additions and removals. "Not comparable"
is the better answer; rebuild one side under the other's profile, or pass
--diagnostic-comparison to see the tentative diff with its assurance
stamped as none. The fingerprint and its carve-outs are owned by
Build Profile Comparability; the
manifest flag by the CLI Reference.
Header-only libraries¶
A header-only library is the limit case: every function is inline or a template, the export table is empty, and the whole contract is the inline bodies and the types they name. The evidence question is then the header graph rather than any binary — a public struct that gains a field of a private type is a break with no symbol anywhere (case191). Static & Header-Only Contracts is the shape without a binary.
Ladder: ← Products, Not Libraries · Step 8 · At Scale · How System Libraries Stay Compatible →