Daily bit(e) of C++ | Numbers are not easy (2023)

Daily bit(e) of C++ #27, The C++ integral and floating-types zoo.

Daily bit(e) of C++ | Numbers are not easy (1)

Arguably one of the most error-prone parts of C++ is integral and floating-point expressions. As this part of the language is inherited from C, it relies heavily on fairly complex implicit conversion rules and sometimes interacts unintuitively with more static parts of C++ language.

This article will cover the rules and several surprising corner cases one can encounter when working with integral and floating-point types and expressions.

There are two phases of potential type changes when working with integral types. First, promotions are applied to types of lower rank than int, and if the resulting expression still contains different integral types, a conversion is applied to arrive at a common type.

The ranks of integral types are defined in the standard:

  1. bool
  2. char, signed char, unsigned char
  3. short int, unsigned short int
  4. int, unsigned int
  5. long int, unsigned long int
  6. long long int, unsigned long long int

As mentioned, integral promotions are applied to types of lower rank than int (e.g. bool, char, short). Such operands will be promoted to int (if int can represent all the values of the type, unsigned int if not).

Promotions are generally harmless and invisible but can pop up when we mix them with static C++ features (more on that later).

uint16_t a = 1;
uint16_t b = 2;

// both operands promoted to int
auto v = a - b;
// v == -1, decltype(v) == int

Open this example in Compiler Explorer.

Conversions apply after promotions when the two operands are still of different integral types.

(Video) Experienced C++ Developers Tell the Truth in 2021

If the types are of the same signedness, the operand of the lower rank is converted to the type of the operand with the higher rank.

int a = -100;
long int b = 500;

auto v = a + b;
// v == 400, decltype(v) == long int

Open this example in Compiler Explorer.

I left the complicated part for last. When we mix integral types of different signedness, there are three possible outcomes.

When the unsigned operand is of the same or higher rank than the signed operand, the signed operand is converted to the type of the unsigned operand.

int a = -100;
unsigned b = 0;
auto v = a + b;
// v ~ -100 + (UINT_MAX + 1), decltype(v) == unsigned

Open this example in Compiler Explorer.

When the type of the signed operand can represent all values of the unsigned operand, the unsigned operand is converted to the type of the signed operand.

unsigned a = 100;
long int b = -200;
auto v = a + b;
// v = -100, decltype(v) == long int

Open this example in Compiler Explorer.

Otherwise, both operands are converted to the unsigned version of the signed operand type.

long long a = -100;
unsigned long b = 0; // assuming sizeof(long) == sizeof(long long)
auto v = a + b;
// v ~ -100 + (ULLONG_MAX + 1), decltype(v) == unsigned long long

Open this example in Compiler Explorer.

Due to these rules, mixing integral types can sometimes lead to non-intuitive behaviour.

int x = -1;
unsigned y = 1;
long z = -1;

auto t1 = x > y;
// x -> unsigned, t1 == true

auto t2 = z < y;
// y -> long, t2 == true

(Video) Want to Catch Numbers of Bass? - This Lure is Hard to Beat

Open this example in Compiler Explorer.

The C++20 standard introduced several tools that can be used to mitigate the issues when working with different integral types.

Firstly, the standard introduced std::ssize(), which allows code that relies on signed integers to avoid mixing signed and unsigned integers when working with containers.

#include <vector>
#include <utility>
#include <iostream>

std::vector<int> data{1,2,3,4,5,6,7,8,9};
// std::ssize returns ptrdiff_t, avoiding mixing
// a signed and unsigned integer in the comparison
for (ptrdiff_t i = 0; i < std::ssize(data); i++) {
std::cout << data[i] << " ";
}
std::cout << "\n";
// prints: "1 2 3 4 5 6 7 8 9"

Open this example in Compiler Explorer.

Second, a set of safe integral comparisons was introduced to correctly compare values of different integral types (without any value changes caused by conversions).

#include <utility>

int x = -1;
unsigned y = 1;
long z = -1;

auto t1 = x > y;
auto t2 = std::cmp_greater(x,y);
// t1 == true, t2 == false

auto t3 = z < y;
auto t4 = std::cmp_less(z,y);
// t3 == true, t4 == true

Open this example in Compiler Explorer.

Finally, a small utility std::in_range will return whether the tested type can represent the supplied value.

#include <climits>
#include <utility>

auto t1 = std::in_range<int>(UINT_MAX);
// t1 == false
auto t2 = std::in_range<int>(0);
// t2 == true
auto t3 = std::in_range<unsigned>(-1);
// t3 == false

Open this example in Compiler Explorer.

(Video) How to PRACTICE READING NUMBERS in English - Easy daily practice routine, habit - Improve every day.

The rules for floating-point types are a lot simpler. The resulting type of an expression is the highest floating-point type of the two arguments, including situations when one of the arguments is an integral type (highest in order: float, double, long double).

Importantly, this logic is applied per operator, so ordering matters. In this example, both expressions end up with the resulting type long double; however, in the first expression, we lose precision by first converting to float.

#include <cstdint>

auto src = UINT64_MAX - UINT32_MAX;
auto m = (1.0f * src) * 1.0L;
auto n = 1.0f * (src * 1.0L);
// decltype(m) == decltype(n) == long double

std::cout << std::fixed << m << "\n"
<< n << "\n" << src << "\n";
// prints:
// 18446744073709551616.000000
// 18446744069414584320.000000
// 18446744069414584320

Open this example in Compiler Explorer.

Ordering is one of the main things to remember when working with floating-point numbers (this is a general rule, not specific to C++). Operations with floating-point numbers are not associative.

#include <vector>
#include <numeric>
#include <cmath>

float v = 1.0f;
float next = std::nextafter(v, 2.0f);
// next is the next higher floating pointer number
float diff = (next-v)/2;
// diff is below the resolution of float
// importantly: v + diff == v

std::vector<float> data1(100, diff);
data1.front() = v; // data1 == { v, ... }
float r1 = std::accumulate(data1.begin(), data1.end(), 0.f);
// r1 == v
// we added diff 99 times, but each time, the value did not change

std::vector<float> data2(100, diff);
data2.back() = v; // data2 == { ..., v }
float r2 = std::accumulate(data2.begin(), data2.end(), 0.f);
// r2 != v
// we added diff 99 times, but we did that before adding to v
// the sum of 99 diffs is above the resolution threshold

Open this example in Compiler Explorer.

Any operation with floating-point numbers of different magnitudes should be done with care.

Before I close this article, I need to note two areas where the more static C++ features can cause potential issues when interacting with the implicit behaviour of integral and floating-point types.

While integral types are implicitly inter-convertible, references to different integral types are not related types and will, therefore, not bind to each other. This has two consequences.

(Video) EWTN Live - 2023-02-01 - Marlene Watkins

First, trying to bind an lvalue reference to a non-matching integral type will not succeed. Second, if the destination reference can bind to temporaries (rvalue, const lvalue), the value will go through an implicit conversion, and the reference will bind to the resulting temporary.

void function(const int& v) {}

long a = 0;
long long b = 0;
// Even when long and long long have the same size
static_assert(sizeof(a) == sizeof(b));
// The two types are unrelated in the context of references
// The following two statements wouldn't compile:
// long long& c = a;
// long& d = b;

// OK, but dangerous, implict conversion to int
// int temporary can bind to const int&
function(a);
function(b);

Open this example in Compiler Explorer.

Finally, we need to talk about type deduction. Because type deduction is a static process, it does remove the opportunity for implicit conversions. However, this also brings potential issues.

#include <vector>
#include <numeric>

std::vector<unsigned> data{1, 2, 3, 4, 5, 6, 7, 8, 9};

auto v = std::accumulate(data.begin(), data.end(), 0);
// 0 is a literal of type int. Internally this means that
// the accumulator (and result) type of the algorithm will be
// int, despite iterating over a container of type unsigned.

// v == 45, decltype(v) == int

Open this example in Compiler Explorer.

But at the same time, when mixed with concepts, we can mitigate implicit conversions while only accepting a specific integral type.

#include <concepts>

template <typename T>
concept IsInt = std::same_as<int, T>;

void function(const IsInt auto&) {}

function(0); // OK
// function(0u); // will fail to compile, deduced type unsigned

(Video) Emails EXPOSE a New โ€˜Science-Based' Gun Control Scheme at CDC

Open this example in Compiler Explorer.

Videos

1. 4 Easy Ways in Excel to Convert Numbers Stored as Text to Numbers - Workbook Included
(TeachExcel)
2. Bite-Sized Bible | Book of Numbers | Chapter Twenty-Six | RSV
(Bible Prayers)
3. DAILY Ep.79 - Take A Hard Look At Your Numbers
(Steve Napolitan)
4. C++ Edinburgh: Barney Dellar โ€” Daily C++
(Cpp Edinburgh)
5. ๐‰๐š๐ง๐ฎ๐š๐ซ๐ฒ ๐Ÿ๐Ÿ•, ๐Ÿ“:๐ŸŽ๐ŸŽ-๐Ÿ”:๐ŸŽ๐ŸŽ๐๐Œ ๐Ÿ๐จ๐ซ ๐จ๐ฎ๐ซ #KwentuhangCybersecurity ๐ญ๐จ๐ ๐ž๐ญ๐ก๐ž๐ซ ๐ฐ๐ข๐ญ๐ก ๐จ๐ฎ๐ซ ๐“๐ž๐œ๐ก ๐๐š๐ซ๐ญ๐ง๐ž๐ซ Infoblox!
(PICSPro)
6. Maximum XOR of Two Numbers in an Array | HashMap | Trie | Maths | Bits | 421 LeetCode | DAY 27
(CodeWithSunny)
Top Articles
Latest Posts
Article information

Author: Jeremiah Abshire

Last Updated: 04/04/2023

Views: 5477

Rating: 4.3 / 5 (74 voted)

Reviews: 89% of readers found this page helpful

Author information

Name: Jeremiah Abshire

Birthday: 1993-09-14

Address: Apt. 425 92748 Jannie Centers, Port Nikitaville, VT 82110

Phone: +8096210939894

Job: Lead Healthcare Manager

Hobby: Watching movies, Watching movies, Knapping, LARPing, Coffee roasting, Lacemaking, Gaming

Introduction: My name is Jeremiah Abshire, I am a outstanding, kind, clever, hilarious, curious, hilarious, outstanding person who loves writing and wants to share my knowledge and understanding with you.