Skip to content

Chapter 9 — Casts, Conversions, and Strings

Chapter 9 — Casts, Conversions, and Strings

The four C++ casts

C-style casts like (int)x work but are a red flag in reviews — one syntax that silently becomes whichever conversion compiles: a const_cast, a static_cast, a reinterpret_cast, or one of the latter two combined with a const_cast. Note which one is missing from that list: the checked one. A C-style cast is never a dynamic_cast, so a parenthesised downcast in C++ is never verified at runtime — unlike C#'s (Derived)obj, which throws if you are wrong. Modern C++ names the intent:

Cast Purpose C# analogy
static_cast<T>(x) 'sensible' conversions: numeric, up/down class hierarchy when YOU know the type. No runtime check. (int)x, explicit conversions
dynamic_cast<T*>(x) checked downcast on polymorphic types; nullptr on failure (reference form throws). as / is
const_cast<T>(x) add or REMOVE const. Legitimate ~only for bad legacy APIs. Modifying an originally-const object is UB. (none)
reinterpret_cast<T>(x) reinterpret the bits: pointer-to-integer, unrelated pointer types. Danger zone; serialization/interop only. unsafe pointer tricks
double d = 3.7;
int i = static_cast<int>(d);              // explicit, searchable, intentional

Shape* s = GetShape();
if (auto* c = dynamic_cast<Circle*>(s))   // checked downcast (needs vtable)
    c->radius = 5;

Derived* d2 = static_cast<Derived*>(s);   // UNchecked downcast: fast, but if
                                          // s isn't really a Derived -> UB

Key principle: "I use static_cast for conversions I can prove, dynamic_cast when I must query at runtime — and I treat const_cast or reinterpret_cast in a code review as a question mark."

Strings and encodings

C# strings are immutable UTF-16 objects. std::string is a mutable byte buffer with no encoding awareness — it stores bytes; whether they're ASCII, UTF-8, or garbage is your problem. The modern convention: keep std::string as UTF-8 everywhere.

Where the bytes actually live. A std::string owns a heap buffer — except when it does not. Every mainstream implementation stores short strings inside the string object itself, the small-string optimization, so copying a short one allocates nothing at all while a longer one needs a heap block of its own. The threshold is around fifteen to twenty-two bytes depending on the library, it is not standardised, and nothing in the type tells you which side of it a given string is on. That matters wherever the cost of a copy decides a design: Appendix H measures parameter shapes against a string built deliberately past the threshold, precisely so the cost being measured is one that exists.

Try it (30 seconds). Predict std::string("Grüße").size() — five characters — then run it, source file saved as UTF-8. The answer is this whole section in one number.

std::string s = "hello";
s += " world";              // mutable in place - no C# immutability
s[0] = 'H';                 // legal!
s.size();                   // BYTES, not characters - differs from what
                            // you'd expect with non-ASCII text!

const char* c = s.c_str();  // borrow a C-style pointer (valid only while
                            // s lives and is unmodified - dangling trap)
std::string_view v = s;     // non-owning view (Chapter 10)

Comparison is by value out of the box (s1 == s2 compares contents), formatting is std::format (C++20, like string interpolation) or the classic streams. And a std::string cannot be null: default-constructed it is empty, s.empty() is the test, and string.IsNullOrEmpty has no work left to do — which makes the null const char* a vendor Get*Name returns for "unnamed" the one null in the picture. Constructing a std::string from it is undefined behavior, and Recipe 23 in Appendix F is the check that goes in front of it.

The SDK reality — multiple string types in one function. Most large SDKs ship their own string class (Qt's QString, Windows' BSTR, many vendor "UniString" types), typically UTF-16 like C# strings internally. std::wstring keeps the same company on Windows without being a vendor type at all: it is standard C++, and it is UTF-16 only because wchar_t is 16 bits there — on Linux and macOS wchar_t is 32 bits and a wstring is conventionally UTF-32. Conversions at the boundary are daily work:

VendorString title("Wall label");          // vendor string: UTF-16 inside
// vendor <-> std::string conversions, encoding NAMED explicitly:
std::string utf8 = title.ToUtf8();
VendorString back = VendorString::FromUtf8(utf8);

Trap: Encoding bugs are THE classic plug-in pitfall: user file and project names with non-ASCII characters (German umlauts, Cyrillic, CJK...) silently corrupt if you treat UTF-16 vendor strings as byte strings. Always convert explicitly with the encoding named.

The conversion itself — including the honest news that the standard library deprecated its own answer and what real codebases use instead — is Recipe 17 in Appendix F, with the mechanism spelled out once so it stops being magic.