3. Basic Concepts
3.1 Scope
3.1.1 Do not hide declarations
Reusing the same identifier for different declarations is confusing and difficult to maintain. If the hiding declaration is later removed or the identifier is renamed, a compilation error may not be generated, as the declaration that was previously hidden will now be found. While hidden namespace scope identifiers can still be accesses with a fully qualified name, hidden block scope identifiers will not be accessible.
#include <cstdint> void foo (int32_t); int32_t i; void bar (int32_t max) { for (int32_t i (0); i < max; ++i) // @@- Non-Compliant -@@ { for (int32_t i (0); i < max; ++i) // @@- Non-Compliant -@@ { // no way to access the outer loop index foo (::i); // namespace scope 'i'. foo (i); // innermost declaration of 'i' } } }
In C++, it is possible for the same identifier to refer to both a type and an object or a function. In this case the object or function will hide the type.
#include <cstdint> // valid C++ class C; int32_t C; // @@- Non-Compliant: object C hides type of same name -@@
References
HIC++ v3.3 - 8.2.1
3.2 Program and Linkage
3.2.1 Do not declare functions at block scope
A declaration for a function should be common to its definition, any redeclarations, and any calls to it. To ensure that the same type is used in all declarations, functions should always be declared at namespace scope (See rules <hicpp ref=”dcl.link.single-decl”/> and <hicpp ref=”dcl.link.unnamed-ns-members”/>).
#include <cstdint> int32_t bar () { int32_t foo (); // @@- Non-Compliant -@@ return foo (); } int32_t foo () { }
References
- JSF AV C++ Rev C – 107
- MISRA C++:2008 – 3-1-2
3.3 Storage Duration
3.3.1 Do not use variables with static storage duration
Variables with linkage (and hence static storage duration), commonly referred to as global variables, can be accessed and modified from anywhere in the translation unit if they have internal linkage, and anywhere in the program if they have external linkage. This can lead to uncontrollable relationships between functions and modules. Additionally, certain aspects of the order of initialization of global variables are unspecified and implementation defined in the C++ language standard. This can lead to unpredictable results for global variables that are initialized at run-time (dynamic initialization). This rule does not prohibit use of a const object with linkage, so long as:
- it is initialized through static initialization
- the object is not ODR used
#include <cstdint> static int32_t foo (); extern int32_t ga (foo ()); // @@- Non-Compliant -@@ extern int32_t gb (ga); // @@- Non-Compliant -@@ namespace { int32_t la (0); // @@- Non-Compliant -@@ const int32_t SIZE (100); // @@+ Compliant +@@ }
The order of initialization of block scope objects with static storage duration is well defined. However, the lifetime of such an object ends at program termination, which may be incompatible with future uses of the code, e.g. as a shared library. It is preferable to use objects with dynamic storage duration to represent program state, allocated from the heap or a memory pool.
class Application { // ... }; Application const & theApp() { static Application app; // @@- Non-Compliant -@@ return app; }
References
- HIC++ v3.3 – 8.2.2
3.4 Object Lifetime
3.4.1 Do not return a reference or a pointer to an automatic variable defined within the function
The lifetime of a variable with automatic storage duration ends on exiting the enclosing block. If a reference or a pointer to such a variable is returned from a function, the lifetime of the variable will have ended before the caller can access it through the returned handle, resulting in undefined behavior.
class String { public: String (char *); String (const String &); }; String & fn1 (char * myArg) { String temp (myArg); return temp; // @@- Non-Compliant: temp destroyed here -@@ } String fn2 (char * myArg) { String temp (myArg); return temp; // @@+ Compliant: the caller will get a copy of temp +@@ }
References
- HIC++ v3.3 – 11.7
3.4.2 Do not assign the address of a variable to a pointer with a greater lifetime
The C++ standard defines four kinds of storage duration:
- Static
- Thread
- Automatic
- Dynamic
The lifetime of objects with the first three kinds of storage duration is fixed, respectively:
- Until program termination
- Until thread termination
- Upon exiting the enclosing block.
Therefore, undefined behavior will likely occur if an address of a variable with automatic storage duration is assigned to a pointer with static or thread storage duration, or one defined in an outer block. Similarly, for a thread_local variable aliased to a pointer with static storage duration.
#include <cstdint> void foo (bool b) { int32_t * p; if (b) { int32_t c = 0; p = &c; // @@- Non-Compliant -@@ } }
If using high_integrity::thread, then references or pointers with local storage duration should not be passed into threads that have the high_integrity::DETACH property.
#include <cstdint> #include "high_integrity.h" using high_integrity::thread; using high_integrity::ThreadExec; void bar(int32_t &); void foo () { int32_t i; thread <ThreadExec::DETACH> t(bar, std::ref(i)); // @@- Non-Compliant: -@@ // @@- lifetime of 'i' may end -@@ // @@- before thread completes -@@ }
References
- JSF AV C++ Rev C – 173
- MISRA C++:2008 – 7-5-2
3.4.3 Use RAII for resources
Objects with non-trivial destructors and automatic storage duration have their destructors called implicitly when they go out of scope. The destructor will be called both for normal control flow and when an exception is thrown. The same principle does not apply for a raw handle to a resource, e.g. a pointer to allocated memory. By using a manager class, the lifetime of the resource can be correctly controlled, specifically by releasing it in the destructor. This idiom is known as Resource Acquisition Is Initialization (RAII) and the C++ language standard provides RAII wrappers for many resources, such as:
- dynamically allocated memory, e.g. std::unique_ptr
- files, e.g. std::ifstream
- mutexes, e.g. std::lock_guard
#include <memory> #include <cstdint> void foo_v1 () { int32_t * p = new int32_t; // @@- Non-Compliant -@@ // ... possibly throwing an exception - resource not freed delete p; } void foo_v2 () { std::unique_ptr<int32_t> p (new int32_t ()); // @@+ Compliant +@@ // ... possibly throwing an exception - resource freed }
The following example demonstrates how RAII can also be used to avoid deadlock when an exception is thrown.
#include <list> #include <mutex> #include <cstdint> class ListWrapper { public: void add1(int32_t val) { // @@- Non-Compliant: 'unlock' not called if exception thrown by 'push_back' -@@ mut.lock (); lst.push_back(val); // May throw an exception mut.unlock (); } void add2(int32_t val) { // @@+ Compliant: Using lock guarantees unlocking, even where an exception is thrown +@@ std::lock_guard<std::mutex> lock(mut); lst.push_back(val); // May throw an exception } // ... private: std::list<int32_t> lst; mutable std::mutex mut; };
Other benefits of using RAII are:
- Clear documentation of resource ownership.
- Pre/post conditions when accessing memory.
#include <cstdint> #include <cassert> #include <memory> int32_t & f1 () { int32_t * result (new int32_t ()); return *result; // @@- Non-Compliant -@@ } std::unique_ptr<int32_t> f2 () { std::unique_ptr<int32_t> result (new int32_t ()); return result; // @@+ Compliant +@@ } void f3 () { std::weak_ptr<int32_t> p1; { std::shared_ptr<int32_t> p2 (std::make_shared<int32_t> (0)); p1 = p2; } // p2 goes out of scope // can check if pointer is expired assert ( ! p1.expired () && "Ensure is still valid" ); int32_t i = *p1.lock (); }
References
- HIC++ v3.3 – 3.2.5
- HIC++ v3.3 – 9.5
- HIC++ v3.3 – 12.5
- HIC++ v3.3 – 12.8
- Williams Concurrency – 3.2.1
- CERT C++ – CON02-CPP
3.5 Types
3.5.1 Do not make any assumptions about the internal representation of a value or object
Avoid C++ constructs and practices that are likely to make your code non-portable:
- A union provides a way to alter the type ascribed to a value without changing its representation. This reduces type safety and is usually unnecessary. In general it is possible to create a safe abstraction using polymorphic types.
- Integer types other than signed / unsigned char have implementation defined size. Do not use integer types directly, instead use size specific typedefs, defined in a common header file, which can then be easily adjusted to match a particular platform.
- Do not mix bitwise and arithmetic operations on the same variable, as this is likely to be non portable between big and little endian architectures.
- Do not assume the layout of objects in memory, e.g. by comparing pointers to different objects with the relational operators, using the offsetof macro, or performing pointer arithmetic within an object with unspecified or implementation defined layout.
#include <cstdint> union U // @@- Non-Compliant -@@ { float f; int32_t i; // @@- Non-Compliant -@@ }; uint32_t foo (uint32_t u) { --u; return u & 0xFFU; // @@- Non-Compliant: mixing arithmetic and bitwise operations -@@ } bool cmp (int32_t * lhs, int32_t * rhs) { return lhs < rhs; // @@- Non-Compliant -@@ }
References
- HIC++ v3.3 - 13.6
- HIC++ v3.3 - 15.1
- JSF AV C++ Rev C - 210
- JSF AV C++ Rev C - 210.1
- JSF AV C++ Rev C - 147
- JSF AV C++ Rev C - 215
- MISRA C++:2008 - 3-9-3
- MISRA C++:2008 - 5-0-15