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

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

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 intauto 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 == trueauto 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 comparisonfor (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 == falseauto 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 == falseauto t2 = std::in_range<int>(0);// t2 == trueauto 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 doublestd::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 numberfloat diff = (next-v)/2;// diff is below the resolution of float// importantly: v + diff == vstd::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 changestd::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 sizestatic_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)

Author information

Name: Jeremiah Abshire

Birthday: 1993-09-14

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

Phone: +8096210939894