49 Intermediate The Hundred
The Character That Went Negative
a pixel so bright it came out the other side
#include <iostream>
int main() {
char sample = 200; // one 8-bit greyscale pixel — nearly white
std::cout << "value: " << static_cast<int>(sample) << "\n";
std::cout << "bright: " << (sample > 100 ? "yes" : "no") << "\n";
std::cout << "dimmed: " << sample / 2 << "\n";
}
Run it. How bright is that pixel?
Why¶
Plain char is a third character type, distinct from both signed char and
unsigned char, and the standard lets every implementation choose which of the two it
behaves like. On x86-64 — Linux, macOS, Windows alike — it is signed, so it spans
−128…127 and the 200 wraps to −56; every comparison and every division downstream then
works on that. Rebuild the same file with -funsigned-char and it prints 200, yes, 100
instead — exactly the default on ARM and PowerPC Linux. -Wall -Wextra say nothing about
the initialization; -Wpedantic does: overflow in conversion from 'int' to 'char'
changes value from '200'.
The practical bite lives in <cctype>: std::toupper, std::isalpha and friends
require an argument representable as unsigned char or EOF, so passing a negative
char — any byte above 127, which includes every non-ASCII UTF-8 byte — is undefined
behavior. glibc keeps 128 spare table entries below zero, so on GCC/x86-64 it quietly
returns the right answer and the bug hides; another library is free to read straight off
the front of its table.
The fix¶
Use unsigned char (or std::uint8_t) for raw bytes, and cast at every ctype call site:
unsigned char sample = 200; // value: 200, bright: yes, dimmed: 100
std::toupper(static_cast<unsigned char>(sample)); // defined for every byte
Takeaway: char is for characters, not for byte values — the moment a byte can
exceed 127, spell the type unsigned char.
Try it: g++ -std=c++17 main.cpp -o demo && ./demo