A signature is the caller's first set of instructions. If two adjacent integers mean different things, or a pointer has an unspoken length, the compiler sees less of your intent than it could. The Core Guidelines' I and F families give us useful questions: who owns this object, what can be absent, which values form a valid request, what changes, and how does failure appear?
This lesson applies I.4, I.5, I.7, I.11, I.13, I.24 and F.16–F.24. It follows the existing lessons on values, spans and expected; it does not replace them.
1. Make different meanings different types
copy_window(data, 10, 3) can accidentally reverse start and count. Aggregate
wrappers Offset{10} and Count{3} retain those meanings in the signature.
Their representation can still be a size_t. Do not add implicit conversions
that erase the distinction. These types do not validate values by themselves;
the callee must check the combined request against the input extent.
#include <cstddef>
struct Offset { std::size_t value; };
struct Count { std::size_t value; };
void read(Offset, Count) {}
int main() { read(Count{2}, Offset{5}); }Strong types suit units, identifiers and sizes with different roles. They are not a reason to create a new class for every temporary integer. Name the distinction that prevents a plausible caller mistake.
2. Borrow data with its extent
Use std::span<const T> for a borrowed read-only sequence, std::span<T> for
borrowed mutable elements, and a reference for an existing mandatory object.
A pointer can express absence for an optional single observer. None of those
types alone owns the object or extends its lifetime. A span can still dangle
if its backing storage dies, and constructing one from a bad pointer is not a
runtime validation strategy.
#include <array>
#include <print>
#include <span>
int main() {
const std::array values{10, 20, 30, 40};
std::span<const int> view(values);
auto middle = view.subspan(1, 2);
std::println("{} {}", middle.size(), middle.front());
}The build validates a request before calling subspan. Invalid requests return an expected error; a zero-length range at the end is a valid result.
3. Choose return ownership deliberately
Returning a span or string_view into a local vector is a lifetime error. Returning a vector gives the caller an owning result. A Message struct can return its tag and owned payload together instead of changing several output arguments. Use unique_ptr when the API transfers a separately allocated polymorphic object, not as the default representation of every result.
#include <array>
#include <print>
#include <span>
#include <vector>
std::vector<int> own(std::span<const int> input) {
return {input.begin(), input.end()};
}
int main() {
std::array data{4, 5}; auto result = own(data); data[0] = 99;
std::println("{} {}", data[0], result[0]);
}4. Separate domain errors from preconditions
Malformed external bytes are expected input failures in this exercise. Decode validates the tag and exact length before constructing a domain Message. An allocation failure can still throw: an expected return type does not make a function noexcept or absorb every possible failure.
Document preconditions and postconditions in the specification. The Guidelines
also discuss GSL not_null, Expects and Ensures. GSL is not bundled here,
and those facilities are not the same thing as this course's experimental
C++26 contracts. Do not present a comment or a reference parameter as an
automatic null/lifetime checker. The coverage page marks that distinction.
Apply it
Implement the bounded window and wire decoder. Explain each parameter's meaning, why input is borrowed, why output owns, and which failure paths are represented by expected. Test reversed arguments at compile time and malformed data at run time. The task checks a bounded API contract, not every possible caller.
Read the linked Core Guidelines section and use the coverage map to continue into resource management, source files and class design.