To add to the other answers with some more background about why this is an issue:
In C++, char, signed char, and unsigned char are three distinct types. char may be signed or unsigned, but that’s mostly irrelevant; char is not the same as either signed char or unsigned char. char is supposed to be used for character data (whatever that may mean; it gets complicated). signed char and unsigned char are supposed to be byte-sized integers, signed and unsigned respectively. (And std::byte is supposed to mean an actual byte just as a bag-of-bits, rather than a (integer) number.) Yes, it’s all numbers under the hood, but numbers mean different things in computing.
As noted in the other answers, some character values are not printable. So some values of char are not printable. However since signed char and unsigned char are supposed to represent numeric values, there should be no values of signed char or unsigned char that are unprintable.
However…
The fixed-width integer types in C++ are defined to be type aliases (“typedefs”)… not actual types. That means that std::int8_t is just an alias for some other type. And what type would it be an alias for? Well, the best choice on most platforms—often the only practical choice—is signed char.
Similarly, the best choice for the underlying type of std::uint8_t is unsigned char.
Now, unfortunately, IOStreams was specified to treat signed char and unsigned char just like char. In my opinion, this was a mistake. The upshot of that is that doing std::cout << std::int8_t{65} is exactly the same as doing std::cout << char{65}… which—even though std::int8_t is supposed to be a number, and should print 65—will probably print A on an ASCII or UTF-8 system.
(And it gets even worse, because std::int8_t doesn’t have to be aliased to signed char. It could be aliased to some other, platform-specific type. And that type could be treated as a number when using IOStreams. So you don’t even know whether std::cout << std::int8_t{??} will print a character or a number on other platforms. It’s a mess, which is why I say treating signed char and unsigned char the same as char when doing output was a mistake.)
This has been fixed in the modern formatting library. std::int8_t is still (probably) signed char, but the modern formatting library is smart enough to realize that signed char and unsigned char are not char, and it treats them appropriately as numbers. See for yourself.
One last thing: while it is not technically wrong to cast std::int8_t to int (and std::uint8_t to unsigned int), it’s a little verbose and unnecessary. It’s much simpler to just use the + prefix:
std::cout << std::int8_t{65}; // prints a character (probably 'A')
std::cout << +std::int8_t{65}; // always prints a number (65)
All the + is doing there is promoting the signed char (which std::int8_t really is) to int, which then prints normally as a number.
Doing it this way is optional for std::int8_t or signed char, because manually casting to int is always correct for those types. But if you ever want to print the numeric value of a char, using the + is the only correct way to do it. That is because you don’t know if char is signed or not, so you don’t know whether it is correct to cast it to a signed char or unsigned char. The + always does the right thing.