Foundations · Module 1 · Lesson 9 of 9
Initialization, const and Conversions
Choose initialization deliberately, preserve invariants with const, and make narrowing conversions visible instead of accidental.
By the end you can
- Distinguish default, value, aggregate and list initialization
- Explain what const protects and where it belongs in an interface
- Detect narrowing and unsafe implicit conversions
Builds on Types Decide What Bits Mean.
01 · A question first
The same type can contain indeterminate bits, zeroes, or a fully initialized object depending on one pair of braces. A numeric conversion can also silently lose data. Which spelling makes the program's contract visible?
Make a guess, then step through the trace below to check it.
02 · Watch it run
int a;int b{};const int limit = 100;int count = 3.7;- value 0a
- value 1b
- value 2limit
- value 3count
The first local has automatic storage but no initializer, so reading it before assignment is invalid. The bits do not become a useful integer by declaration alone.
Machine work: reserve storage; no value yet
03 · The idea
Start with the problem
C++ gives several ways to create an object, and they are not interchangeable. int x; leaves an automatic integer without a value. int x{}; gives it zero. A class may select a different constructor depending on the spelling. The difference is visible in the trace before any algorithm runs.
Initialization establishes an object's first valid state. const restricts mutation through a name or interface. An implicit conversion changes a value or type without an explicit cast at the call site. Good C++ makes those choices obvious because a lost bit or invalid invariant is cheaper to prevent than to debug.
How the idea works
Default initialization does nothing for scalar locals:
int count; // no initialization
int total{}; // zero
std::string s; // constructs an empty stringClasses still run their default constructor under default initialization. Aggregate initialization fills members in order:
struct Point {
int x;
int y;
};
Point origin{0, 0};Prefer braces when they express the desired state. They also reject narrowing:
int safe{3}; // fine
// int unsafe{3.7}; // compile error
int truncated = 3.7;const belongs at the narrowest boundary that should not mutate:
int size(const std::vector<int>& values);
void print(const Record& record);The function promises not to mutate the argument through that reference. A const member function similarly promises not to change the observable state through this, which lets callers invoke it on const objects.
Conversions are sometimes useful and sometimes a defect. Promotions such as int to long preserve the value on supported implementations. Conversions from a wider integer to a narrower one, from floating point to integer, and from signed to unsigned can lose information or change meaning. Use an explicit cast only after deciding that behavior is correct. Warnings such as -Wconversion help expose accidental cases.
Why it is correct
Initialization rules are part of the type and object-lifetime model. A constructed object has invariants that later code can rely on; an uninitialized scalar has no such promise. Const-correct interfaces make mutation visible in the function signature, so callers and tools can reason about aliasing.
Brace initialization is not magic safety, but its narrowing rule catches a valuable class of mistakes. Explicit conversion is not automatically safe either: it documents intent, while range checks or a type such as std::optional may still be required when input is untrusted.
What it costs
Zero-initializing storage costs writes, although compilers often remove redundant initialization or place constants in read-only sections. Const can enable optimization and improves API clarity, but it does not make an object immutable across all aliases. Runtime checked conversions cost a branch; silent conversions cost correctness.
Pitfalls and variations
Do not read an uninitialized scalar. Do not rely on a narrowing conversion merely because test values fit. Beware auto with brace syntax: auto x{1} and auto x = {1} have different deduced types. Prefer std::size_t or a signed index consistently rather than mixing signed and unsigned arithmetic without a reason.
These rules make the later class, container and template lessons safer: every object starts valid, every mutation is deliberate and every conversion has an owner.
Production guidance
Use the initialization form that communicates intent. Brace initialization is a strong default because it rejects narrowing conversions, but it can select an initializer_list overload unexpectedly. Parentheses are sometimes the right choice for a constructor with a competing list overload, and a factory function can make that choice explicit at the call site. Always initialize members in declaration order; changing the order in the constructor list does not change construction order and can hide a warning or a dependency between fields.
For class invariants, prefer constructors that establish a complete valid state over a default constructor followed by setters. In-class member initializers provide sensible defaults, while a constructor can validate values that have no universally valid default. Mark observers const, and use const on references and pointers to state whether the function may mutate the object, the referred-to object, or neither. const is not a thread-safety guarantee: a logically const operation may still mutate a cache or race on shared state.
Conversions deserve a boundary review. An implicit conversion from a signed integer to an unsigned type can turn a negative value into a very large one; a floating-to-integer conversion truncates and can be undefined or implementation-dependent when the value is out of range. Use std::to_chars, checked parsing or an explicit validation step for external input. Mark single-argument constructors explicit unless the conversion is genuinely part of the type’s vocabulary. static_cast documents a conversion but does not make an invalid conversion safe.
References and views do not extend lifetimes. A const std::string& parameter can bind to a temporary for the duration of a call, but storing that reference is unsafe. Be equally careful with string_view, span and iterators returned from a temporary container. At ownership boundaries, take or return a value, smart pointer or another type whose lifetime is clear. Compile with conversion and initialization warnings enabled, and let tests cover boundary values rather than only ordinary positive inputs.
One useful review technique is to follow a value from its input boundary to its final storage. Identify where text becomes an integer, where a borrowed reference becomes a member, and where a narrow type becomes a wider one. Put validation before the conversion, and preserve the validated type afterward instead of repeatedly casting. This keeps dangerous decisions local and makes interfaces easier to audit.
04 · Prove it
Question 1 of 3 · answer from memory
What value does int b{} receive?
Something wrong with this lesson? Report a problem