← C++, End to End

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.

c++ · live, editable, runnableOpen in Compiler Explorer ↗

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 nullptr if 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 removing const to mutate something, you're very likely about to invoke undefined behavior unless you're certain the underlying object wasn't actually declared const.
  • 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.

c++ · live, editable, runnableOpen in Compiler Explorer ↗

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.

c++ · live, editable, runnableOpen in Compiler Explorer ↗

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-level const and references. If expr is a const int&, x is just int.
  • auto& x = expr; deduces a reference, and top-level const is preserved this time (const auto& explicitly keeps it and additionally allows binding to temporaries).
  • auto deduces the *declared* type of the initializer, not necessarily what you'd guess. auto x = {1, 2, 3}; deduces std::initializer_list<int>, not an array or vector, a common surprise.
c++ · live, editable, runnableOpen in Compiler Explorer ↗

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).

c++ · live, editable, runnableOpen in Compiler Explorer ↗
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*.

c++ · live, editable, runnableOpen in Compiler Explorer ↗