Implementations might differ between the actual sizes of types, but on most, types like unsigned int and float are always 4 bytes. But why does a type always occupy a certain amount of memory no matter its value? For example, if I created the following integer with the value of 255
int myInt = 255;
Then myInt
would occupy 4 bytes with my compiler. However, the actual value, 255
can be represented with only 1 byte, so why would myInt
not just occupy 1 byte of memory? Or the more generalized way of asking: Why does a type have only one size associated with it when the space required to represent the value might be smaller than that size?
I like Sergey's house analogy, but I think a car analogy would be better.
Imagine variable types as types of cars and people as data. When we're looking for a new car, we choose the one that fits our purpose best. Do we want a small smart car that can only fit one or two people? Or a limousine to carry more people? Both have their benefits and drawbacks like speed and gas mileage (think speed and memory usage).
If you have a limousine and you're driving alone, it's not going to shrink to fit only you. To do that, you'd have to sell the car (read: deallocate) and buy a new smaller one for yourself.
Continuing the analogy, you can think of memory as a huge parking lot filled with cars, and when you go to read, a specialized chauffeur trained solely for your type of car goes to fetch it for you. If your car could change types depending on the people inside it, you would need to bring a whole host of chauffeurs every time you wanted to get your car since they would never know what kind of car will be sitting in the spot.
In other words, trying to determine how much memory you need to read at run time would be hugely inefficient and outweigh the fact that you could maybe fit a few more cars in your parking lot.
The compiler is allowed to make a lot of changes to your code, as long as things still work (the "as-is" rule).
It would be possible to use a 8-bit literal move instruction instead of the longer (32/64 bit) required to move a full
int
. However, you would need two instructions to complete the load, since you would have to set the register to zero first before doing the load.It is simply more efficient (at least according to the main compilers) to handle the value as 32 bit. Actually, I've yet to see a x86/x86_64 compiler that would do 8-bit load without inline assembly.
However, things are different when it comes to 64 bit. When designing the previous extensions (from 16 to 32 bit) of their processors, Intel made a mistake. Here is a good representation of what they look like. The main takeaway here is that when you write to AL or AH, the other is not affected (fair enough, that was the point and it made sense back then). But it gets interesting when they expanded it to 32 bits. If you write the bottom bits (AL, AH or AX), nothing happens to the upper 16 bits of EAX, which means that if you want to promote a
char
into aint
, you need to clear that memory first, but you have no way of actually using only these top 16 bits, making this "feature" more a pain than anything.Now with 64 bits, AMD did a much better job. If you touch anything in the lower 32 bits, the upper 32 bits are simply set to 0. This leads to some actual optimizations that you can see in this godbolt. You can see that loading something of 8 bits or 32 bits is done the same way, but when you use 64 bits variables, the compiler uses a different instruction depending on the actual size of your literal.
So you can see here, compilers can totally change the actual size of your variable inside the CPU if it would produce the same result, but it makes no sense to do so for smaller types.
There are objects that in some sense have variable size, in the C++ standard library, such as
std::vector
. However, these all dynamically allocate the extra memory they will need. If you takesizeof(std::vector<int>)
, you will get a constant that has nothing to do with the memory managed by the object, and if you allocate an array or structure containingstd::vector<int>
, it will reserve this base size rather than putting the extra storage in the same array or structure. There are a few pieces of C syntax that support something like this, notably variable-length arrays and structures, but C++ did not choose to support them.The language standard defines object size that way so that compilers can generate efficient code. For example, if
int
happens to be 4 bytes long on some implementation, and you declarea
as a pointer to or array ofint
values, thena[i]
translates into the pseudocode, “dereference the address a + 4×i.” This can be done in constant time, and is such a common and important operation that many instruction-set architectures, including x86 and the DEC PDP machines on which C was originally developed, can do it in a single machine instruction.One common real-world example of data stored consecutively as variable-length units is strings encoded as UTF-8. (However, the underlying type of a UTF-8 string to the compiler is still
char
and has width 1. This allows ASCII strings to be interpreted as valid UTF-8, and a lot of library code such asstrlen()
andstrncpy()
to continue to work.) The encoding of any UTF-8 codepoint can be one to four bytes long, and therefore, if you want the fifth UTF-8 codepoint in a string, it could begin anywhere from the fifth byte to the seventeenth byte of the data. The only way to find it is to scan from the beginning of the string and check the size of each codepoint. If you want to find the fifth grapheme, you also need to check the character classes. If you wanted to find the millionth UTF-8 character in a string, you’d need to run this loop a million times! If you know you will need to work with indices often, you can traverse the string once and build an index of it—or you can convert to a fixed-width encoding, such as UCS-4. Finding the millionth UCS-4 character in a string is just a matter of adding four million to the address of the array.Another complication with variable-length data is that, when you allocate it, you either need to allocate as much memory as it could ever possibly use, or else dynamically reallocate as needed. Allocating for the worst case could be extremely wasteful. If you need a consecutive block of memory, reallocating could force you to copy all the data over to a different location, but allowing the memory to be stored in non-consecutive chunks complicates the program logic.
So, it’s possible to have variable-length bignums instead of fixed-width
short int
,int
,long int
andlong long int
, but it would be inefficient to allocate and use them. Additionally, all mainstream CPUs are designed to do arithmetic on fixed-width registers, and none have instructions that directly operate on some kind of variable-length bignum. Those would need to be implemented in software, much more slowly.In the real world, most (but not all) programmers have decided that the benefits of UTF-8 encoding, especially compatibility, are important, and that we so rarely care about anything other than scanning a string from front to back or copying blocks of memory that the drawbacks of variable width are acceptable. We could use packed, variable-width elements similar to UTF-8 for other things. But we very rarely do, and they aren’t in the standard library.
The short answer is: Because the C++ standard says so.
The long answer is: What you can do on a computer is ultimately limited by hardware. It is, of course, possible to encode an integer into a variable number of bytes for storage, but then reading it would either require special CPU instructions to be performant, or you could implement it in software, but then it would be awfully slow. Fixed-size operations are available in the CPU for loading values of predefined widths, there are none for variable widths.
Another point to consider is how computer memory works. Let's say your integer type could take up anywhere between 1 to 4 bytes of storage. Suppose you store the value 42 into your integer: it takes up 1 byte, and you place it at memory address X. Then you store your next variable at location X+1 (I'm not considering alignment at this point) and so on. Later you decide to change your value to 6424.
But this doesn't fit into a single byte! So what do you do? Where do you put the rest? You already have something at X+1, so can't place it there. Somewhere else? How will you know later where? Computer memory does not support insert semantics: you can't just place something at a location and push everything after it aside to make room!
Aside: What you're talking about is really the area of data compression. Compression algorithms exist to pack everything tighter, so at least some of them will consider not using more space for your integer than it needs. However, compressed data is not easy to modify (if possible at all) and just ends up being recompressed every time you make any changes to it.
There are pretty substantial runtime performance benefits from doing this. If you were to operate on variable size types, you would have to decode each number before doing the operation (machine code instructions are typically fixed width), do the operation, then find a space in memory big enough to hold the result. Those are very difficult operations. It's much easier to simply store all of the data slightly inefficiently.
This is not always how it is done. Consider Google's Protobuf protocol. Protobufs are designed to transmit data very efficiently. Decreasing the number of bytes transmitted is worth the cost of additional instructions when operating on the data. Accordingly, protobufs use an encoding which encodes integers in 1, 2, 3, 4, or 5 bytes, and smaller integers take fewer bytes. Once the message is received, however, it is unpacked into a more traditional fixed-size integer format which is easier to operate on. It's only during network transmission that they use such a space-efficient variable length integer.
Computer memory is subdivided into consecutively-addressed chunks of a certain size (often 8 bits, and referred to as bytes), and most computers are designed to efficiently access sequences of bytes that have consecutive addresses.
If an object's address never changes within the object's lifetime, then code given its address can quickly access the object in question. An essential limitation with this approach, however, is that if an address is assigned for address X, and then another address is assigned for address Y which is N bytes away, then X will not be able to grow larger than N bytes within the lifetime of Y, unless either X or Y is moved. In order for X to move, it would be necessary that everything in the universe that holds X's address be updated to reflect the new one, and likewise for Y to move. While it's possible to design a system to facilitate such updates (both Java and .NET manage it pretty well) it's much more efficient to work with objects that will stay in the same location throughout their lifetime, which in turn generally require that their size must remain constant.