What's the difference between your byte_copy function and the standard
On pretty much any modern system you can expect a char to be 8 bits. The C++ standard guarantees that char and unsigned char are at least 8 bits wide, with a range of at least −2⁷ to 2⁷ − 1 (inclusive) and 0 to 2⁸ − 1 (inclusive) respectively, see [basic.fundamental] in the standard.
uint8_t, the C++ standard refers back to the C standard:
184.108.40.206 Exact-width integer types
The typedef name intN_t designates a signed integer type with width N, no padding
bits, and a two’s complement representation. Thus, int8_t denotes such a signed
integer type with a width of exactly 8 bits.
The typedef name uintN_t designates an unsigned integer type with width N and no
padding bits. Thus, uint24_t denotes such an unsigned integer type with a width of
exactly 24 bits.
These types are optional. However, if an implementation provides integer types with
widths of 8, 16, 32, or 64 bits, no padding bits, and (for the signed types) that have a
two’s complement representation, it shall define the corresponding typedef names.
I'm a little confused because I keep reading things like "unsigned char" is always a byte, but then I read in other places don't use it in math.
If you need an integer of 8 bits (an integer you want to perform math on), use int8_t/uint8_t, not char/unsigned char. Even though uint8_t == unsigned char on most platforms, it's about showing your intent.
If you need raw memory access, don't use uint8_t, use std::byte in C++20, or char/unsigned char before C++20. See also [basic.lval], 220.127.116.11
If a program attempts to access (3.1) the stored value of an object through a glvalue whose type is not
similar (7.3.5) to one of the following types the behavior is undefined: 51
(11.1) — the dynamic type of the object,
(11.2) — a type that is the signed or unsigned type corresponding to the dynamic type of the object, or
(11.3) — a char, unsigned char, or std::byte type.