Manual Type Casting Techniques
Convert types explicitly with static_cast and understand why C-style casts are dangerous.
What Is Explicit Type Conversion?
A cast is an operator that lets you name the type a value should arrive as. Feed it an expression and a destination type, and it produces a value of that destination type. Explicit type conversion is the label for this: the type is written down by you, in the source, rather than worked out by the compiler from the surrounding code. The conversions covered earlier in this chapter, where the compiler reaches for a conversion because an assignment or an operator demanded one and nobody said anything about it, are the implicit kind.
The operator that handles nearly every conversion you will ever write is static_cast.
#include <iostream>
int main()
{
char gradeLetter{ 'Q' };
std::cout << "As a character: " << gradeLetter << '\n';
std::cout << "As a number: " << static_cast<int>(gradeLetter) << '\n';
std::cout << "Still a char: " << gradeLetter << '\n';
return 0;
}
As a character: Q
As a number: 81
Still a char: Q
Watch the third line of output. gradeLetter is a char before the cast and a char after it. A cast never reaches into the object you handed it and rewrites its type; it builds a separate, unnamed value of the target type and leaves the original alone. Most of what follows in this lesson is a consequence of that single fact.
Reading a Cast Expression
static_cast<int>(gradeLetter) has three moving parts:
| Part | Role |
|---|---|
static_cast |
the operator itself |
<int> |
the type you want back, inside angle brackets |
(gradeLetter) |
the expression to convert, inside parentheses |
Laid out that way it looks like a call to a function called static_cast<int>, and it behaves like one. Because the whole construct evaluates to something, it can appear anywhere the destination type would be legal: inside an initializer, as an operand of an arithmetic operator, as a function argument, or straight into std::cout as above. What it evaluates to is a temporary object, direct-initialized from the operand and gone again at the end of the full expression.
Overriding the Compiler's Arithmetic
The first reason to name a type yourself is that the automatic rules are applied one operation at a time, and that order is sometimes not the one you wanted. Division is where beginners meet this.
#include <iostream>
int main()
{
int totalRainfall{ 91 };
int daysRecorded{ 14 };
std::cout << "Millimetres per day: " << totalRainfall / daysRecorded << '\n';
return 0;
}
Millimetres per day: 6
91 divided by 14 is 6.5, so where did the half go? Both operands are int, so the two types already agree and no conversion is needed at all. operator/ selects integer division, discards the remainder, and hands back 6. Parking the answer in a double afterwards rescues nothing, because the remainder was thrown away before the assignment ever happened; you would print 6 as a double.
If both operands were literals you could settle it by writing one of them with a decimal point, as in 91.0 / 14. Variables have no suffix you can bolt on, so the conversion has to be requested at the point of use:
#include <iostream>
int main()
{
int totalRainfall{ 91 };
int daysRecorded{ 14 };
std::cout << "Millimetres per day: " << static_cast<double>(totalRainfall) / daysRecorded << '\n';
return 0;
}
Millimetres per day: 6.5
static_cast<double>(totalRainfall) hands operator/ a temporary double holding 91.0. The operands no longer match, the usual arithmetic conversions bring daysRecorded up to double as well, and floating-point division runs. One operand is all it takes; casting the second one too would compile but add nothing.
Signing Off on a Lossy Conversion
The mirror image of that problem is a conversion the compiler is willing to perform but nervous about. A char holds one byte and an int normally holds four, so pushing an int into a char can lose information, and the compiler has no way to know whether your particular value fits.
This program is deliberately written the wrong way:
#include <iostream>
int main()
{
int codePoint{ 74 };
char initial{ codePoint };
std::cout << initial << '\n';
return 0;
}
Trimmed diagnostic:
s.cpp: In function 'int main()':
s.cpp:6:19: warning: narrowing conversion of 'codePoint' from 'int' to 'char' [-Wnarrowing]
6 | char initial{ codePoint };
| ^~~~~~~~~
The program still builds and still runs:
J
By the letter of the standard a narrowing conversion inside braces is ill-formed. GCC softens that to a warning by default and only refuses the program outright under -pedantic-errors, so do not count on the build stopping. Whichever way your compiler is configured, the message is a question: did you mean this?
A cast is how you answer it:
char initial{ static_cast<char>(codePoint) };
The initializer is now already a char, so no mismatch is left for the compiler to flag and the warning goes away. Truncation still happens whenever the value is too big; what changed is that the code now states on its face that truncation is acceptable here, so the next reader does not have to work out whether it was an accident.
Not every lossy conversion announces itself. Plain assignment is not list initialization, and the platform's -Wall -Wextra build has nothing to say about this:
#include <iostream>
int main()
{
int budget{ 240 };
budget = budget / 1.6;
std::cout << "Remaining: " << budget << '\n';
return 0;
}
Remaining: 150
The division yields the double 150.0, which is chopped down to an int on its way into budget. Turning on -Wconversion reveals what the default flags let through:
s.cpp: In function 'int main()':
s.cpp:6:21: warning: conversion from 'double' to 'int' may change value [-Wfloat-conversion]
6 | budget = budget / 1.6;
| ~~~~~~~^~~~~
Writing the cast makes the decision visible no matter how the warning switches are set, and it silences the warning for the people who do build with -Wconversion:
budget = static_cast<int>(budget / 1.6);
What static_cast Refuses
A cast is a request, not a command. static_cast is resolved entirely while the program is being compiled, and if no conversion exists between the two types it reports that instead of generating code. Text is not a number, so this goes nowhere:
#include <iostream>
#include <string>
int main()
{
std::string reading{ "38.4" };
int parsed{ static_cast<int>(reading) };
std::cout << parsed << '\n';
return 0;
}
Trimmed diagnostic:
s.cpp: In function 'int main()':
s.cpp:7:17: error: invalid 'static_cast' from type 'std::string' {aka 'std::__cxx11::basic_string<char>'} to type 'int'
7 | int parsed{ static_cast<int>(reading) };
| ^~~~~~~~~~~~~~~~~~~~~~~~~
That compile-time check is the first of the two properties worth remembering. The second is that static_cast was given a deliberately short reach: the conversions that rewrite a value's meaning rather than its representation, such as discarding a const guarantee or treating one type's bits as another's, are outside what it will do at all. Ask for one of those and you get a diagnostic, not a surprise.
A value that has to arrive somewhere as a different type should say so in the source: put that type inside a
static_cast, rather than leaving the outcome to whichever conversion the surrounding expression happens to trigger.
The Cast Family at a Glance
C++ has five casts in total. Four of them are spelled as keywords and are known collectively as the named casts; the fifth is a bare pair of parentheses inherited from C. Ordered by how often they show up in ordinary code:
| Cast | Converts between | You reach for it when | Risk |
|---|---|---|---|
static_cast |
related types, checked while compiling | any everyday value conversion | none it cannot verify first |
dynamic_cast |
pointers and references inside a polymorphic hierarchy, checked while running | you hold a base handle and need to know what it really refers to | none; it reports failure |
const_cast |
a type and the same type with const added or stripped |
an old interface takes a non-const parameter it never modifies | stripping const from an object that truly is const is undefined behaviour |
reinterpret_cast |
unrelated types sharing the same bits | low-level work such as walking raw bytes | high; nothing is verified |
| C-style cast | anything static_cast, const_cast, or reinterpret_cast can reach, tried in a fixed order |
never, in new code | high; you cannot see which conversion it chose |
The differences between them are entirely about permission. Each is handed the same two things and produces the same one thing; what varies is which conversions it is willing to carry out, and how loudly it complains when the answer is none of them.
Two rows in that table barely belong in application code. Arriving at
const_cast or reinterpret_cast is usually evidence that something further upstream was modelled wrong, so go back and look for another route before you commit to either.
The Parentheses Cast C Left Behind
C had no cast keywords. You wrote the destination type in parentheses, put the value to its right, and that was the whole thing. C++ inherited the syntax and calls it a C-style cast, which is why you still meet it in code that started life as C. A second spelling moves the parentheses onto the operand, giving a function-style cast that reads like a call:
#include <iostream>
int main()
{
int totalRainfall{ 91 };
int daysRecorded{ 14 };
std::cout << (double)totalRainfall / daysRecorded << '\n';
std::cout << double(totalRainfall) / daysRecorded << '\n';
return 0;
}
6.5
6.5
Both lines do the same conversion as the static_cast version earlier, and the function-style form at least makes the operand look like an argument. Modern C++ walks away from both of them anyway, for two reasons.
The first is that one spelling stands for five behaviours. Faced with (double)totalRainfall, the compiler works down a fixed list until something fits: const_cast, then static_cast, then static_cast followed by const_cast, then reinterpret_cast, then reinterpret_cast followed by const_cast. The parentheses you read as "turn this number into a double" may in another context be reinterpreting raw bytes or quietly dropping a const promise, and neither the code nor the compiler will mention which one happened. Mistakes of that shape do not fail the build; they wait and fail while the program is running.
The second is that a type name in brackets is invisible. A named cast can be searched for, so static_cast and reinterpret_cast are one grep away when you audit a codebase, and each one announces its own limits at the point of use. There is no string to search for in (double), and nothing in it that a reader can skim past and still be sure what it did.
The short form saves a few keystrokes and costs you the ability to find the conversion later, or even to say which conversion it was. Spell out the named cast on every line, including the ones where
(double) would have fitted.
One trick belongs to the parenthesised form alone. Where private inheritance has walled a base class off, the parentheses will still get you to it, and no named cast will. File that under curiosities, not reasons.
static_cast or a Braced Temporary?
Suppose some variable reading needs to become an int. Two spellings are worth comparing:
| Spelling | What comes back | How it is initialized |
|---|---|---|
static_cast<int>(reading) |
a temporary int |
direct-initialized from the operand |
int{ reading } |
a temporary int |
direct-list-initialized from the operand |
A third spelling, int( reading ), is a C-style cast in disguise and drags along everything the previous section described, so leave it out of the comparison.
The braced temporary is shorter, and it does convert. Three things separate it from the cast, and each of them points the same way once the job at hand is a conversion rather than an initialization.
Braces refuse to lose data. List initialization bans narrowing conversions, which is what you want when setting up a variable and precisely what gets in the way when the loss is the point. The restriction also reaches further than most people expect. Try the rainfall division again with braces instead of a cast:
#include <iostream>
int main()
{
int totalRainfall{ 91 };
int daysRecorded{ 14 };
std::cout << double{ totalRainfall } / daysRecorded << '\n';
return 0;
}
GCC reports the same conversion twice here, once for the braced initialization and once for the expression it feeds:
s.cpp: In function 'int main()':
s.cpp:8:26: warning: narrowing conversion of 'totalRainfall' from 'int' to 'double' [-Wnarrowing]
8 | std::cout << double{ totalRainfall } / daysRecorded << '\n';
| ^~~~~~~~~~~~~
s.cpp:8:26: warning: narrowing conversion of 'totalRainfall' from 'int' to 'double' [-Wnarrowing]
As before the build survives the complaint, and the division you wanted is the division you get:
6.5
91 plainly survives the trip into a double, and so would every other 32-bit int value, but that is not the test being applied. Braces exempt an integer-to-floating-point conversion only when the source is a constant expression whose value the compiler can check for itself, so double{ 91 } is accepted while double{ totalRainfall } is not. Declaring totalRainfall as constexpr makes the complaint go away; leaving it an ordinary variable means no amount of reasoning about how wide an int is will help. static_cast<double>(totalRainfall) steps around the question entirely, because a cast is read as your acknowledgement that any loss was intended.
Braces only accept a one-word type name. Several corners of the grammar admit a single type keyword and nothing longer; the standard calls those names simple type specifiers. int qualifies, unsigned int does not, and the failure is a parse error rather than anything that mentions conversions:
#include <iostream>
int main()
{
unsigned short port{ 8080 };
std::cout << "port: " << port << '\n';
std::cout << "widened: " << unsigned int { port } << '\n';
return 0;
}
Trimmed diagnostic:
s.cpp: In function 'int main()':
s.cpp:8:33: error: expected primary-expression before 'unsigned'
8 | std::cout << "widened: " << unsigned int { port } << '\n';
| ^~~~~~~~
s.cpp:8:55: error: expected primary-expression before '<<' token
8 | std::cout << "widened: " << unsigned int { port } << '\n';
| ^~
Angle brackets have no such restriction, so the cast just works:
#include <iostream>
int main()
{
unsigned short port{ 8080 };
std::cout << "port: " << port << '\n';
std::cout << "widened: " << static_cast<unsigned int>(port) << '\n';
return 0;
}
port: 8080
widened: 8080
You can invent a one-word name for unsigned int and get the braced form compiling, and the next lesson shows how, but that is effort spent working around a spelling rather than writing the conversion.
The cast is louder. static_cast<double> is longer than double{ }, and on a conversion that is a feature: it draws the eye, it survives a search, and it distinguishes the places where a type genuinely changes from the places where a variable is merely being set up.
int{ reading } and static_cast<int>(reading) both produce an int. Only one of them survives a multi-word type name, allows a loss you decided on, and turns up in a search. Braces are for initializing variables; the cast is for converting.
Looking Forward
dynamic_cast waits until inheritance and polymorphism are on the table, because a runtime check of "what does this base handle actually point at" only makes sense once there is a hierarchy to ask about.
static_cast picks up more jobs as more of the language arrives. It is the operator that converts an enumeration to its underlying integer type and back, and the one that returns a void* to the typed pointer it came from, both of which need types this chapter has not introduced yet. It also matters that the temporary it returns is direct-initialized: once you write classes of your own, a constructor marked explicit is a candidate for that initialization, where an implicit conversion would have skipped it.
The very next lesson covers type aliases, which is where the one-word name for unsigned int comes from.
Key Terminology
Explicit type conversion is a conversion the programmer names in the source using a cast operator, in contrast to an implicit conversion that the compiler supplies on its own.
Cast operator names a destination type and hands back a value of it, built from whatever you put in the parentheses.
Named casts are the four keyword-spelled cast operators: static_cast, dynamic_cast, const_cast, and reinterpret_cast.
C-style cast is the legacy (type)value form, along with its function-style spelling type(value).
Simple type specifier is the grammar's name for a one-word type name, and it is why int{ reading } parses while unsigned int { reading } does not.
Summary
Casting is how you take the type decision away from the compiler and record it in the source. static_cast is the operator for the job: it is checked while compiling, it produces a temporary of the type you asked for without disturbing the operand, and it declines the conversions that would reinterpret bits or discard const.
Two situations account for most casts you will write. In the first, the compiler's automatic rules pick a conversion you did not want, and the cast changes the operand types so a different rule applies; integer division turning into floating-point division is the standard example. In the second, the compiler is right to be suspicious of a lossy conversion and warns, and the cast is your signature confirming that the loss is intended.
The alternatives are both worse. A C-style cast resolves to one of const_cast, static_cast, reinterpret_cast, or a pairing of those, without telling you which, and it leaves nothing behind that a search can find. A braced temporary such as int{ reading } blocks deliberate narrowing, rejects multi-word type names, and blends in with ordinary initialization. Keep the named cast and the conversion stays visible, searchable, and limited to what you actually asked for.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Manual Type Casting Techniques - Quiz
Test your understanding of the lesson.
Practice Exercises
Explicit Type Conversion with static_cast
Practice using static_cast for safe, explicit type conversions. Learn when and why to use explicit casts instead of implicit conversions.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!