Reading Time: 3 minutes
In my previous post, I introduced a process you can apply to prepare the application for the 64-bit porting process:
Perform mutation testing via runtime memory error detection
Use static analysis to identify error-prone and non-portable code
Repeat runtime memory error detection
Perform unit testing (Optional)
In this post, we’ll take a closer look at some specific static code analysis rules you might want to check in step 2.
Rules that Expose Error-Prone Code
- Never return a reference to a local object or a dereferenced pointer initialized by “new” within the function. Returning a reference to a local object might cause stack corruption. Returning a dereferenced pointer initialized by “new” within the function might cause a memory leak.
- Never convert a const to non-const. This can undermine the data integrity by allowing values to change that are assumed to be constant. This practice also reduces the readability of the code because you cannot assume const variables to be constant.
- If a class has any virtual functions it shall have a virtual destructor. This standard prevents memory leaks in derived classes. A class that has any virtual functions is intended to be used as a base class, so it should have a virtual destructor to guarantee that the destructor will be called when the derived object is referenced through a pointer to the base class.
- Public member functions shall return const handles to member data.
- When you provide non-const handles to member data, you undermine encapsulation by allowing callers to modify member data outside of member functions.
- A pointer to a class shall not be converted to a pointer of a second class unless it inherits from the second. This “invalid” down casting can result in wild pointers, data corruption problems, and other errors.
- Do not directly access global data from a constructor.
The order of initialization of static objects defined in different compilation units is not defined in the C++ language definition. Therefore, accessing global data from a constructor might result in reading from uninitialized objects.
For more rules, see the works of Scott Meyers, Martin Klaus, and Herb Sutter.
Rules that Expose Difficult-to-Port Code
After you locate and repair this error-prone code, start looking for code that works fine on your current platform/architecture, but that might not port well. Some rules that are applicable to most 64-bit porting projects include:
- Use standard types whenever applicable. Consider using size_t rather than “int”, for example. Use uint64_t if you want a 64-bit unsigned integer. Not only will this practice help identify and prevent current bugs in the code, it will help with porting effort in the future when the code is ported to 128-bit processors.
- Review all existing uses of long data types in the source code. If the values to be held in such variables, fields, and parameters will fit in the range of 2Gig-1 to –2Gig or 4Gig to 0, then it is probably best to use int32_t or uint32_t, respectively.
- Examine all instances of narrowing assignment. Avoid such assignments because the assignment of a long value to an int will result in truncation of the 64 bit value.
- Find narrowing casts. Use narrowing casts on expressions, not operands.
- Find casts from long* to int*. In a 32 bit environment, these might have been used interchangeably. Examine all instances of incompatible pointer assignments.
- Find casts from int* to long*. In a 32 bit environment, these might have been used interchangeably. Examine all instances of incompatible pointer assignments.
- Find long values that are initialized with int literals. Avoid such initializations because integral constants might be represented as 32 bit types even when used in expressions with 64 bit types.
- Locate int literals in binary operations for which the result is assigned to a long value. 64 bit multiplication is desired if the result is a 64 bit value.
- Find int constants used in 64 bit expressions. Use 64 bit values in 64 bit expressions.
- Find uses of multiplicative expressions not containing a 64-bit type in either operand. To have integral expressions produce 64 bit results, at least one of the operands must have a 64-bit signed or unsigned data type.
- Use appropriate suffixes on integer and floating literal constants if your compiler allows them. For example:
unsigned int j = 123u;
unsigned long k = 456UL;
Photo credit: quapan