You declare a float in C++ with the type name followed by a name and a value: float price = 19.99f;. The f suffix is the part people leave off, and it matters, because 19.99 without it is a double that then gets narrowed to fit. On almost every modern platform a float occupies 4 bytes and gives you roughly 7 significant decimal digits, which is a lot less precision than most beginners assume.
float x = 3.14f; with the f suffix. Use double instead unless you have a specific reason (memory pressure, GPU buffers, a file format) to want 4 bytes. Never compare two floats with ==. Compare the absolute difference against a tolerance derived from std::numeric_limits<float>::epsilon().Everything below was compiled and run with GCC 13.3 using g++ -std=c++20 -Wall -Wextra, and the outputs shown are the real outputs from those runs. Values that depend on the platform, such as the size of long double, are called out where they appear.
The basic declaration
Four ways to write a literal, all of them producing a float.
#include <iostream>
int main() {
float price = 19.99f;
float ratio = 2.0f / 3.0f;
float zero = 0.0f;
float sci = 6.022e23f;
std::cout << price << ' ' << ratio << ' ' << zero << ' ' << sci << '\n';
std::cout << "sizeof(float) = " << sizeof(float) << " bytes\n";
return 0;
}g++ -std=c++20 -Wall -Wextra float.cpp -o float
./float
# 19.99 0.666667 0 6.022e+23
# sizeof(float) = 4 bytesNotice that ratio printed as 0.666667, six digits after the leading zero. That is not the stream truncating an exact value: it is close to all the precision a float has.
float x = 1.0 / 3.0; without suffixes computes the division in double and then narrows the result. When the right hand side is a constant expression that fits, as it is here, the language allows it and brace initialization allows it too. What brace initialization does catch is narrowing from a value only known at run time: double y = f(); float x{y}; produces a narrowing diagnostic, which GCC reports as a warning by default and as an error under -Werror=narrowing or -pedantic-errors. I verified both behaviors with GCC 13.3.float versus double versus long double
The three floating types differ in width and therefore in how many decimal digits they can round trip. Print the same value in each and the difference is obvious.
#include <iostream>
#include <iomanip>
int main() {
float f = 0.1f;
double d = 0.1;
long double ld = 0.1L;
std::cout << std::setprecision(20);
std::cout << "float " << f << '\n';
std::cout << "double " << d << '\n';
std::cout << "long double " << ld << '\n';
std::cout << "sizes: " << sizeof(float) << ' ' << sizeof(double)
<< ' ' << sizeof(long double) << '\n';
return 0;
}# float 0.10000000149011611938
# double 0.10000000000000000555
# long double 0.1
# sizes: 4 8 16None of the three stores 0.1 exactly, because 0.1 is not representable in binary. The float is off in the eighth digit, the double in the seventeenth. The size of long double is the least portable number here: 16 on x86 64 Linux with GCC (holding 80 bits of value with padding), 8 on MSVC where it is simply an alias for double, and 16 with full quadruple precision on some other targets.
| Type | Typical size | Decimal digits | Approximate range | Suffix |
|---|---|---|---|---|
float | 4 bytes | 6 guaranteed, 9 to round trip | 1.2e-38 to 3.4e38 | f or F |
double | 8 bytes | 15 guaranteed, 17 to round trip | 2.2e-308 to 1.8e308 | none |
long double | 8, 12 or 16 bytes | 15 to 33, platform dependent | Platform dependent | l or L |
Reading the real limits with numeric_limits
Do not memorize the numbers in that table. Ask the compiler, because the answer is exact for whatever platform you are building on.
#include <iostream>
#include <limits>
int main() {
std::cout << "float digits10 " << std::numeric_limits<float>::digits10 << '\n';
std::cout << "float max_digits10 " << std::numeric_limits<float>::max_digits10 << '\n';
std::cout << "float max " << std::numeric_limits<float>::max() << '\n';
std::cout << "float min normal " << std::numeric_limits<float>::min() << '\n';
std::cout << "float epsilon " << std::numeric_limits<float>::epsilon() << '\n';
std::cout << "double digits10 " << std::numeric_limits<double>::digits10 << '\n';
std::cout << "double epsilon " << std::numeric_limits<double>::epsilon() << '\n';
return 0;
}# float digits10 6
# float max_digits10 9
# float max 3.40282e+38
# float min normal 1.17549e-38
# float epsilon 1.19209e-07
# double digits10 15
# double epsilon 2.22045e-16Two of those deserve attention. digits10 is how many decimal digits you can write, store and read back unchanged: six for float. max_digits10 is how many you must print to guarantee a value survives a round trip through text: nine. If you serialize floats to JSON with fewer than nine digits, you are silently losing data.
std::numeric_limits<float>::min() is not the most negative float. It is the smallest positive normalized value. For the most negative one, use std::numeric_limits<float>::lowest(), which was added precisely because the naming trips people up.Comparing floats correctly
The single most common float bug. Accumulate a tenth ten times and you do not get one.
#include <iostream>
#include <cmath>
#include <limits>
#include <algorithm>
bool nearly_equal(float a, float b, float rel = 1e-5f) {
float diff = std::fabs(a - b);
if (diff <= std::numeric_limits<float>::min()) {
return true;
}
return diff <= rel * std::max(std::fabs(a), std::fabs(b));
}
int main() {
float sum = 0.0f;
for (int i = 0; i < 10; ++i) {
sum += 0.1f;
}
std::cout << std::boolalpha;
std::cout << "sum == 1.0f " << (sum == 1.0f) << '\n';
std::cout << "nearly_equal " << nearly_equal(sum, 1.0f) << '\n';
return 0;
}# sum == 1.0f false
# nearly_equal trueThe comparison is relative, scaled by the larger operand, because a fixed absolute tolerance is wrong at both ends of the range. A tolerance of 0.0001 is far too coarse for values near 1e-30 and far too fine for values near 1e30. The check against the smallest normal value handles the case where both inputs are essentially zero, where relative comparison stops making sense.
Formatting float output
By default the stream prints six significant digits and drops trailing zeros. When you need money, a report column or a fixed width, you have to say so.
#include <iostream>
#include <iomanip>
#include <format>
#include <string>
int main() {
float temp = 36.6667f;
std::cout << std::fixed << std::setprecision(2) << temp << '\n';
std::cout << std::setw(10) << temp << '\n';
std::cout << std::scientific << std::setprecision(3) << temp << '\n';
std::cout << std::format("{:.3f}\n", temp);
std::cout << std::format("{:>10.1f}\n", temp);
std::cout << std::format("{:e}\n", temp);
return 0;
}# 36.67
# 36.67
# 3.667e+01
# 36.667
# 36.7
# 3.666670e+01Prefer std::format in new code. Stream manipulators like std::fixed and std::setprecision are sticky: they change the stream until you change them back, which produces action at a distance when a helper function nobody suspects has set std::scientific. std::format carries the formatting in the string and touches nothing global.
Special values: infinity, NaN and subnormals
Floats have values that are not numbers, and arithmetic on them does not throw. It just propagates.
#include <iostream>
#include <limits>
#include <cmath>
int main() {
float inf = std::numeric_limits<float>::infinity();
float nan = std::numeric_limits<float>::quiet_NaN();
float tiny = 1.0e-45f;
std::cout << std::boolalpha;
std::cout << "inf " << inf << " isinf=" << std::isinf(inf) << '\n';
std::cout << "nan " << nan << " isnan=" << std::isnan(nan) << '\n';
std::cout << "nan == nan " << (nan == nan) << '\n';
std::cout << "subnormal " << tiny << " isnormal=" << std::isnormal(tiny) << '\n';
float big = 3.0e38f;
std::cout << "overflow " << big * 10.0f << '\n';
return 0;
}# inf inf isinf=true
# nan nan isnan=true
# nan == nan false
# subnormal 1.4013e-45 isnormal=false
# overflow infThe line to remember is nan == nan returning false. NaN compares unequal to everything, including itself, which is exactly why std::isnan exists. It also means a NaN sneaking into a sort comparator produces a strict weak ordering violation and, in a libstdc++ debug build, an assertion or a crash.
Floats in aggregates and with auto
The declaration syntax is the same inside a struct, an array or a container, but two things change: default member initializers and type deduction.
struct Vertex {
float x{0.0f};
float y{0.0f};
float z{0.0f};
};
float coords[3] = {1.5f, 2.5f, 3.5f};
std::vector<float> samples(1024, 0.0f);
auto a = 1.5f; // float, because of the suffix
auto b = 1.5; // double, no suffix
float c = 1.5; // double literal narrowed to float, allowed
float d{1.5}; // also allowed: 1.5 is a constant that fits exactly
double runtime = some_function();
// float e{runtime}; // narrowing diagnostic: not a constant expressionauto follows the literal, so dropping the suffix silently gives you a double where you meant a float. In a struct destined for a GPU vertex buffer that is not a rounding concern, it is a layout bug: the buffer is twice the expected size and every field is at the wrong offset. Give every member an explicit type and an explicit default.
Troubleshooting
warning: conversion from ‘double’ to ‘float’ changes value. A literal is missing its f suffix, or you assigned a double expression to a float. Add the suffix, or make the variable a double.
The result is slightly different from Python or from a colleague’s build. Intermediate results may be computed at higher precision and rounded at different points. Do not compile numeric code with -ffast-math, which explicitly permits the compiler to reassociate operations and assume no NaN or infinity.
Money calculations drift by a cent. Do not use floating point for currency at all. Store integer cents in a long long and format for display. Every binary floating type gets 0.10 plus 0.20 slightly wrong.
A sum of many values is visibly wrong. Adding a tiny number to a large accumulator loses the tiny number entirely. Sum in double even if the inputs are float, sort ascending before summing, or use a compensated approach such as Kahan summation.
Comparing a float to an int behaves oddly. The integer is converted to floating point first, and integers above roughly 16.7 million are not all representable as a float. Compare in double, or compare integers as integers.
Frequently asked questions
Should I use float or double by default?
Use double. It is the type floating literals default to, it is what the standard math functions accept, and on modern desktop hardware it is not measurably slower for scalar work. Choose float when you have millions of values and memory bandwidth is your bottleneck, or when an external format demands 4 bytes.
What does the f suffix actually do?
It makes the literal a float instead of a double. Without it the value is computed in double precision and then converted, which can cause an extra rounding step and changes which overload is selected when you pass the literal to a function.
How many decimal digits does a float really hold?
Six are guaranteed to survive a round trip from text and back, which is digits10. You need nine digits when printing to guarantee the exact same bit pattern is recovered, which is max_digits10. Both values are available at compile time from <limits>.
Why is 0.1 + 0.2 not 0.3?
Binary floating point cannot represent one tenth or one fifth exactly, in the same way decimal cannot represent one third exactly. Each literal is stored as the nearest representable value, and the tiny errors do not cancel. Compare with a tolerance instead of with ==.
Can I declare a float without initializing it?
You can write float x; at block scope, but reading it before assignment is undefined behavior, not merely an unpredictable number. Always initialize, ideally with braces: float x{}; gives you a well defined 0.0f.
The bottom line
Declaring the variable is one line. The discipline is in the three habits around it: put the f on your literals, never compare with ==, and print with enough digits that your data survives being written to a file. Ask <limits> for the numbers rather than trusting a table you memorized.
If you find yourself fighting precision, the honest answer is usually that float was the wrong type. Move to double and the class of bug disappears for most application code. For related C++ fundamentals, see our guides on joining two vectors in C++ and writing a file in C++, and the numeric_limits reference documents every trait used above. If you are formatting floats for display, our notes on using threads in C++ cover why sticky stream state is especially dangerous once more than one thread writes output.
