warning: implicit conversion loses integer precision: 'int' to 'unsigned char'

Thread Starter

AlbertHall

Joined Jun 4, 2014
12,624
I get this loss of precision message on the last line of the function below converting data to BCD form.
This message once again confuses me as there no 'ints'.

Can I safely ignore the message or how do I get rid of it?

Code:
unsigned char get_bcd(unsigned char data)
{
   unsigned char nibh;
   unsigned char nibl;

   nibh=data/10;
   nibl=data-(nibh*10);

   return((nibh<<4) | nibl);
}
 

BobTPH

Joined Jun 5, 2013
11,488
data/10 and nibh*10 are promoted to int because that is the type of the constant.

Try (unsigned char)10. That might get rid of the messages.
 

WBahn

Joined Mar 31, 2012
32,777
I get this loss of precision message on the last line of the function below converting data to BCD form.
This message once again confuses me as there no 'ints'.

Can I safely ignore the message or how do I get rid of it?

Code:
unsigned char get_bcd(unsigned char data)
{
   unsigned char nibh;
   unsigned char nibl;

   nibh=data/10;
   nibl=data-(nibh*10);

   return((nibh<<4) | nibl);
}
Before ignoring a warning, you want to very carefully analyze every step in your algorithm with the thought in mind of what happens, under every possible legitimate value of the data, it is converted from a signed integer to an unsigned char. This is easier said than done, because there can be some subtleties that are easy to overlook.

Your algorithm is simple enough, that this isn't too onerous. But type issues aside, you do have a potential issue. What if the value of data is greater than 99? If the code that calls this function guarantees that this never happens, then you are fine. However, in that case, you should document that with a comment that this function only returns valid results for 0 <= data <= 99.

Even if you are satisfied that you can safely ignore a compiler warning, that should be a last resort. If you have warnings that are being thrown that you are ignoring, then there is a much greater risk that you will do something that throws a warning that can't be ignored and you won't catch it because you are already used to seeing and ignoring warnings when you compile it.

As BobTPH said, the most likely culprit is that your constant is causing an implicit promotion to a signed integer and then the assignment is resulting in a narrowing conversion to unsigned char. There are a few ways of trying to deal with this.

One is to type case the constant, which is what Bob suggested.

Code:
unsigned char get_bcd(unsigned char data)
{
   unsigned char nibh;
   unsigned char nibl;

   nibh = data / (unsigned char) 10;
   nibl = data - (nibh * (unsigned char) 10);

   return (nibh<<4) | nibl;
}
Another is to let the compiler promote the expression and then do an explicit cast at assignment.

Code:
unsigned char get_bcd(unsigned char data)
{
   unsigned char nibh;
   unsigned char nibl;

   nibh = (unsigned char) (data / 10);
   nibl = (unsigned char) (data - (nibh * 10));

   return (nibh<<4) | nibl;
}
A third is to define a constant of the correct type -- the compiler will almost certainly not allocate memory for it.

Code:
unsigned char get_bcd(unsigned char data)
{
   unsigned char nibh;
   unsigned char nibl;
   const unsigned char 10;

   nibh = data / ten;
   nibl = data - (nibh * ten);

   return (nibh<<4) | nibl;
}
 

nsaspook

Joined Aug 27, 2009
16,275
None of these worked.
What did get rid of the warning was casting the return value:
return (unsigned char)((nibh << 4) | nibl);code
That 'warning' can be tricky to shut-up because of the way integer promotion works on a 8-bit C compiler. I usually don't compute return values on the return statement on 8-bit C code because of these types of issues, using a separate 'ret' variable in the function for previously computed return values improves code readability and is usually just as efficient in space, time and memory with level 2 compiler optimization.

https://rmbconsulting.us/publications/efficient-c-code-for-8-bit-microcontrollers/

The integer promotion rules of ANSI C are probably the most heinous crime committed against those of us who labor in the 8-bit world. I have no doubt that the standard is quite detailed in this area. However, the two most important rules in practice are the following:
 

WBahn

Joined Mar 31, 2012
32,777
None of these worked.
What did get rid of the warning was casting the return value:
return (unsigned char)((nibh << 4) | nibl);
I looked at the return statement and concluded that there was no issue because I didn't see anything that would require a promotion. I should have gone and looked at the standard. instead of relying on ancient memory, because it requires that

If an int can represent all values of the original type, the value is converted to an int; otherwise, it is converted to an unsigned int. These are called the integer promotions. All other types are unchanged by the integer promotions.
Then, looking at the semantics for the left-shift operator, the very first thing it says is:

The integer promotions are performed on each of the operands. The type of the result is that of the promoted left operand.
So the result of the expression is an int, which must then be narrowed since the function returns an unsigned char, hence the warning.
 

Futurist

Joined Apr 8, 2025
734
I get this loss of precision message on the last line of the function below converting data to BCD form.
This message once again confuses me as there no 'ints'.

Can I safely ignore the message or how do I get rid of it?

Code:
unsigned char get_bcd(unsigned char data)
{
   unsigned char nibh;
   unsigned char nibl;

   nibh=data/10;
   nibl=data-(nibh*10);

   return((nibh<<4) | nibl);
}
Unless you happen to have the documentation for your compiler's implementation of the language handy and some idea of the actual standard (if any) that it complies with, this is often a guessing game, particularly with the C language. Unlike the hardware components we use, the programming languages are often obscure and behave differently from vendor to vendor.

This nibh << 4 likely doesn't express what you think. The literal 4 itself is assigned a type by the compiler and even though common sense implies it is a char, you can't assume that. Expressions like that have a result type that's a function of the operand types, so if the literal 4 is being treated as something bigger than a char, the the result type too will almost certainly be bigger than a char.

Arithmetic expressions (+, x etc) can yield results that are bigger (in terms of number of bits) than either operand but bit shifting (and other logic operators) is not arithmetic and cannot generate larger results, nevertheless C and many other languages don't behave intuitively unfortunately.


Also please share the exact diagnostic message.
 
Last edited:

WBahn

Joined Mar 31, 2012
32,777
Unless you happen to have the documentation for your compiler's implementation of the language handy and some idea of the actual standard (if any) that it complies with, this is often a guessing game, particularly with the C language. Unlike the hardware components we use, the programming languages are often obscure and behave differently from vendor to vendor.
The language standard is a very good place to start, but as you noted, your compiler's implementation may not be compliant. This is particularly an issue in the resource-starved embedded world.

This nibh << 4 likely doesn't express what you think. The literal 4 itself is assigned a type by the compiler and even though common sense implies it is a char, you can't assume that. Expressions like that have a result type that's a function of the operand types, so if the literal 4 is being treated as something bigger than a char, the the result type too will almost certainly be bigger than a char.
For the shift operator, the types don't have to be the same because, unlike the arithmetic, relational, and logical operations, the two operands are not being combined. The right operand is simply telling the compiler how much to shift the right operand, so it's size doesn't affect the type of the result.

Arithmetic expressions (+, x etc) can yield results that are bigger (in terms of number of bits) than either operand but bit shifting (and other logic operators) is not arithmetic and cannot generate larger results, nevertheless C and many other languages don't behave intuitively unfortunately.
How confident are you that bit shifting cannot generate larger results? Particularly in light of the snippet from the C standard that I quoted earlier.

If the compiler is standards-compliant, then it most certainly can and must be able to generate larger results than the type of either operand if an int is wider.

Consider the following, in which both operands are unsigned chars.

Code:
#include <stdio.h>

int main(void)
{
	unsigned char x = 1;
	
	for (unsigned char n = 0; n < 32; n++)
		printf("1<<%2i = %i\n", n, x<<n);
	
	return 0;
}
On this compiler, which is a 64-bit compiler with 32-bit ints, the output is

Code:
 1<< 0 = 1
1<< 1 = 2
1<< 2 = 4
1<< 3 = 8
1<< 4 = 16
1<< 5 = 32
1<< 6 = 64
1<< 7 = 128
1<< 8 = 256
1<< 9 = 512
1<<10 = 1024
1<<11 = 2048
1<<12 = 4096
1<<13 = 8192
1<<14 = 16384
1<<15 = 32768
1<<16 = 65536
1<<17 = 131072
1<<18 = 262144
1<<19 = 524288
1<<20 = 1048576
1<<21 = 2097152
1<<22 = 4194304
1<<23 = 8388608
1<<24 = 16777216
1<<25 = 33554432
1<<26 = 67108864
1<<27 = 134217728
1<<28 = 268435456
1<<29 = 536870912
1<<30 = 1073741824
1<<31 = -2147483648
This just underscores that subtle fine points that are easily overlooked (or forgotten) matter and can byte you.

A naive (but perfectly natural) expectation is that the bits will roll off the left end after the eighth bit. But if you rely on this expectation, your code will not work as expected. This can result in extremely difficult to detect and track down.

While C very arguably has a lot more of these subtleties (and, more to the point, a lot more implementation-defined and undefined behavior) that most languages, all languages have them. The C has so many more is actually one of its strengths for the kinds of tasks that it is well-suited for. Unfortunately, hardly anyone uses it for those kinds of tasks, as so it is a significant weakness in most instances.
 

Futurist

Joined Apr 8, 2025
734
The language standard is a very good place to start, but as you noted, your compiler's implementation may not be compliant. This is particularly an issue in the resource-starved embedded world.



For the shift operator, the types don't have to be the same because, unlike the arithmetic, relational, and logical operations, the two operands are not being combined. The right operand is simply telling the compiler how much to shift the right operand, so it's size doesn't affect the type of the result.



How confident are you that bit shifting cannot generate larger results? Particularly in light of the snippet from the C standard that I quoted earlier.

If the compiler is standards-compliant, then it most certainly can and must be able to generate larger results than the type of either operand if an int is wider.

Consider the following, in which both operands are unsigned chars.

Code:
#include <stdio.h>

int main(void)
{
    unsigned char x = 1;

    for (unsigned char n = 0; n < 32; n++)
        printf("1<<%2i = %i\n", n, x<<n);

    return 0;
}
On this compiler, which is a 64-bit compiler with 32-bit ints, the output is

Code:
1<< 0 = 1
1<< 1 = 2
1<< 2 = 4
1<< 3 = 8
1<< 4 = 16
1<< 5 = 32
1<< 6 = 64
1<< 7 = 128
1<< 8 = 256
1<< 9 = 512
1<<10 = 1024
1<<11 = 2048
1<<12 = 4096
1<<13 = 8192
1<<14 = 16384
1<<15 = 32768
1<<16 = 65536
1<<17 = 131072
1<<18 = 262144
1<<19 = 524288
1<<20 = 1048576
1<<21 = 2097152
1<<22 = 4194304
1<<23 = 8388608
1<<24 = 16777216
1<<25 = 33554432
1<<26 = 67108864
1<<27 = 134217728
1<<28 = 268435456
1<<29 = 536870912
1<<30 = 1073741824
1<<31 = -2147483648
This just underscores that subtle fine points that are easily overlooked (or forgotten) matter and can byte you.

A naive (but perfectly natural) expectation is that the bits will roll off the left end after the eighth bit. But if you rely on this expectation, your code will not work as expected. This can result in extremely difficult to detect and track down.

While C very arguably has a lot more of these subtleties (and, more to the point, a lot more implementation-defined and undefined behavior) that most languages, all languages have them. The C has so many more is actually one of its strengths for the kinds of tasks that it is well-suited for. Unfortunately, hardly anyone uses it for those kinds of tasks, as so it is a significant weakness in most instances.
I know how illogically C (and several other languages) implement these operations, that's a choice made by the designers, fact is shifting is inherently a fixed size concept as you know. Look at a shift register or a CPU register, shifts are simple and uncontroversial.

Lots of languages fall short in these areas, C# for example always converts byte arguments to logical operators to Int32, in that language

byte result = byte1 | byte2; // operands converted to Int32, result therefore Int32

Won't compile (needs an explicit cast), but

result_byte &= some_byte; // No conversion applied by this operator.

will, C# like C, mindlessly "promotes" all the time when no arithmetic is taking place.

Ideally non-size altering operators would behave more intuitively and thus reduce weird behaviors in what are actually pretty simple concept. As you point out the size of the RHS to << is immaterial. So the type of the LHS alone, should determine the results type and that could be chosen to always be the same, same goes for AND, OR, XOR too IMHO.
 
Last edited:

Futurist

Joined Apr 8, 2025
734
Interestingly, more and more compilers are using LLVM for the backend (e.g. Clang), that abstract assembly language is very expressive and allows you to define integer type as any length, from 1 bit to thousands of bits, so one could even have a nibble type in some language and shifting etc is 100% controlled by the code. It's as easy to do arithmetic on 25 or 173 bit integers as it is on 8, 16 or 32, it's totally flexible but few languages are able to leverage that.

LLVM-IR assumes you have infinite registers of any desired length, the LLVM code generator takes care of implementing the abstract assembly code on the physical target, its incredibly powerful, a truly impressive design.
 
Last edited:

WBahn

Joined Mar 31, 2012
32,777
Interestingly, more and more compilers are using LLVM for the backend (e.g. Clang), that abstract assembly language is very expressive and allows you to define integer type as any length, from 1 bit to thousands of bits, so one could even have a nibble type in some language and shifting etc is 100% controlled by the code. It's as easy to do arithmetic on 25 or 173 bit integers as it is on 8, 16 or 32, it's totally flexible but few languages are able to leverage that.

LLVM-IR assumes you have infinite registers of any desired length, the LLVM code generator takes care of implementing the abstract assembly code on the physical target, its incredibly powerful, a truly impressive design.
While tools like this are extremely useful in lots of applications, they are also usually good examples of the costs associated with moving to higher levels of abstraction when working in resource-starved environments, which were the treacherous seas that C was navigating. The first UNIX operating system written in C ran on a PDP-11/20, with a max clock speed of just over 1 MHz and a whopping 64 KB of memory (if it was fully decked out, but could be as little as 24 KB, IIRC).
 

nsaspook

Joined Aug 27, 2009
16,275
It's a common problem with programmers due to their training on abstract concepts often resulting in a search for the clever and new.
Being so far sighted with solutions that they ignore the immediate reality of getting things done.

 
Last edited:

Futurist

Joined Apr 8, 2025
734
It's a common problem with programmers due to their training on abstract concepts often resulting in a search for the clever and new.
Being so far sighted with solutions that they ignore the immediate reality of getting things done.

Shifting a byte or anding two bytes is simple yet these languages have to complicate it with "promotions" forcing the insertion of reverse casts to get the desired byte result, so much for getting things done and abstraction! But if C is all you have then C is all you can use.
 
Last edited:

Futurist

Joined Apr 8, 2025
734
While tools like this are extremely useful in lots of applications, they are also usually good examples of the costs associated with moving to higher levels of abstraction when working in resource-starved environments, which were the treacherous seas that C was navigating. The first UNIX operating system written in C ran on a PDP-11/20, with a max clock speed of just over 1 MHz and a whopping 64 KB of memory (if it was fully decked out, but could be as little as 24 KB, IIRC).
Abstractions are everywhere and the basis of modeling. UNIX emphasizes a "everything is a file" abstraction for example. There's nothing inherently bad about abstracting problems either, yes treating an abstraction as if it were reality can carry costs, but that's a misapplication I think.

Look at the C language again for example, that abstracts bytes as word sized integers, a byte's a byte but the designers chose (on the basis of assumptions) to always promote a byte to an int in an expression because they felt that would be more efficient.

All programming languages abstract the execution model and C is no exception, a C "string" is another inefficient abstraction, getting the current length of 1,000 byte string isn't cheap nor is it constant time, it's unpredictable not a good thing for some real-time MCU systems.
 
Last edited:

WBahn

Joined Mar 31, 2012
32,777
Abstractions are everywhere and the basis of modeling. UNIX emphasizes a "everything is a file" abstraction for example. There's nothing inherently bad about abstracting problems either, yes treating an abstraction as if it were reality can carry costs, but that's a misapplication I think.

Look at the C language again for example, that abstracts bytes as word sized integers, a byte's a byte but the designers chose (on the basis of assumptions) to always promote a byte to an int in an expression because they felt that would be more efficient.

All programming languages abstract the execution model and C is no exception, a C "string" is another inefficient abstraction, getting the current length of 1,000 byte string isn't cheap nor is it constant time, it's unpredictable not a good thing for some real-time MCU systems.
Abstraction is extremely useful and important. Nothing I have said implies that it is not. But C is intended for low-level systems programming, where speed and efficiency are often the primary considerations, and even the degree of abstraction that C imposes can be too detrimental, which is why C allows for the incorporation of assembly language snippets.

The reason that C does integer promotion is because the size of an int is intended to match the natural processing width of the underlying hardware. On many processors, particularly less-capable processors, doing operations on smaller width data at every stop results in a lot of masking operations. Promoting them to the natural data width at the beginning usually costs nothing and doing all of the subsequent work at that natural width is the most time and space efficient. If you have to reduce the width at the end of processing, at least you only have to do it once.

The reason that C has so much undefined and implementation-defined behavior was to allow compiler writers the ability to better match their implementations to the capabilities and limitations of the hardware their compiler was targeting. A good example of this is how the language dealt with integer division. For a long time, the result of dividing two integers that resulted in a negative non-integer nominal result was implementation defined. The natural behavior of most processors is to truncate the result by throwing away any fractional part. In most representations, such as two's complement, this results in rounding toward negative infinity. But most people, which asked what -4.76 would be if the fractional part is thrown away, will say that it is -4, because they are stuck with the mindset of a signed magnitude representation that we humans work with, the result being that round-toward-zero is used. Other languages that came along opted to impose the requirement that integer division use round-to-zero because it is a better match to how humans think. Later versions of the C standard now require this, too. But the result is that a non-trivial amount of code and execution time is spent on forcing this behavior over the much more natural way that almost all processors behave. Few programmers are aware of this, and so they have no reason to even be aware of the performance penalty they are accepting when performing integer arithmetic on signed integers in situations in which their application only needs unsigned values. Those that do can get significant performance gains just by judiciously choosing which variables they declare as unsigned integers.

But, as with all things, there is a price for doing that. In the case of C, giving the kind of power and control that it offers to programmers requires that those same programmers have the knowledge, skill, and self-control to exercise that power and control responsibly. Not surprisingly, this has always been a weak spot in the chain and, again not surprisingly, that has become more and more of an issue as subsequent generations of programmers because increasingly less well-versed in the underlying principles of how computers work and more reliant on the ever-more sophisticated tools to do their thinking for them. The majority of programmers should NOT be programming in C, because they are not capable to doing it properly.
 

Futurist

Joined Apr 8, 2025
734
Abstraction is extremely useful and important. Nothing I have said implies that it is not. But C is intended for low-level systems programming, where speed and efficiency are often the primary considerations, and even the degree of abstraction that C imposes can be too detrimental, which is why C allows for the incorporation of assembly language snippets.

The reason that C does integer promotion is because the size of an int is intended to match the natural processing width of the underlying hardware. On many processors, particularly less-capable processors, doing operations on smaller width data at every stop results in a lot of masking operations. Promoting them to the natural data width at the beginning usually costs nothing and doing all of the subsequent work at that natural width is the most time and space efficient. If you have to reduce the width at the end of processing, at least you only have to do it once.

The reason that C has so much undefined and implementation-defined behavior was to allow compiler writers the ability to better match their implementations to the capabilities and limitations of the hardware their compiler was targeting. A good example of this is how the language dealt with integer division. For a long time, the result of dividing two integers that resulted in a negative non-integer nominal result was implementation defined. The natural behavior of most processors is to truncate the result by throwing away any fractional part. In most representations, such as two's complement, this results in rounding toward negative infinity. But most people, which asked what -4.76 would be if the fractional part is thrown away, will say that it is -4, because they are stuck with the mindset of a signed magnitude representation that we humans work with, the result being that round-toward-zero is used. Other languages that came along opted to impose the requirement that integer division use round-to-zero because it is a better match to how humans think. Later versions of the C standard now require this, too. But the result is that a non-trivial amount of code and execution time is spent on forcing this behavior over the much more natural way that almost all processors behave. Few programmers are aware of this, and so they have no reason to even be aware of the performance penalty they are accepting when performing integer arithmetic on signed integers in situations in which their application only needs unsigned values. Those that do can get significant performance gains just by judiciously choosing which variables they declare as unsigned integers.
I was interested to learn recently that LLVM-IR assembly code, does not itself support signed/unsigned types. Instead all types are simply signless integers of 'n' bits. With respect to add/sub/mul these are agnostic and behave identically (whether the user intends signed or unsigned) with two's complement data.

The LLVM abstract CPU is very interesting, very well designed by true engineers.

https://www.packtpub.com/en-us/lear.../introducing-llvm-intermediate-representation

But, as with all things, there is a price for doing that. In the case of C, giving the kind of power and control that it offers to programmers requires that those same programmers have the knowledge, skill, and self-control to exercise that power and control responsibly. Not surprisingly, this has always been a weak spot in the chain and, again not surprisingly, that has become more and more of an issue as subsequent generations of programmers because increasingly less well-versed in the underlying principles of how computers work and more reliant on the ever-more sophisticated tools to do their thinking for them. The majority of programmers should NOT be programming in C, because they are not capable to doing it properly.
I don't do much C these days other than the odd MCU hobby project, but I used it professionally for thirty years and recognized that without very sound discipline the codebase can turn into a monster with headers including headers including the same headers and all sorts. It's productivity is a bit low for me these days, stuff that could be straightforward sometimes isn't (like no true strings and the need to forward declare stuff all the time and the endless casting back and forth that's often forced upon us) none of those characteristics are in any way helpful really just artifacts of the very limited resources the designers had at their disposal at the time.
 
Last edited:
Top