Lesson 8 of 8
C++ converts between numeric types automatically, and mostly that's helpful. Sometimes it silently throws away your data.
Widening: safe and silent
Converting to a type that can hold everything the original could is harmless:
#include <iostream>
int main() {
int small = 42;
double wide = small;
long long huge = small;
std::cout << wide << " " << huge << "\n";
int a = 7;
double b = 2.0;
std::cout << (a / b) << "\n";
return 0;
}
42 42
3.5
That last line shows usual arithmetic conversions at work: mixing int and double in one
expression promotes the int, so you get 3.5 rather than 3.
Narrowing: lossy and also silent
The dangerous direction:
#include <iostream>
int main() {
double precise = 3.99;
int truncated = precise;
std::cout << truncated << "\n";
int big = 300;
char small = big;
std::cout << static_cast<int>(small) << "\n";
double negative = -1.5;
unsigned int wrapped = negative;
std::cout << wrapped << "\n";
return 0;
}
3
44
4294967295
Three separate disasters, all compiling without error:
3.99→3. Note it truncates, it doesn't round.std::round()from<cmath>rounds.300→44. Acharholds −128 to 127; 300 doesn't fit, and what's left is the low byte.-1.5→ 4,294,967,295, via the unsigned wraparound from lesson 3.3.
None of these produce an error. With -Wall -Wextra you'll get warnings for some — another
reason those flags matter.
Integer division: the classic
#include <iostream>
int main() {
int total = 7;
int count = 2;
double wrong = total / count;
double right = static_cast<double>(total) / count;
std::cout << wrong << " " << right << "\n";
return 0;
}
3 3.5
total / count is int / int, so the division itself is integer division producing 3. Assigning
to a double afterwards is too late — the fraction is already gone. You must convert before
dividing.
This is the single most common numeric bug in beginner C++.
Brace initialization stops it
C++11 introduced {} initialization, and its best feature is that narrowing conversions inside
braces are diagnosed:
double precise = 3.99;
int truncated{precise}; // narrowing — diagnosed
warning: narrowing conversion of 'precise' from 'double' to 'int' [-Wnarrowing]
The standard says this is ill-formed, but compilers differ on how hard they push back: Clang
rejects it outright, while GCC issues a warning by default for backward compatibility. You can
make GCC agree with the standard by adding -Werror=narrowing, which is worth doing.
Either way, the point stands: the same code written with = says nothing at all. Braces turn a
silent data loss into something the compiler mentions. That's the entire argument for preferring
them:
#include <iostream>
int main() {
int a{42};
double b{3.14};
char c{'x'};
bool d{true};
int safe{static_cast<int>(3.99)}; // explicit: I meant to truncate
std::cout << a << " " << b << " " << c << " " << d << " " << safe << "\n";
return 0;
}
42 3.14 x 1 3
You can still narrow — you just have to say so with a cast, which makes it visible to the next reader. The conversion isn't blocked; the silence is.
Braces also solve the uninitialized-variable problem cleanly, since int x{}; zero-initializes:
#include <iostream>
int main() {
int a{};
double b{};
bool c{};
std::cout << a << " " << b << " " << c << "\n";
return 0;
}
0 0 0
static_cast: converting on purpose
When you genuinely want a conversion, say so:
#include <iostream>
int main() {
double value = 9.7;
std::cout << static_cast<int>(value) << "\n";
std::cout << static_cast<char>(66) << "\n";
std::cout << static_cast<double>(7) / 2 << "\n";
return 0;
}
9
B
3.5
static_cast<T>(x) is deliberately verbose. C also allows (int)value, which works but is easy
to miss when scanning code and is harder to search for. C++ has three other cast forms —
dynamic_cast, const_cast, and reinterpret_cast — covered later; static_cast is the one
you'll use most.
Practical rules
- Initialize with braces —
int count{0};— so narrowing is diagnosed (add-Werror=narrowingon GCC to make it fatal). - Convert before dividing, not after.
- Use
static_castwhen you mean it, so the intent is on the page. - Keep
-Wall -Wextraon to catch the narrowing that happens outside braces.
Try it
Write a program that computes the average of int values 7, 8, and 10, correctly, as a
double. Then show what happens when the conversion is done in the wrong place.
Module 3 complete
You now know what the fundamental types are and how big they are, when to reach for fixed-width
integers, how overflow behaves differently for signed and unsigned, why floating point can't
represent 0.1, that char is really a number, when auto helps, what const and constexpr
promise, and how brace initialization stops silent data loss.
That's the value layer. Next up: Module 4 — Control Flow, where programs start making decisions.
Questions about this lesson
Log in to ask a question.