(I'm an novice, so there may be inaccuracies in what I say)
In my current mental model, an overflow is an arithmetical phenomenon (occurs when we perform arithmetic operations), and an implicit conversion is an assignment (initialization or not) phenomenon (occurs when we make assignments which the right-hand's value dont fit into left-hand value.
However, often I see the concepts 'overflow' and 'implicit conversion' used interchangeably, , different from what I expect. For example, this quote from the learncpp team, talking about overflow and 'bit insufficiency' for signed int:
Integer overflow (often called overflow for short) occurs when we try to store a value that is outside the range of the type. Essentially, the number we are trying to store requires more bits to represent than the object has available. In such a case, data is lost because the object doesn’t have enough memory to store everything [1].
and this, talking about overflow for unsigned int :
What happens if we try to store the number 280 (which requires 9 bits to represent) in a 1-byte (8-bit) unsigned integer? The answer is overflow [2]*
and especially this one, who uses 'modulo wrapping':
Here’s another way to think about the same thing. Any number bigger than the largest number representable by the type simply “wraps around” (sometimes called “modulo wrapping”). 255 is in range of a 1-byte integer, so 255 is fine. 256, however, is outside the range, so it wraps around to the value 0. 257 wraps around to the value 1. 280 wraps around to the value 24 [2].
In such cases, it is said that assignments that exceed the limits of the lefthand lead to overflow, but I would expect, in this context, the term 'implicit conversion'.
I see the term overflow used also for arithmetic expressions whose result exceeds the limits of the lefthand.
1 Is there any technical difference between implicit conversion and overflow/underflow?
I think so. In the reference [3] in the section 'Numeric conversions - Integral conversions', for unsigned integer:
[...] the resulting value is the smallest unsigned value equal to the source value modulo 2^n where n is the number of bits used to represent the destination type [3].
and for signed (bold mine):
If the destination type is signed, the value does not change if the source integer can be >represented in the destination type. Otherwise the result is implementation-defined (until C++20)the unique value of the destination type equal to the source value modulo 2n where n is the number of bits used to represent the destination type. (since C++20). ** (Note that this is different from signed integer arithmetic overflow, which is undefined)**[3].
If we go to the referenced section (Overflow), we found (bold mine):
Unsigned integer arithmetic is always performed modulo 2n where n is the number of bits in that particular integer. [..]
When signed integer arithmetic operation overflows (the result does not fit in the result type), the behavior is undefined [4].
To me, clearly overflow is an arithmetic phenomenon and implicit conversion is a phenomenon in assignments that do not fit. Is my interpretation accurate?
2 Is there on bit level (cpu) any difference between implicit conversion and overflow?
I think so also. I'm far from being good at c++, and even more so at assembly, but as an experiment, if we check the output of the code below with MSVC (flag /std:c++20) and MASM (Macro Assembly), especially checking the flag register, different phenomena occur if is an arithmetic operation or is an assignment ('Implicit conversion').
(I checked the flags register in the Debugger of Visual Studio 2022. The Assembly below is is practically the same as the from debugging).
#include <iostream>
#include <limits>
int main(void) {
long long x = std::numeric_limits<long long>::max();
int y = x;
//
//
long long k = std::numeric_limits<long long>::max();
++k;
}
The output is:
y$ = 32
k$ = 40
x$ = 48
main PROC
$LN3:
sub rsp, 72 ; 00000048H
call static __int64 std::numeric_limits<__int64>::max(void) ;
std::numeric_limits<__int64>::max
mov QWORD PTR x$[rsp], rax
mov eax, DWORD PTR x$[rsp]
mov DWORD PTR y$[rsp], eax
call static __int64 std::numeric_limits<__int64>::max(void)
; std::numeric_limits<__int64>::max
mov QWORD PTR k$[rsp], rax
mov rax, QWORD PTR k$[rsp]
inc rax
mov QWORD PTR k$[rsp], rax
xor eax, eax
add rsp, 72 ; 00000048H
ret 0
main ENDP
It can be checked at https://godbolt.org/z/6j6G69bTP
The copy-initialization of y in c++ corresponds to that in MASM:
int y = x;
mov eax, DWORD PTR x$[rsp]
mov DWORD PTR y$[rsp], eax
The mov statement simply ignores the 64 bits of 'x' and captures only its 32 bits. It is cast from the operator dword ptr and stores the result in the 32-bit eax register. The mov statement don't set neither the overflow or carry flag.
The increment of k in c++ corresponds to that in MASM:
++k;
mov rax, QWORD PTR k$[rsp]
inc rax
mov QWORD PTR k$[rsp], rax
When the inc statement is executed, the overflow flag (signed overflow) is set to 1.
To me, although you can implement (mov) conversions in different ways, there is a clear difference between conversions using mov variants and arithmetic overflow: arithmetic sets the flags. Is my interpretation accurate?
Notes
- *Apparently there's a discussion about the term overflow for unsigned, but that's not what I'm discussing
References
[1] https://www.learncpp.com/cpp-tutorial/signed-integers/ [2] https://www.learncpp.com/cpp-tutorial/unsigned-integers-and-why-to-avoid-them/ [3] https://en.cppreference.com/w/cpp/language/implicit_conversion [4] https://en.cppreference.com/w/cpp/language/operator_arithmetic#Overflows