![]() | |
![]() ![]() |
![]() | Imperfect C++ Practical Solutions for Real-Life Programming By Matthew Wilson |
Table of Contents | |
Chapter 13. Fundamental Types |
13.1. May I Have a byte?Computers use the byte as their fundamental unit of memory storage,[1] so it would seem natural to have a type with which to access and manipulate bytes. Most of the time in C++, and in C, we are interested in dealing with specific types, for example, char const*, Person&, and so on. However, there are times when we need to manipulate opaque blocks of memory, such as in data compression. In such cases, we are dealing with chunks of bytes, where the content of individual bytes has no meaning.
Unfortunately, there is no byte type in C/C++, and so the common practice in such cases is to use char. This makes sense, since the size of char is always one byte. The (C++-98) standard does not say this directly, but it does say (C++-98: 5.3.3) that "the sizeof operator yields the number of bytes in the object representation of its operand" and "sizeof(char), sizeof(signed char) and sizeof(unsigned char) are 1." Clearly, then, sizeof(char) == sizeof(byte) must always be true. (Note that a byte is not necessarily 8 bits, just that it is "large enough to fit the basic character set" [C++-98: 3.9.1.1]. Irrespective of the bit size, however, sizeof(byte) == 1 is an axiom.) However, there are problems with this approach. 13.1.1 Look for a SignThe first problem is that the "signedness" of the char type, when not qualified with signed or unsigned, is implementation dependent. This leads to problems when the char-as-byte variable is used in arithmetic conversions where we need to interpret its meaning as a number. If char is signed, then assigning it to a larger type will sign-extend the value [Stro1997]. If an 8-bit char's value were 0xff, then when it is involved in an integer promotion, say to a 32-bit integer (whether signed or unsigned), the result would be 0xffffffff, which is –1 for signed or 4294967295 for unsigned. If char is unsigned, the same assignment will result in a value of 0x000000ff, which is 255 irrespective of integer sign. The answer to this issue is very simple: always specify the sign. In my opinion the better option is to use unsigned to avoid the sign extension, since this fits my notion of a byte as a collection of bits, though I concede that this is not unequivocally superior to choosing signed; some argue that signed is preferable for finding underrun bugs. The important thing is to make an explicit choice and remove the ambiguity. 13.1.2 It's All in the NameThe second problem is that the names of types carry semantic significance: they suggest their intended use. Of course, the names of types don't matter in the least to a compiler, but they are extremely important to human writers and readers of code. Using (un)signed char for a byte is misleading. Using a type char indicates to the reader that the variable represents a character; if it's a byte, it should be called byte, or byte_t or something similarly obvious. Given Robert L. Glass's [Glas2003] assertion that 60 percent of software engineering is maintenance, it seems prudent to keep your code's maintainer(s)—which may be you in 18 months' time—as informed as possible. The answer here is to provide a suitable typedef for a byte type, as in: typedef unsigned char byte_t; Consider the following two variable declarations: unsigned char v1; byte_t v2; v2 is unambiguously a byte, with all the semantic significance that conveys. v1 may or may not connote "byte" as opposed to character, depending on the experience and instincts of the developer reading this code. Unfortunately, the APIs for multibyte character set (MBCS) encoding schemes in the very widely used Visual C++ libraries use unsigned char (const) * for MBCS character strings, which pretty much nullifies these instincts for developers who use these libraries. Even when unsigned char always says "byte" to all the readers of such code, it is all too easy to leave off the sign qualifier, either in your code or in your mind. Another benefit of the explicit stipulation of sign is that compilers will reject statements like the following: signed byte_t x; unsigned byte_t y; This further emphasizes the (logical) independence of bytes from sign. 13.1.3 Peering into the VoidAn important use for a byte type is to be able to represent pointers to bytes. A common practice is to use void*, which certainly primes the reader to think in terms of "pure memory." Using void* has the advantage that the compiler will pick up on any attempts to use pointer arithmetic, which it otherwise performs automatically for us with other pointer types. However, it presents its own difficulties in that pointer arithmetic involves casts to and from byte-sized type pointers, with the consequent potential for mistakes. Some APIs express pointers to bytes as unsigned char (const)*. The problem from a human point of view is that seeing a parameter of type char* inclines the reader to think of the recipient of a writable string buffer or of a single character, and a parameter of type char const* to think of a null-terminated string. Explicitly qualifying with (un)signed helps in this regard, of course, but it only takes a couple of slips of the sign qualifier to propagate throughout the code base and you're at 300kph on the autobahn to unmaintainability. In any case, we've seen that some libraries use unsigned char to represent a character. A slightly better solution might be to use the C99 type uint8_t (const)* (see next item), but this is not (yet) part of standard C++, and it still says "integer" (number) rather than "byte" (opaque value).[2]
Using byte_t (const)* s means that we can express pointers to opaque bytes in a simple, readable way, and we don't have to resort to any casting with pointer arithmetic, so the code is immune to pointer-type mismatch and offset problems. 13.1.4 Extra SafetyThere's one more step that can be employed to eke out a little extra type safety. I've used the approach shown in Listing 13.1 in several libraries. Listing 13.1.#if defined(ACMELIB_COMPILER_IS_INTEL) || \ defined(ACMELIB_COMPILER_IS_MSVC) typedef unsigned __int8 byte_t; ... #else typedef unsigned char byte_t; #endif /* compiler */ The __int8 type is an 8-bit integer defined by the Intel and Visual C++ (and a few other) compilers. There is a feature (maybe it's a bug?) of the Visual C++ 6.0 compiler, which is emulated in the Intel compiler,[3] which means that int8 and char are not considered as simple aliases of each other (see section 18.3). Rather they represent distinct types.
We can turn this to our own advantage by defining byte_t to be unsigned __int8 when compiling with these compilers, which allows us to write functions with greater type safety. (Naturally, the fact that we're defining byte in terms of a specifically 8-bit integer would run aground on a platform where a byte might contain 9 bits, but on such a platform we'd adjust the definition of byte_t accordingly.) Consider the following class, which is written to work with memory but not characters. class NoCharsPlease { public: NoCharsPlease(byte_t *); // Not to be implemented private: #ifdef ACMELIB_CF_DISTINCT_BYTE_SUPPORT NoCharsPlease(char *); NoCharsPlease(signed char *); NoCharsPlease(unsigned char *); #endif /* ACMELIB_CF_DISTINCT_BYTE_SUPPORT */ }; Now if you try to pass a pointer to a char-based block of memory, the compiler will reject it.
byte_t *pc = new byte_t[10];
unsigned char *puc = new char[10];
NoCharsPlease ncp1(pc); // Ok
NoCharsPlease ncp2(puc); // Compile error
Since this only works on a small (and aging) subset of compilers, and relies on nonstandard compiler features at that, it is arguable as to whether you would want to use this technique. It can be helpful when you are using several compilers to maximize your information, however (see Appendix C). Since I tend to do that, I use this technique. A portable and future-proof alternative, for C++ compilation only, is to use the True Typedef technique (see chapter 18), thereby making conversions between the byte type and any other integral type invalid. ![]() |
![]() | |
![]() ![]() |