C++, End to End
Type Conversion, Aliases, and Deduction
C++ will convert between types for you constantly, sometimes usefully, sometimes in ways that quietly change what your program does. Knowing which conversions are automatic and which need an explicit cast is one of the fastest ways to stop fighting the type system.
Implicit conversions
An int assigned to a double converts automatically (safe: no information is lost). A double assigned to an int also converts automatically, but truncates the fractional part, which is a narrowing conversion: information can be lost, and the compiler usually still allows it with at most a warning.
Brace initialization (int i{d}; instead of int i = d;) refuses to compile narrowing conversions at all, catching this class of bug at compile time. Prefer braces for exactly this reason when the value isn't already known to fit.
Integer promotion
Before most arithmetic happens, small integer types (bool, char, short) get promoted to int (or unsigned int) automatically. This is why char c1 = 'a'; char c2 = 'b'; auto sum = c1 + c2; gives you an int, not a char: the addition happens on promoted ints, and only assigning back into a smaller type would truncate again.
Explicit casts
When you want a conversion the compiler wouldn't do implicitly (or want to make an implicit one explicit and visible in the code), C++ gives you four distinct cast operators, each with a narrower job than the old C-style (Type)value cast:
- static_cast<T>(value): the everyday cast, for conversions the language already understands (numeric conversions, pointer conversions within a class hierarchy, explicit constructor calls). Checked at compile time, no runtime cost.
- dynamic_cast<T>(ptr): safely casts within a polymorphic class hierarchy (a base pointer to a derived pointer), returning
nullptrif the object isn't actually of that derived type. Has a real runtime cost (it uses RTTI, run-time type information) and requires the base class to be polymorphic (have at least one virtual function). - const_cast<T>(value): adds or removes
const/volatile. Almost never the right tool; if you're removingconstto mutate something, you're very likely about to invoke undefined behavior unless you're certain the underlying object wasn't actually declaredconst. - reinterpret_cast<T>(value): reinterprets the raw bits of one type as another, unrelated type. Legitimately needed sometimes (low-level bit manipulation, interfacing with hardware or C APIs), but it bypasses the type system's safety entirely.
The old C-style cast (int)x still compiles and silently picks whichever of the four above would apply, which is exactly the problem: reading (SomeType)value tells you nothing about which of those four very different operations is actually happening. The C++ casts are more to type, but they're grep-able, and each one documents its own intent at the call site.
Type aliases: typedef and using
Both typedef and the newer using create an alternate name for an existing type. They're equivalent in what they can express for simple cases, but using also handles templated aliases, which typedef can't do at all.
Prefer using in new code. It reads left-to-right like a normal declaration (using Name = Type;), and you'll eventually want a templated alias somewhere, at which point typedef simply isn't an option.
auto and type deduction
auto asks the compiler to deduce a variable's type from its initializer. It's not "dynamic typing", the type is fixed at compile time exactly as if you'd spelled it out, auto just saves you from spelling it. The rules have a few sharp edges worth knowing:
auto x = expr;deduces a plain value type, dropping top-levelconstand references. Ifexpris aconst int&,xis justint.auto& x = expr;deduces a reference, and top-levelconstis preserved this time (const auto&explicitly keeps it and additionally allows binding to temporaries).autodeduces the *declared* type of the initializer, not necessarily what you'd guess.auto x = {1, 2, 3};deducesstd::initializer_list<int>, not an array or vector, a common surprise.
decltype
Where auto deduces a type from a value you're initializing with, decltype(expr) gives you back the exact type of an expression without evaluating it or requiring an initializer, useful in template code and when declaring a variable whose type should exactly match another variable or expression, including its reference-ness and const-ness (which plain auto would strip).
Try it yourself: pick the right cast›
Replace the C-style cast below with the correct C++ cast for what's actually happening (a numeric conversion), then try changing it to a dynamic_cast scenario: add a Derived class inheriting from Base with a virtual destructor, and safely downcast a Base* to Derived*.