It's unnecessary complicated for historical reasons. And as history goes further, it only gets more complicated. E.g., due to its promotion rules stemming from the 60s PDP-7, the expression arguments are getting cast into signed type during the operation, 50000 * 50000 == -1794967296. But only on MSVC because, for compatibility reasons, MSVC still keeps its `int` type 32-bit while, for instance, clang doesn't, so on clang, the same expression will give you the correct 2500000000. Imagine writing the code, running all the tests and only discovering the issue on one of the CI build machines with no proper way to debug it. Yeah...
And this is totally by the book, the size of 'int' is allowed to deviate from compiler to compiler by design. This comes from the 70s C that was suppose to become "portable assembly" and back then, apparently, portability meant compiling without errors. So C++ addresses this issue and proposes types with fixed length. You can now write your types explicitly. Like this: uint64_t(uint16_t(50000) * uint16_t(50000))
And get 18446744071914584320. Because, nevertheless the new fancy types, the promotion rules from PDP-7 still apply. Your expression is getting cast to the next signed standard type that can accommodate `uint16_t` and this is still `int`. So you get -1794967296 and only then convert it to unsigned `uint64_t` number.
Fiddly bits and surprises will keep on coming.
I've been writing in C++ for, what, 17 years now?, just yesterday I was rewriting a unit test with floating point data into integers. It was something like this: "1.234, 0.123, 5.678" and I wanted "1234, 123, 5678". So I just removed the decimal points and let the heading zero in 0123 hang. 0ops! In C++, heading zero marks octal notation, and 0123 actually means 83.
But the thing responsible for the most rants is C++ static polymorphism. Since you don't have Rust-like type traits, the only way to ensure that one piece of code will work with another is to compile them both together. This means that for every minor change in an underlying library, you have to recompile all the dependies to see if their tests still work and this gives you enough time to write a rant or two.