└── README.md /README.md: -------------------------------------------------------------------------------- 1 | This is an explanation with code examples for NASA Jet Propulsion Lab's "The Power of 10: Rules for Developing Safety-Critical Code" 2 | 3 | Found here: https://en.wikipedia.org/wiki/The_Power_of_10:_Rules_for_Developing_Safety-Critical_Code 4 | 5 | ------------------ 6 | 7 | 1. Avoid complex flow constructs, such as goto and recursion. 8 | 9 | ------------------ 10 | 11 | **Rule Explanation:** 12 | This rule advises against the use of complex flow constructs such as "goto" statements and recursion. The "goto" statement can lead to code that's difficult to trace and understand, making it more prone to errors. Recursion, on the other hand, can lead to stack overflow issues if not used carefully, which can crash the program. 13 | 14 | **Without Rule Applied:** 15 | ```c 16 | // using goto statement 17 | void someFunction(int num) { 18 | start: 19 | if(num > 0) { 20 | std::cout << num << std::endl; 21 | num--; 22 | goto start; 23 | } 24 | } 25 | 26 | // using recursion 27 | int factorial(int n) { 28 | if(n <= 0) 29 | return 1; 30 | else 31 | return n * factorial(n - 1); 32 | } 33 | 34 | ``` 35 | **With Rule Applied:** 36 | ```c 37 | // replacing goto with a loop 38 | void someFunction(int num) { 39 | while(num > 0) { 40 | std::cout << num << std::endl; 41 | num--; 42 | } 43 | } 44 | 45 | // replacing recursion with a loop 46 | int factorial(int n) { 47 | int result = 1; 48 | for(int i = 1; i <= n; i++) { 49 | result *= i; 50 | } 51 | return result; 52 | } 53 | 54 | ``` 55 | In the updated examples, control flow is easier to follow, and the risk of stack overflow is significantly reduced. 56 | 57 | ------------------ 58 | 59 | 2. All loops must have fixed bounds. This prevents runaway code. 60 | 61 | ------------------ 62 | 63 | **Rule Explanation:** 64 | This rule suggests that all loops should have a predefined limit to the number of iterations they can perform. This is critical in safety-related systems to prevent "runaway" code, where a loop could continue indefinitely and either freeze the system or consume resources excessively, potentially leading to system instability or failure. 65 | 66 | **Without Rule Applied:** 67 | ```cpp 68 | // loop with an unpredictable end 69 | int i = 0; 70 | while(i != 10) { 71 | // if due to some bug or condition, 'i' never reaches 10, this becomes an infinite loop 72 | i += 2; 73 | if(i > 20) i = 0; 74 | } 75 | 76 | ``` 77 | **With Rule Applied:** 78 | ```cpp 79 | // loop with a fixed bound 80 | for(int i = 0; i < 10; ++i) { 81 | // the loop will execute exactly 10 times 82 | } 83 | 84 | ``` 85 | In the updated example, the loop has a fixed upper bound (10 iterations), which prevents it from potentially becoming a runaway code. 86 | 87 | ------------------ 88 | 89 | 3. Avoid heap memory allocation. 90 | 91 | ------------------ 92 | 93 | **Rule Explanation:** 94 | This rule advises against using heap memory allocation. Memory allocation on the heap can lead to problems such as memory leaks (if not deallocated properly) or out-of-memory errors (if too much memory is allocated). Also, dynamic memory allocation can result in fragmented memory, which can degrade performance. These issues can be critical in a safety-related system. 95 | 96 | **Without Rule Applied:** 97 | ```cpp 98 | int* arr = new int[10]; // Allocating memory on the heap 99 | //... use arr 100 | delete[] arr; // If this line is missed or not reached due to an early return, a memory leak occurs 101 | 102 | ``` 103 | **With Rule Applied:** 104 | ```cpp 105 | int arr[10]; // Allocating memory on the stack 106 | //... use arr 107 | // No need for explicit deallocation. Memory gets automatically freed when arr goes out of scope 108 | 109 | ``` 110 | In the updated example, the array is allocated on the stack, which doesn't require explicit memory deallocation and is less prone to errors. Note that stack memory is limited, and large arrays may still need to be allocated on the heap, albeit with diligent management to avoid the aforementioned issues. 111 | 112 | ------------------ 113 | 114 | 4. Restrict functions to a single printed page. 115 | 116 | ------------------ 117 | 118 | **Rule Explanation:** 119 | This rule encourages limiting the length of a function to what can be viewed on a single printed page (or a single screen page), traditionally about 60 lines. Long functions tend to be more complex, harder to understand, test, and debug. Keeping functions short and focused on one task improves readability and maintainability of the code. 120 | 121 | **Without Rule Applied:** 122 | ```cpp 123 | void complexFunction() { 124 | // Block of code 1 125 | // ... 126 | // Block of code 2 127 | // ... 128 | // Block of code 3 129 | // ... 130 | // Block of code n 131 | // This function spans multiple pages/screens 132 | } 133 | 134 | ``` 135 | **With Rule Applied:** 136 | ```cpp 137 | void task1() { 138 | // Block of code 1 139 | } 140 | 141 | void task2() { 142 | // Block of code 2 143 | } 144 | 145 | void task3() { 146 | // Block of code 3 147 | } 148 | 149 | void simpleFunction() { 150 | task1(); 151 | task2(); 152 | task3(); 153 | // This function fits in a single printed/screen page 154 | } 155 | 156 | ``` 157 | In the updated example, the complex function has been broken down into smaller, more manageable functions, each performing a specific task. This enhances the readability and maintainability of the code. 158 | 159 | ------------------ 160 | 161 | 5. Use a minimum of two runtime assertions per function. 162 | 163 | ------------------ 164 | 165 | **Rule Explanation:** 166 | This rule suggests using at least two runtime assertions per function. Assertions are a defensive programming technique used to catch programming or logical errors. When the condition in the assertion statement evaluates to false, the program will typically halt execution and generate an error message. This aids in detecting and diagnosing bugs during development and testing. 167 | 168 | **Without Rule Applied:** 169 | ```cpp 170 | int divide(int numerator, int denominator) { 171 | return numerator / denominator; // If denominator is zero, a divide by zero error will occur at runtime 172 | } 173 | 174 | ``` 175 | **With Rule Applied:** 176 | ```cpp 177 | #include 178 | 179 | int divide(int numerator, int denominator) { 180 | assert(denominator != 0 && "Denominator cannot be zero!"); // Runtime assertion 181 | int result = numerator / denominator; 182 | assert(result * denominator == numerator && "Division result incorrect!"); // Another runtime assertion 183 | return result; 184 | } 185 | 186 | ``` 187 | In the updated example, we have two assertions to ensure that the denominator isn't zero (which would cause a divide-by-zero error) and to verify the correctness of the division operation. If any of these assertions fail, the program will halt, allowing developers to identify and correct the issues. 188 | 189 | ------------------ 190 | 191 | 6. Restrict the scope of data to the smallest possible. 192 | 193 | ------------------ 194 | 195 | **Rule Explanation:** 196 | This rule suggests that the scope of data (variables, constants, objects) should be restricted as much as possible. This helps in maintaining encapsulation, preventing accidental modification from other parts of the code, and reducing potential bugs and complexity. 197 | 198 | **Without Rule Applied:** 199 | ```cpp 200 | int globalVar = 10; // Global variable 201 | 202 | void function1() { 203 | globalVar += 5; // Modifies global variable 204 | } 205 | 206 | void function2() { 207 | globalVar -= 2; // Modifies global variable 208 | } 209 | 210 | ``` 211 | **With Rule Applied:** 212 | ```cpp 213 | void function1() { 214 | int localVar1 = 10; // Local variable 215 | localVar1 += 5; // Modifies local variable 216 | } 217 | 218 | void function2() { 219 | int localVar2 = 10; // Local variable 220 | localVar2 -= 2; // Modifies local variable 221 | } 222 | 223 | ``` 224 | In the updated example, `globalVar` has been replaced with `localVar1` and `localVar2`, which are local to their respective functions. This prevents unintended side-effects from function calls and makes the code easier to understand and debug. 225 | 226 | ------------------ 227 | 228 | 7. Check the return value of all non-void functions, or cast to void to indicate the return value is useless. 229 | 230 | ------------------ 231 | 232 | **Rule Explanation:** 233 | This rule suggests that the return value of all non-void functions should be checked, or if it is useless, it should be cast to void. This helps prevent unnoticed errors when a function fails, and also makes the programmer's intentions clear when a return value is intentionally disregarded. 234 | 235 | **Without Rule Applied:** 236 | ```cpp 237 | double divide(int a, int b) { 238 | if(b == 0) return -1; // Returns -1 when error occurs 239 | return static_cast(a) / static_cast(b); 240 | } 241 | 242 | void someFunction() { 243 | divide(10, 0); // Return value not checked 244 | } 245 | 246 | ``` 247 | **With Rule Applied:** 248 | ```cpp 249 | double divide(int a, int b) { 250 | if(b == 0) return -1; // Returns -1 when error occurs 251 | return static_cast(a) / static_cast(b); 252 | } 253 | 254 | void someFunction() { 255 | double result = divide(10, 0); 256 | if(result == -1) { 257 | std::cerr << "Error: Division by zero.\n"; 258 | } 259 | // OR 260 | (void)divide(10, 0); // Explicitly cast to void to indicate that we don't care about the result 261 | } 262 | 263 | ``` 264 | In the updated example, the return value of the `divide` function is either checked for errors or explicitly cast to void, making the programmer's intentions clear and helping prevent unnoticed errors. 265 | 266 | ------------------ 267 | 268 | 8. Use the preprocessor sparingly. 269 | 270 | ------------------ 271 | 272 | **Rule Explanation:** 273 | This rule recommends using the preprocessor sparingly. The C++ preprocessor is a powerful tool that allows for macro substitution, conditional compilation, and inclusion of header files. However, misuse of the preprocessor can lead to code that is hard to understand and debug. Issues can arise due to unexpected macro expansion, name collisions, and difficulty in tracing the code. 274 | 275 | **Without Rule Applied:** 276 | ```cpp 277 | #define SQUARE(x) x * x 278 | 279 | void someFunction() { 280 | int a = 5; 281 | int b = SQUARE(a + 1); // This expands to a + 1 * a + 1 (which is 11, not 36 as expected due to operator precedence) 282 | } 283 | 284 | ``` 285 | **With Rule Applied:** 286 | ```cpp 287 | inline int square(int x) { 288 | return x * x; 289 | } 290 | 291 | void someFunction() { 292 | int a = 5; 293 | int b = square(a + 1); // This correctly computes the square of (a + 1) 294 | } 295 | 296 | ``` 297 | In the updated example, a function `square()` is used instead of a preprocessor macro. This makes the code easier to read, debug, and less prone to errors due to unexpected macro expansion. It also provides type safety which macros lack. 298 | 299 | ------------------ 300 | 301 | 9. Limit pointer use to a single dereference, and do not use function pointers. 302 | 303 | ------------------ 304 | 305 | **Rule Explanation:** 306 | This rule suggests limiting pointer use to a single dereference and avoiding function pointers. Multiple dereferences can make the code complex and harder to read, increasing the likelihood of errors. Function pointers add another layer of indirection and can make the code more difficult to follow, increasing the risk of programming mistakes. 307 | 308 | **Without Rule Applied:** 309 | ```cpp 310 | int a = 5; 311 | int* p = &a; 312 | int** pp = &p; 313 | 314 | **pp = 10; // Multiple dereferences 315 | 316 | void someFunction() { // Function pointer 317 | void (*fp)() = &someOtherFunction; 318 | (*fp)(); 319 | } 320 | 321 | ``` 322 | **With Rule Applied:** 323 | ```cpp 324 | int a = 5; 325 | int* p = &a; 326 | 327 | *p = 10; // Single dereference 328 | 329 | void someFunction() { 330 | someOtherFunction(); // Direct function call 331 | } 332 | 333 | ``` 334 | In the updated examples, only a single dereference is performed when using a pointer, and a direct function call is used instead of a function pointer, making the code clearer and easier to understand. 335 | 336 | ------------------ 337 | 338 | 10. Compile with all possible warnings active; all warnings should then be addressed before release of the software. 339 | 340 | ------------------ 341 | 342 | **Rule Explanation:** 343 | This rule suggests compiling code with all possible warnings enabled. Compiler warnings can often highlight potential issues in the code that might not necessarily prevent the code from compiling, but could lead to logical errors or undefined behavior during execution. Resolving these warnings before releasing the software will help ensure the software is robust and reliable. 344 | Most compilers allow you to specify the warning level. For instance, in GCC and Clang, `-Wall` enables all the standard warnings, and `-Wextra` enables some additional warnings. You can also use `-Werror` to treat all warnings as errors, which forces you to fix them since the code won't compile otherwise. 345 | 346 | **Without Rule Applied:** 347 | 348 | Compiling code with default settings might not reveal all potential issues. For example, the code might compile successfully but still contain logical errors: 349 | ```cpp 350 | int main() { 351 | int a; 352 | int b = a + 5; // Using uninitialized variable 'a' 353 | return 0; 354 | } 355 | 356 | ``` 357 | **With Rule Applied:** 358 | 359 | If you compile with all warnings enabled (for example, using `g++ -Wall -Wextra` in GCC), the compiler will warn about the uninitialized variable: 360 | ```cpp 361 | int main() { 362 | int a = 0; // Variable 'a' is now initialized 363 | int b = a + 5; 364 | return 0; 365 | } 366 | 367 | ``` 368 | In the updated example, the warning about the uninitialized variable 'a' is addressed by initializing 'a' before use. This prevents potential undefined behavior from occurring when the program runs. 369 | 370 | ------------------ 371 | 372 | --------------------------------------------------------------------------------