CodeRigor

FREE LESSON

Format strings, snprintf & bounded input

Match variadic format arguments exactly, interpret snprintf counts and truncation, and separate trusted formats from input data.

The printf and scanf families cross a variadic boundary: the format string determines how argument values are interpreted. C++ does not make a mismatched call safe. A bounded wrapper must validate both the format contract and the storage contract.

The required count excludes the terminator
Capacity five stores four payload bytes and NUL; the full word would need eight bytes.
The required count excludes the terminator
Capacity five stores four payload bytes and NUL; the full word would need eight bytes.

Match every argument exactly

Use %d for int, %ld for long, %lld for long long and %zu for size_t. The * width and precision arguments are int. Default promotions matter: float becomes double in printf arguments, while scanf %f expects float* and %lf expects double*. A pointer to a destination is not interchangeable with a promoted value.

Keep the format trusted and pass external text as data. A name containing %n must stay literal text, not become a write directive. Our metric formatter uses %.*s with an explicit view length, because string_view.data() need not have a NUL at the view boundary. The precision counts bytes, not Unicode code points, and does not make overlapping snprintf source/destination buffers valid.

Follow one complete example

Run this program, then change one boundary while keeping the API preconditions valid. Predict its output before pressing Run.

C++
#include <cstdio>
int main() {
    char word[8]{}; int consumed=-1;
    int assigned=std::sscanf("orchard rest","%7s%n",word,&consumed);
    std::printf("%d %s %d\n",assigned,word,consumed);
    char short_output[5]{};
    int needed=std::snprintf(short_output,sizeof short_output,"%s","orchard");
    std::printf("%d %s\n",needed,short_output);
}
Check the signed result before capacity
Do not turn a negative formatting error into a huge unsigned count.
Check the signed result before capacity
Do not turn a negative formatting error into a huge unsigned count.

Interpret snprintf as both a count and an operation

For nonzero capacity, snprintf writes a terminated prefix when output is truncated. Its nonnegative return is the count that would have been written with enough room, excluding NUL. A result equal to capacity therefore means truncation. Check the signed return for an error before converting to size_t.

A zero-capacity call writes nothing and can use a null destination pointer, but still needs valid arguments for its conversions. An empty string_view may have a null data pointer, so pass a valid empty string for %s rather than relying on an implementation printing a special null spelling. The exercise keeps the truncated prefix and reports truncation explicitly; another API could instead format privately and commit only complete output.

Formats are code, names are data
An explicit precision also bounds a borrowed string view.
Formats are code, names are data
An explicit precision also bounds a borrowed string view.

Bounded scanning still needs a parser policy

For %s and a scanset, a scanf field width limits payload characters but excludes the appended NUL: %7s needs at least eight destination bytes. %c does not add a terminator. Check assignment counts; a successful conversion can leave trailing bytes. The %n conversion can record consumed input but does not increment the assignment count, and must never come from an untrusted format.

Numeric scanf conversions are not a robust validation interface for arbitrary out-of-range numbers: the portable contract does not safely represent every overflowing result. Read bounded input and use the checked strtol/strtod sequence for hostile numeric fields. gets was removed from the C standard and provides no acceptable bounded-input policy. Compare new C++ output code with std::format/std::print while retaining these rules for real C interfaces.

Read the return value

Predict this smaller case. The check item later changes the values, so explain the rule as well as the output.

C++
#include <cstdio>
int main(){char b[5];int n=std::snprintf(b,sizeof b,"%s:%d","abc",12);std::printf("%d %s\n",n,b);}

Complete the faded example

State the missing precondition or result check before filling the code. The fill exercise supplies the rest of the implementation.

C++
int required=std::snprintf(dst, capacity, "%.*s=%lld", precision, name, value);
// Fill: handle negative required before converting it.
// Fill: distinguish exact-capacity truncation from complete output.

Check your model

A four-byte payload formatted into capacity four returns four. Why does the wrapper report truncation even though the number sounds like an exact fit?

Practice the boundary

Implement format_metric(std::span<char> output, std::string_view name, long long value) -> std::expected<std::size_t,format_error> with snprintf.

Write name=value in decimal using a trusted format. Name is data, so percent signs in it are literal. Name may be an unterminated string_view; it must not overlap output. Reject embedded NUL or a name length larger than INT_MAX as invalid before any output mutation. Empty names are valid. The caller retains the "C" locale.

On success return the character count excluding NUL. Report a negative snprintf result as encoding. Report truncation when its nonnegative would-have-written count is at least the output capacity, including zero capacity. For nonempty output, preserve snprintf's truncated, terminated prefix; do not overwrite beyond the supplied span. Invalid input leaves output unchanged. Zero capacity writes nothing. The terminator requires a separate byte. The variadic precision has type int and the value has type long long.

Read → six checks → two bug hunts → fill → build. A successful graded submission is evidence; reading or watching alone does not grant mastery. Review cards reinforce API choices separately.

Watch and verify

The Watch & read panel links a free lecture for the relevant background and primary API references. The lecture notes say which parts are background; the tested contract here supplies the exact wrapper policy.

C standard-library course · C library review cards · Unix/Linux systems

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →