Pointers¶
Heap modeled internally as a Z3 array from mathematical addresses to width-neutral integer cells
cppverify::modifies(*p, ...)— framecppverify::aliases(p, q)— allow aliasingImplicit object-range disjointness for distinct mutable pointer/reference parameters
Example:
void write(int *p, int v)
cv::pre(p != nullptr)
cv::modifies(*p)
cv::post(*p == v)
{
*p = v;
}
Scalar lvalue references¶
Contracted executable free functions support scalar T& and const T&
parameters for boolean, integral, and enum referents:
void swap_values(int& left, int& right)
cv::modifies(left, right)
cv::post(left == cv::old(right) && right == cv::old(left))
{
int temporary = left;
left = right;
right = temporary;
}
The VCR parameter is an immutable address. Reading left loads the heap,
assignment stores through the address, and cppverify::old(left) reads the entry heap.
CppVerify adds a non-null and live entry precondition for each reference, and
verifies the body as reading the referent initialized; a call that passes
storage not yet initialized is Unresolved (construct.unsupported). Mutable pointer/reference parameter pairs are object-range disjoint
by default (over whole valid(p, n) extents);
cppverify::aliases(left, right) permits same-object aliasing.
Out-parameters¶
A function is verified as if it read its reference and pointer parameters’
storage, so every such parameter must refer to initialized storage at a call.
C++ requires less: a function that writes its parameter before reading it,
an out-parameter, may receive storage that is not initialized yet. Until a
contract can say that a function writes a parameter first, such a call is
Unresolved with reason construct.unsupported: neither proved nor
reported as a failure.
void init(int &out)
cv::modifies(out)
cv::post(out == 7)
{
out = 7;
}
int seven()
cv::post(cv::result == 7)
{
int a;
init(a);
return a;
}
int seven_initialized()
cv::post(cv::result == 7)
{
int a = 0;
init(a);
return a;
}
int read_too_early()
cv::post(cv::result == 3)
{
int a;
int &r = a;
int copy = r;
r = 3;
return copy;
}
Verified: init [backend=z3]
out.cpp:12:3: Unresolved: seven [backend=z3] [reason=construct.unsupported] (the verifier cannot model a construct here: binding a reference or pointer parameter to storage that is not initialized, since the callee is verified as reading initialized storage)
Verified: seven_initialized [backend=z3]
out.cpp:29:3: error: verification failed: read_too_early [...::initialization@29:3] (...) [backend=z3] [reason=counterexample]
Initializing the storage before the call, as
seven_initializeddoes, is the way to verify such code today.The same holds for an element of a local array (
init(arr[0])), for storage fromnew intpassed to a pointer parameter before anything writes it (int *p = new int; init_ptr(p);), and for a function that returns a pointer to storage not initialized yet.A local reference may bind storage that is not initialized, as C++ allows, and a write through it initializes the object. A read before the first write is a real error, reported where it happens, as in
read_too_early.
Before 0.2 each of these calls was a reported failure (verification failed:
... initialization), and int a; init(a); stopped the whole file with
error: seven: read of uninitialized local value: a.
Aliasing between parameters¶
The default is a precondition every caller must establish. A function that
works when its parameters share storage says so with cppverify::aliases, and its
body is then verified for both cases:
void copy_or_self(int *dst, int *src)
cv::aliases(dst, src)
cv::pre(dst != nullptr && src != nullptr)
cv::modifies(*dst)
cv::post(*dst == cv::old(*src))
{
*dst = *src;
}
void copy_one(int *dst, int *src)
cv::pre(dst != nullptr && src != nullptr)
cv::modifies(*dst)
cv::post(*dst == cv::old(*src))
{
*dst = *src;
}
void same_cell(int *p)
cv::pre(p != nullptr)
cv::modifies(*p)
cv::post(*p == cv::old(*p))
{
copy_or_self(p, p);
}
void same_cell_wrong(int *p)
cv::pre(p != nullptr)
cv::modifies(*p)
{
copy_one(p, p); // fails: the aliasing check at this call
}
cppverify::modifies(left) names the referent, a single scalar object, so a call
changes only that cell. A provenance-backed automatic or dynamic scalar actual
is framed as one exact cell after the callee passes the structural non-escape
scan.
A reference argument may also be an initialized ordinary scalar local:
void set_value(int& target, int value)
cv::modifies(target)
cv::post(target == value)
{
target = value;
}
void set_pointer(int* target, int value)
cv::pre(target != nullptr)
cv::modifies(*target)
cv::post(*target == value)
{
set_value(*target, value);
}
int set_local(int value)
cv::post(cv::result == value)
{
int local = 0;
int& alias = local;
set_value(alias, value);
return local;
}
Only address-required scalar locals are spilled from scalar SSA. They use fresh automatic lifetime identities, target size/alignment, byte ownership, liveness, and initialization. Local bindings snapshot the address, may chain, and cannot escape. Each automatic object’s lifetime ends at the closing brace of its block and on each early return.
A reference may bind to a field or a fixed-array element, of a promoted local
object or of a parameter’s buffer. Conditional bindings, temporaries,
reference returns, address-taking, rvalue references, and non-scalar
referents fail closed. Addressable declarations inside loops and cppverify::old of an automatic local/local
binding are rejected, while an outer automatic local and a local reference
declared inside a loop are supported. Requiring initialized storage excludes
output-only references to an indeterminate object for now.
Recursion that stores through a pointer or reference parameter, or uses
automatic objects, is supported; recursion through a function that allocates
or frees storage is construct.unsupported.
Buffers and array indexing¶
Pointer arithmetic and subscripting are supported: *(p + i) and p[i]
(read and write) lower to indexed heap accesses. Addresses use target bytes, so
each T* step is scaled by Clang’s target sizeof(T); field selection adds
the target record-layout byte offset. Distinct indices are therefore distinct
locations, so storing to p[k] leaves p[i] unchanged for i != k —
the verifier gets this from array theory:
void set(int* p, int i, int j, int v)
cv::pre(valid(p, 1000) && i != j)
cv::pre(0 <= i && i < 1000 && 0 <= j && j < 1000)
cv::pre(p[j] == 5)
cv::modifies(*p)
cv::post(p[i] == v && p[j] == 5) // p[j] is preserved
{ p[i] = v; }
Note
Addresses are byte-scaled, so separating p[i] from p[j] means
separating 4*i from 4*j. The default integer encoding derives that
from i != j alone. With --int-encoding=bitvector it needs range
reasoning about sign extension that bit-vector solvers do not do well, and
the bit-vector attempt can time out on such a query; the verifier then
retries it in the default encoding.
Within a verified function body, cppverify::modifies(*p) authorizes the whole
region rooted at p — a write to any p[i] is covered. A write through
a different base pointer that is not in cppverify::modifies is still rejected.
A cppverify::modifies footprint is one of:
a cell:
p[i],p->field, a field or array element inside the recordpaddresses (p->inner.x,p->buf[i]), or a reference, at its exact address;a range
p[lo : n]: thenelements fromp[lo], half-open[lo, lo + n)(Clang’s array-section syntax), and likewisep->buf[lo : n]for an array member (Structs and type invariants);a region
*p: the objectpaddresses, which is itsvalid(p, n)extent when it has one and otherwise one object.
At a modular call the caller’s heap changes only inside the callee’s footprints, instantiated with the arguments; every other cell keeps its value. A callee writing a range or a whole slice frames the rest of the buffer:
void zero_middle(int* p, int n, int lo, int len)
cv::pre(valid(p, n) && n >= 0 && n <= 1000 && 0 <= lo && 0 <= len &&
lo + len <= n)
cv::modifies(p[lo : len])
cv::post(cv::forall(i, lo, lo + len, p[i] == 0))
{
int j = lo;
while (j < lo + len)
cv::invariant(lo <= j && j <= lo + len && cv::forall(i, lo, j, p[i] == 0))
cv::modifies(p[lo : len])
cv::decreases(lo + len - j)
{ p[j] = 0; j = j + 1; }
}
void keeps_rest(int* p, int n)
cv::pre(valid(p, n) && n >= 4 && n <= 1000)
cv::modifies(*p)
cv::post(p[0] == cv::old(p[0]) && p[3] == cv::old(p[3]))
cv::post(p[1] == 0 && p[2] == 0)
{
zero_middle(p, n, 1, 2);
}
Each callee footprint must lie within the caller’s own frame, cell by cell,
whichever pointer reaches it. A callee that writes its one object
(cv::modifies(*p)) may be passed a cell the caller may write, or a copy of
a pointer into an object the caller may write:
void put(int *p, int v)
cv::pre(p != nullptr)
cv::modifies(*p)
cv::post(*p == v)
{
*p = v;
}
void set_third(int *a)
cv::pre(cv::valid(a, 4))
cv::modifies(a[2])
cv::post(a[2] == 5 && a[1] == cv::old(a[1]))
{
put(a + 2, 5);
}
void set_through_copy(int *a)
cv::pre(cv::valid(a, 4))
cv::modifies(*a)
cv::post(a[2] == 5)
{
int *q = a + 2;
put(q, 5);
}
void set_fourth(int *a)
cv::pre(cv::valid(a, 4))
cv::modifies(a[2])
{
put(a + 3, 5);
}
Verified: put [backend=z3]
Verified: set_third [backend=z3]
Verified: set_through_copy [backend=z3]
frames.cpp:30:3: error: verification failed: set_fourth [...::frame@30:3] (counterexample: a [ssa=a_0] [type=pointer] = 1; trace: call.put@30:3 arg0=13 arg1=5) [backend=z3] [reason=counterexample]
set_fourth may write only a[2], at address 9 when a is 1, but
passes a + 3, address 13. A pointer-taking
callee with no explicit cppverify::modifies that may write memory is treated as
writing the whole heap, which cannot fit inside an explicit caller frame; an
unframed caller may still call it. Under --no-check-ub a region footprint
also forgets the whole heap. The whole-heap fallback can lose a true fact
about an unrelated object, but it cannot prove a false one.
To reason about a whole range, use a bounded quantifier in the loop invariant and
postcondition. The solver chooses the terms that instantiate it, unless a
cppverify::trigger names one (see Contract expressions). A fill/zero loop
verifies end-to-end:
void zero(int* p, int n)
cv::pre(valid(p, n) && n >= 0 && n <= 1000)
cv::modifies(*p)
cv::post(cv::forall(i, 0, n, p[i] == 0))
{
int j = 0;
while (j < n)
cv::invariant(0 <= j && j <= n && cv::forall(i, 0, j, p[i] == 0))
cv::decreases(n - j)
{ p[j] = 0; j = j + 1; }
}
A loop writes only the objects its stores and calls reach, so every other
object keeps its value without an invariant saying so. A loop may also state
what it writes with cppverify::modifies after its invariants (ACSL’s loop
assigns); the footprints are read in each iteration’s state, so
cppverify::modifies(p[0 : j]) describes the prefix written so far.
The objects a store reaches are found from where its pointer came from, not its spelling: every pointer variable has origins, the objects it may address (see Pointer origins below). A pointer that walks through a buffer still writes only that buffer, and a pointer chosen among several parameters writes one of their objects:
void fill_walk(int *a, int n, int *count)
cv::pre(valid(a, n) && n >= 0 && n <= 1000 && valid(count, 1))
cv::modifies(*a)
cv::post(cv::forall(k, 0, n, a[k] == 7))
cv::post(*count == cv::old(*count))
{
int *q = a;
for (int i = 0; i < n; i = i + 1)
cv::invariant(0 <= i && i <= n && q == a + i)
cv::invariant(cv::forall(k, 0, i, a[k] == 7))
cv::decreases(n - i)
{
*q = 7;
q = q + 1;
}
}
void clear_one(int *a, int *b, int *c, int n, bool first)
cv::pre(valid(a, n) && valid(b, n) && valid(c, 1) && n >= 1 && n <= 1000)
cv::modifies(*a, *b)
cv::post(*c == cv::old(*c))
{
int *p = first ? a : b;
for (int i = 0; i < n; i = i + 1)
cv::invariant(0 <= i && i <= n)
cv::decreases(n - i)
{
p[i] = 0;
}
}
Both verify without an invariant about *count or *c.
Two-buffer copy loops (memcpy-style) need the source and destination
ranges to be disjoint; otherwise storing to the destination could clobber a
source cell still to be read, exactly the C memcpy vs memmove
distinction. valid(d, n) and valid(s, n) extents provide this
automatically: the implicit non-aliasing default treats each declared extent
as the pointer’s complete object, so the two extents are disjoint unless a
pointer is null or an extent is empty, and every caller must prove that. An
cppverify::aliases(d, s) pair keeps the single-object rule and may overlap.
void copy(int* d, const int* s, int n)
cv::pre(valid(d, n) && valid(s, n) && n >= 0 && n <= 1000)
cv::modifies(*d)
cv::post(cv::forall(i, 0, n, d[i] == s[i]))
{
int j = 0;
while (j < n)
cv::invariant(0 <= j && j <= n && cv::forall(i, 0, j, d[i] == s[i]))
cv::decreases(n - j)
{ d[j] = s[j]; j = j + 1; }
}
Addresses are reasoned about as mathematical integers, so range conditions like the non-overlap above are exact (no wraparound).
Same-array pointer difference¶
Pointer-pointer subtraction is supported in executable code when both positions are proved to belong to one array object:
long distance(int *p, int n, int i, int j)
cv::pre(valid(p, n) && p != nullptr && n >= 0 &&
0 <= i && i <= n && 0 <= j && j <= n)
cv::post(cv::result == i - j)
{
return (p + i) - (p + j);
}
The operands must have the same complete pointee type. Two null pointers
differ by 0, as C++ defines; otherwise the operands must have one origin, and
each must lie in its origin’s object: a valid(p, n) marker supplies the
extent and each position must lie in the closed interval [0, n]; the
inclusive endpoint is the legal one-past position. CppVerify subtracts
target-byte addresses, divides by sizeof(T), proves that the mathematical
element distance is representable by the target ptrdiff_t, and only then
materializes the machine result. Signed, unsigned, and target-width indices
follow their C++ machine representations. A position may be stepped or
copied any number of times, in a loop or not:
int length_of(const char *s, int n)
cv::pre(valid(s, n) && n >= 1 && n <= 1000)
cv::post(0 <= cv::result && cv::result < n)
{
const char *p = s;
while (p < s + (n - 1) && *p != 0)
cv::invariant(s <= p && p <= s + (n - 1))
cv::decreases(s + (n - 1) - p)
{
p = p + 1;
}
return p - s;
}
Without a declared extent, a direct abstract parameter or represented scalar
dynamic allocation has only its complete-object positions 0 and 1.
Local dynamic aliases establish one origin through their shared, nonzero
lifetime identity. Two different parameters may point into one caller array,
which the object model cannot express, so a difference between them (or
between pointers that may come from either) is construct.unsupported:
neither proved nor reported as an error. Proving left == right does not
change that.
Two null pointers differ by 0, as C++ defines ([expr.add]/5); a difference with one null operand is undefined and fails:
long two_nulls(const int *p, const int *q)
cv::pre(p == nullptr && q == nullptr)
cv::post(cv::result == 0)
{
return p - q;
}
long one_null(const int *p, const int *q)
cv::pre(p == nullptr && q != nullptr)
{
return p - q;
}
Verified: two_nulls [backend=z3]
nulls.cpp:11:3: error: verification failed: one_null [...::pointer-difference@11:3] (counterexample: p [ssa=p_0] [type=pointer] = 0, q [ssa=q_0] [type=pointer] = 1; ...) [backend=z3] [reason=counterexample]
Before 0.2, two_nulls failed the same way.
A difference with one null operand, a dangling operand, a position outside
its object, or a distance that does not fit ptrdiff_t fails with a
counterexample. A difference of pointers loaded from memory, pointer compound
assignment (p += k), and a pointer difference inside a cv::spec or
lifted constexpr function are rejected: the run stops with an error naming
the construct, for example error: gap: pointer difference in spec or lifted
constexpr functions is unsupported.
Local scalar dynamic storage¶
Ordinary scalar new/delete is supported for a direct local pointer.
Allocation state is SSA-versioned: the verifier records the
owning byte range, liveness, target size/alignment, and initialization. It
therefore rejects use-after-delete, double-delete, overlap between simultaneous
allocations, and uninitialized reads:
int roundtrip(int value)
cv::post(cv::result == value)
{
int *p = new int;
*p = value;
int observed = *p;
delete p;
return observed;
}
Its identity propagates through matching-typed local copies, reassignment,
conditional selection, direct nullptr assignment, and branch merges.
Deleting through any alias invalidates all aliases of that lifetime.
Type-erasing/indirect copies, general arithmetic, and pointer reassignment or
allocation/free in a loop body remain unsupported. The same-object difference
fragment above is the only dynamic pointer arithmetic exception. A
restricted interface admits direct scalar access and acyclic direct-pointer
forwarding through matching parameters of verified, non-allocating executable
callees. A direct/conditional/null pointer result may retain caller-owned
provenance when its contract relates the result to those inputs.
A separate inferred factory effect admits the exact live initialized base of one fresh scalar allocation (or null) from a body-present acyclic function. Direct and local-alias forwarding compose, and the caller may mutate or delete the result. Contracts still state null correlation and pointee values. No contract or external declaration can claim freshness: uninitialized, freed, arithmetic-derived, multiply allocated, secondarily escaped, recursive, and type-erased results fail closed. See Dynamic storage for the complete boundary.
Buffer bounds with valid¶
Memory accesses are checked by default (--no-check-ub turns it off).
valid(p, n) in a precondition says that p points to n objects,
p[0] to p[n - 1]. <cppverify.h> provides it for every pointee
type:
using cppverify::valid;
int get(const int* p, int n, int i)
cv::pre(valid(p, n) && 0 <= i && i < n)
cv::post(cv::result == p[i])
{ return p[i]; }
int past_end(const int* p, int n)
cv::pre(valid(p, n) && n >= 1)
{ return p[n]; }
Verified: get [backend=z3]
error: verification failed: past_end [...::bounds@10:10]
The marker means n >= 0. A positive extent also means p is non-null and
abstractly valid; an extent of zero permits null. Every p[i] or *(p + i)
access rooted at that parameter must then prove 0 <= i < n, and pointer
arithmetic must stay in [0, n]: p + n is the one-past position, which
may be formed and compared but not read. The marker must occur as a positive
top-level conjunction clause of a cppverify::pre with a bare complete-object pointer,
and each pointer may have only one marker; ambiguous forms (under ||,
!, or a conditional, or a second marker for one pointer) fail closed.
Contracts read under the same rule. An access in a precondition, a
postcondition, a loop invariant, or a cppverify::check must lie in its
extent or object where the clause is evaluated, so a precondition reads
p[i] only after the clauses that bound i: cv::pre(valid(p, n) && 0 <=
i && i < n && p[i] > 0), not cv::pre(valid(p, n) && p[i] > 0 && 0 <= i &&
i < n) (see Functions and loops).
Where the marker may appear¶
valid(p, n) is not a condition that a state satisfies or violates. In a
precondition it declares the extent the caller provides, and the verifier
checks every access against that extent; that is its whole meaning. Written
anywhere else (a cppverify::post, a loop invariant, a cppverify::check,
ghost code, or a spec function) it would be a condition that always holds, so
a claim made with it would verify without saying anything. That is an error
for the function, which is then not verified; the other functions of the file
still are:
int *start_of(int *buf)
cv::pre(valid(buf, 4))
cv::post(valid(cv::result, 4))
{
return buf;
}
error: start_of: valid(p, n) declares an extent only as a top-level condition of a cppverify::pre, and has no meaning in a postcondition
The message ends with the place it found the marker: a postcondition, a loop
invariant, a cppverify::check, ghost code, or a spec function. The same
holds for a marker you declare yourself as a spec named valid.
To tell callers where a result points, relate it to a parameter whose extent they know; their own precondition then covers the accesses through it:
int *start_of(int *buf)
cv::pre(valid(buf, 4))
cv::post(cv::result == buf)
{
return buf;
}
int fourth(int *a)
cv::pre(valid(a, 4) && a[3] == 7)
cv::post(cv::result == 7)
{
int *q = start_of(a);
return q[3];
}
Verified: start_of [backend=z3]
Verified: fourth [backend=z3]
An extent declared by the precondition holds for the whole function, so a loop invariant does not repeat it. The invariant bounds the index, and each access is checked against the extent:
int sum4(const int *a, int n)
cv::pre(valid(a, n) && n >= 0 && n <= 4)
cv::pre(cv::forall(k, 0, n, 0 <= a[k] && a[k] <= 100))
cv::post(0 <= cv::result && cv::result <= 100 * n)
{
int s = 0;
for (int i = 0; i < n; i = i + 1)
cv::invariant(0 <= i && i <= n && 0 <= s && s <= 100 * i)
cv::decreases(n - i)
{
s = s + a[i];
}
return s;
}
Verified: sum4 [backend=z3]
With --no-check-ub every marker, in a precondition too, folds to true
with a warning, and a misplaced one is not an error.
valid exists only for verification, like the spec collections of the same
header; Sema rejects it in executable code. Write cppverify::valid or add
using cppverify::valid;. Programs written before the header may declare
the marker themselves, and that form keeps working: a cppverify::spec function named
valid with a pointer and an integer parameter is the same marker.
cv::spec bool valid(const long* p, int n) { return true; }
long last(const long* a, int n)
cv::pre(valid(a, n) && n >= 1)
{ return a[n - 1]; }
A user declaration covers only its own pointer type, so one is needed per pointee type; the header’s template covers all of them.
A pointer without a marker addresses a single object, as Frama-C’s \valid
guards and Verus permissions require: p[1] through int *p fails, and
so does forming p + 10 from valid(p, 2), since pointer arithmetic must
stay within the object’s closed range [0, n]. The object of a pointer is
the one its origin names, below; when its origin is unknown it is some
parameter’s object, or the object at a base known to be valid such as a
callee’s result.
Pointer origins¶
Every pointer variable has, at every point, its origins: the objects it may
address, as CompCert’s memory blocks and Frama-C’s base addresses. A pointer
parameter starts with the object it addresses on entry and a global’s address
with the global. Pointer arithmetic keeps the origin, assignment copies it,
and branches and loops join the possibilities, so q = first ? a : b has
the origins a and b. A pointer loaded from memory or returned by a
call has no known origin.
An access or a step must stay in its origin’s object. A pointer that came
from a and moved one past its end is rejected there even when another
object happens to start at that address, as C++ requires. A pointer
difference needs one origin, and a loop writes only its stores’ origins.
A pointer into a record, such as r->buf or &r->head, has the
record’s origin and, in addition, the bounds of the array, field, or nested
record it points into. Its accesses, steps, and differences must stay inside
those bounds, so q = r->buf; q[8] is rejected although it lies in *r
(see Structs and type invariants).
Inside a loop, the verifier proves two facts for you: a pointer the loop moves stays a whole number of elements from its origin’s start, and a pointer that may have either of two origins keeps one of them. An invariant still states addresses, and an address alone does not say where a pointer came from. When a walker may be in either of two objects, tie it to the condition that chose:
void clear_chosen(int *a, int *b, int n, bool s)
cv::pre(valid(a, n) && valid(b, n) && n >= 1 && n <= 1000)
cv::modifies(*a, *b)
{
int *q = s ? a : b;
for (int i = 0; i < n; i = i + 1)
cv::invariant(0 <= i && i <= n && (s ? q == a + i : q == b + i))
cv::decreases(n - i)
{
*q = 0;
q = q + 1;
}
}
With only q == a + i || q == b + i, the invariant would allow q one
past the end of a at the start of b while it came from a, and the
store there would be rejected.
The same marker composes across modular calls. An argument for a parameter
with valid(q, length) must have its length elements in one of the
caller’s objects: passing p + offset from valid(p, n) requires
0 <= offset, 0 <= length, and offset + length <= n. An empty slice
may start at p + n. The argument may be computed in any way, as a copy, a
choice, a pointer stepped in a loop, or a callee’s result whose contract says
where it points; the elements must lie in the objects it may address:
int first(const int *p, int n)
cv::pre(cv::valid(p, n) && n >= 1)
cv::post(cv::result == p[0])
{
return p[0];
}
int second(const int *a, const int *b, bool from_a)
cv::pre(cv::valid(a, 4) && cv::valid(b, 3))
cv::post(cv::result == (from_a ? a[1] : b[0]))
{
const int *q = from_a ? a + 1 : b;
return first(q, 3);
}
int third(const int *a)
cv::pre(cv::valid(a, 4))
{
const int *q = a + 2;
return first(q, 3);
}
Verified: first [backend=z3]
Verified: second [backend=z3]
slices.cpp:20:3: error: verification failed: third [...::bounds@20:3] (counterexample: a [ssa=a_0] [type=pointer] = 1, q [ssa=q_1] [type=pointer] = 9; trace: call.first@20:3 arg0=9 arg1=3) [backend=z3] [reason=counterexample]
third passes three elements from a + 2, of which only two are in
a: with a at address 1, q is 9, and the call would read up to
address 21, past a’s end at 17. Read-only slice calls preserve the heap, including
acyclic chains of read-only callees; exact-cell effects such as
cppverify::modifies(q[0]), ranges, and a whole-slice cppverify::modifies(*q) are also
supported, and frame every cell of p outside the slice.
With --no-check-ub, dereferences must still be non-null and live, but
accesses are not checked against objects. Parameter pointers use abstract
entry-state allocation and initialization assumptions. Concrete identity,
liveness, alignment, and initialization are available only for promoted
automatic objects (address-taken scalar locals, fixed local arrays, and their
enclosing records) and for the bounded scalar allocation and inferred
fresh-owned return subset. Their provenance is first-class across supported
local values and checked scalar call/return interfaces, but abstract buffers
and general pointer interfaces remain unsupported.
Frames also preserve unrelated objects across a write:
void write_result(int *out, int *preserved, int value)
cv::pre(out != nullptr && preserved != nullptr)
cv::modifies(*out)
cv::post(*out == value)
cv::post(*preserved == cv::old(*preserved))
{
*out = value;
}
Distinct mutable pointer/reference parameters are non-aliasing by default. The
cppverify::modifies(*out) clause permits the store and frames *preserved at its
entry value.