Tech It Yourself
tháng 4 22, 2020
0
General guidelines:
Here we will present the more general guidelines to be followed.
1 - Treat all warnings as compiling errors. Increase the warning level.
Treating warnings as errors can help catch many subtle bugs. The warning level should be raised (in Visual studio W4 should be adequate).
Please notice that it may be necessary to disable some warnings that are caused by external libraries.
2 - Keep functions short, when possible, to facilitate code review.
Straightforward. If the function is too long, consider breaking it into subfunctions.
3 - Use automated code quality tools (e.g.: Visual Studio)
This kind of tool also may help to catch bugs. There are a variety of tools available. If you are using Visual Studio, there is a
build in tool for code analysis in the menu [Analyze->Run Code Analysis]. After clicking this option for the first time, a second option will appear: [Analyze->Configure Code Analysis]. In this option, it is recommended to raise the analysis level to the maximum.
Please notice that it is likely that you will have to ignore warnings in external code. Also, sometimes the analysis may be wrong, so
attention is necessary.
4 - Add logs, data dumps, or visual representations of the system status, to aid finding and fixing more complex bugs.
Also straightforward. Having some textual or visual representation of the system state may greatly help to catch bugs.
5 - Add plenty of comments to the code at the same time you are writing it.
Straightforward. Just take care to update the comments if the source code changes. It is better to write the comments as you code because the meaning of the lines is fresh in one's memory. It would be harder and less precise to comment the code later.
6 - When possible, prefer simpler flow control methods (avoid goto, recursion, polymorphism, etc.).
Complex flow control methods add uncertainty and complexity to the source code. When there is recursion, it may be hard to calculate how many times the recursive function will be called. When there is polymorphism, it may be hard to determine which implementation of the function is being called at a given stage. Please notice that if using those complex flow control methods will make the code simpler, then they should be used.
7 - In a function/method, check the validity of the input, check if the output is valid and has the correct properties, check the collateral effects (when applicable).
This is a part of Design by Contract (DC) methodology: In a function, the correctness of the input should never be trusted,
and the correctness of the output be guaranteed. Of course, it may be not possible to check input/output with 100% certainty.
An example of code without DC:
template<typename T>
vector<T> random_vector_t(int size, T mean = T(0), T sigma = T(1.0)) {
static std::default_random_engine generator;
std::normal_distribution<T> distribution(mean, sigma);
vector<T> res(size);
for (int idx = 0; idx < size; idx++) {
res[idx] = distribution(generator);
}
return res;
}
In the code above, we do not know if the value sigma, the standard deviation, is valid (i.e.: positive, bigger than 0).
Also, the size of the output vector should not be zero. As a post-condition, the size of the output vector should be the same as the parameter size. We create pre and post condition macros to test the conditions.
//Verification by Design by Contract (Pre-condition)
#define PRECONDITION( x ) assert ( (x) )
//Verification by Design by Contract (Post-condition)
#define POSTCONDITION( x ) assert ( (x) )
template<typename T>
vector<T> random_vector_t(int size, T mean = T(0), T sigma = T(1.0)) {
PRECONDITION(size > 0);
PRECONDITION(sigma > 0);
static std::default_random_engine generator;
std::normal_distribution<T> distribution(mean, sigma);
vector<T> res(size);
for (int idx = 0; idx < size; idx++) {
res[idx] = distribution(generator);
}
POSTCONDITION((int)res.size() == size);
return res;
}
In the code above, both input and output conditions are tested.
8 - Know when to check for error using "assert" or error handling code.
Asserts can be disabled in the production code. This is useful because the error checking may have a performance impact.
Hence, asserts must be used to check programming errors. However, if the error may be caused by the user, this error should be handled in the production code as well. For example:
void foo(FILE * f){
assert(f != 0);
fprintf(f, "Hello world\n");
}
In the code above, since we assume that "f" will never be null when the source code is correct, we may use "assert(f != 0)".
However, in the code below:
void foo(string filename){
File* f = fopen(filename.c_str(), "r");
if(f == 0){
//report error and quit!!
}
fprintf(f, "Hello world\n");
}
Even if the code is correct, "f" may be null if the user passes a file with a wrong filename. This check should be in the production code.
-----------------------------------------------------------------------------------------------------------------------------------------------------------
Specific guidelines
These guidelines are specific good practices that should be followed whenever possible.
1 - Do not use assign operators in sub-expressions.
This may cause side effects hard to guess. Also, it may help avoid confusing = with ==
- Bad example:
int x;
if((x = foo()) == 100){...}
- Good example:
int x = foo();
if(x == 100){...}
2 - Floating-point expressions shall not be directly or indirectly tested for equality or inequality.
Some values that have a finite representation in decimal may have an infinite representation in binary. Also, the float operations inherently add error to the computations.
- Bad example:
float x = ...;
if( x == 100.0f){...}
- Good example:
float EPS = 1e-8f;//Small value
float x = ...;
if (fabs(x - 100.0f) < EPS){...} //Difference is smaller than the error EPS
3 - The statements forming the body of a loop or an if construct must always be enclosed by {}, even if it is a single statement.
Having no {} may cause the common mistake of adding a new statement to a loop while forgetting to add the {}.
- Bad example:
//Original code
while(x > 0)
x = foo(x);
//Error
while(x > 0)
float new_x = foo(x);
x = new_x;//This line is out of the loop!!
- Good example:
while(x > 0){
x = foo(x);
}
4 - All "if ... else if" constructs shall be terminated with an else clause.
This is done to make sure all possibilities are being covered. If the final "else" statement is empty, it should contain comments
explaining why it is so.
- Bad example:
if((x % 4) == 0){
...
}else if((x % 3) == 0){
...
}
- Good example:
if((x % 4) == 0){
...
}else if((x % 3) == 0){
...
}else{
//Final else; may be empty
}
5 - All non-empty switch-clause should be finished by a break statement. The final clause shall be a default clause.
Failing to add a break to the switch clause is generally an error. The rationale about always having a default clause is similar to the final else in the previous guideline.
- Bad example:
switch( x ){
case 0:
foo();//Error!
case 1:
case 2:
bar();//Cases 1 and 2 should have the same effect
break;
}//No default!
- Good example:
switch( x ){
case 0:
foo();
break;
case 1:
case 2:
bar();//Cases 1 and 2 should have the same effect
break;
default:
process_error();//Or some other action...
}
6 - A for loop shall contain a single loop counter which shall not have floating type.
A loop with multiple loop counters or with a float counter is commonly implemented as a "while".
- Bad example:
y = 0;
for(x = 0; x < y; x = y++){...}
7 - If the loop counter is not modified by -- or ++, then the loop condition shall be specified by <=, >=, <, or >.
If the loop is not incremented by units, the stop condition may not occur if == or != is used in the condition.
- Bad example:
for(int i = 1; i != 10; i += 2){} //Infinite loop
- Good example:
for(int i = 1; i < 10; i += 2){} //Eventually stops
8 - The loop counter shall not be modified within the condition or statement.
Modifying the counter may easily cause errors in the loop.
- Bad example:
void foo(int& x_p){ (*x)--;}
...
for(x = 10; foo(&x); ){
//...
}
9 - The loop counter shall be modified by one of: --, ++, -=n, or +=n. The value of n remains constant.
This makes proving the loop termination much easier.
- Bad example:
for(int x = 0; x < 100; x = bar(x)){...}
- Good example:
for(int x = 0; x < 100; x += 10){...}
10 - Loop control variables other than the loop counter should not be modified, with exception of boolean flags.
Same reason as above, with the exception of boolean flags, used to break the loop.
- Bad example:
float v;
for(int x = 0; x < 100 && v < 10; x++){
v = foo();
}
- Good example:
bool f = true;
for(int x = 0; x < 100 && flag; x++){
float v = foo();
f = (v < 10);
}
11 - If a function generates error information, this information shall be tested.
Straightforward. It greatly helps to find errors.
- Bad example:
bool foo(float &x){
bool error = false;
if(x < 0){
error = true;
}else{
x = sqrt(x);
}
return error;
}
...
float x = ...;
foo(x);
- Good example:
...
float x = ...;
bool error = foo(x);
if(error){
//Process error
}
Here we will present the more general guidelines to be followed.
1 - Treat all warnings as compiling errors. Increase the warning level.
Treating warnings as errors can help catch many subtle bugs. The warning level should be raised (in Visual studio W4 should be adequate).
Please notice that it may be necessary to disable some warnings that are caused by external libraries.
2 - Keep functions short, when possible, to facilitate code review.
Straightforward. If the function is too long, consider breaking it into subfunctions.
3 - Use automated code quality tools (e.g.: Visual Studio)
This kind of tool also may help to catch bugs. There are a variety of tools available. If you are using Visual Studio, there is a
build in tool for code analysis in the menu [Analyze->Run Code Analysis]. After clicking this option for the first time, a second option will appear: [Analyze->Configure Code Analysis]. In this option, it is recommended to raise the analysis level to the maximum.
Please notice that it is likely that you will have to ignore warnings in external code. Also, sometimes the analysis may be wrong, so
attention is necessary.
4 - Add logs, data dumps, or visual representations of the system status, to aid finding and fixing more complex bugs.
Also straightforward. Having some textual or visual representation of the system state may greatly help to catch bugs.
5 - Add plenty of comments to the code at the same time you are writing it.
Straightforward. Just take care to update the comments if the source code changes. It is better to write the comments as you code because the meaning of the lines is fresh in one's memory. It would be harder and less precise to comment the code later.
6 - When possible, prefer simpler flow control methods (avoid goto, recursion, polymorphism, etc.).
Complex flow control methods add uncertainty and complexity to the source code. When there is recursion, it may be hard to calculate how many times the recursive function will be called. When there is polymorphism, it may be hard to determine which implementation of the function is being called at a given stage. Please notice that if using those complex flow control methods will make the code simpler, then they should be used.
7 - In a function/method, check the validity of the input, check if the output is valid and has the correct properties, check the collateral effects (when applicable).
This is a part of Design by Contract (DC) methodology: In a function, the correctness of the input should never be trusted,
and the correctness of the output be guaranteed. Of course, it may be not possible to check input/output with 100% certainty.
An example of code without DC:
template<typename T>
vector<T> random_vector_t(int size, T mean = T(0), T sigma = T(1.0)) {
static std::default_random_engine generator;
std::normal_distribution<T> distribution(mean, sigma);
vector<T> res(size);
for (int idx = 0; idx < size; idx++) {
res[idx] = distribution(generator);
}
return res;
}
In the code above, we do not know if the value sigma, the standard deviation, is valid (i.e.: positive, bigger than 0).
Also, the size of the output vector should not be zero. As a post-condition, the size of the output vector should be the same as the parameter size. We create pre and post condition macros to test the conditions.
//Verification by Design by Contract (Pre-condition)
#define PRECONDITION( x ) assert ( (x) )
//Verification by Design by Contract (Post-condition)
#define POSTCONDITION( x ) assert ( (x) )
template<typename T>
vector<T> random_vector_t(int size, T mean = T(0), T sigma = T(1.0)) {
PRECONDITION(size > 0);
PRECONDITION(sigma > 0);
static std::default_random_engine generator;
std::normal_distribution<T> distribution(mean, sigma);
vector<T> res(size);
for (int idx = 0; idx < size; idx++) {
res[idx] = distribution(generator);
}
POSTCONDITION((int)res.size() == size);
return res;
}
In the code above, both input and output conditions are tested.
8 - Know when to check for error using "assert" or error handling code.
Asserts can be disabled in the production code. This is useful because the error checking may have a performance impact.
Hence, asserts must be used to check programming errors. However, if the error may be caused by the user, this error should be handled in the production code as well. For example:
void foo(FILE * f){
assert(f != 0);
fprintf(f, "Hello world\n");
}
In the code above, since we assume that "f" will never be null when the source code is correct, we may use "assert(f != 0)".
However, in the code below:
void foo(string filename){
File* f = fopen(filename.c_str(), "r");
if(f == 0){
//report error and quit!!
}
fprintf(f, "Hello world\n");
}
Even if the code is correct, "f" may be null if the user passes a file with a wrong filename. This check should be in the production code.
-----------------------------------------------------------------------------------------------------------------------------------------------------------
Specific guidelines
These guidelines are specific good practices that should be followed whenever possible.
1 - Do not use assign operators in sub-expressions.
This may cause side effects hard to guess. Also, it may help avoid confusing = with ==
- Bad example:
int x;
if((x = foo()) == 100){...}
- Good example:
int x = foo();
if(x == 100){...}
2 - Floating-point expressions shall not be directly or indirectly tested for equality or inequality.
Some values that have a finite representation in decimal may have an infinite representation in binary. Also, the float operations inherently add error to the computations.
- Bad example:
float x = ...;
if( x == 100.0f){...}
- Good example:
float EPS = 1e-8f;//Small value
float x = ...;
if (fabs(x - 100.0f) < EPS){...} //Difference is smaller than the error EPS
3 - The statements forming the body of a loop or an if construct must always be enclosed by {}, even if it is a single statement.
Having no {} may cause the common mistake of adding a new statement to a loop while forgetting to add the {}.
- Bad example:
//Original code
while(x > 0)
x = foo(x);
//Error
while(x > 0)
float new_x = foo(x);
x = new_x;//This line is out of the loop!!
- Good example:
while(x > 0){
x = foo(x);
}
4 - All "if ... else if" constructs shall be terminated with an else clause.
This is done to make sure all possibilities are being covered. If the final "else" statement is empty, it should contain comments
explaining why it is so.
- Bad example:
if((x % 4) == 0){
...
}else if((x % 3) == 0){
...
}
- Good example:
if((x % 4) == 0){
...
}else if((x % 3) == 0){
...
}else{
//Final else; may be empty
}
5 - All non-empty switch-clause should be finished by a break statement. The final clause shall be a default clause.
Failing to add a break to the switch clause is generally an error. The rationale about always having a default clause is similar to the final else in the previous guideline.
- Bad example:
switch( x ){
case 0:
foo();//Error!
case 1:
case 2:
bar();//Cases 1 and 2 should have the same effect
break;
}//No default!
- Good example:
switch( x ){
case 0:
foo();
break;
case 1:
case 2:
bar();//Cases 1 and 2 should have the same effect
break;
default:
process_error();//Or some other action...
}
6 - A for loop shall contain a single loop counter which shall not have floating type.
A loop with multiple loop counters or with a float counter is commonly implemented as a "while".
- Bad example:
y = 0;
for(x = 0; x < y; x = y++){...}
7 - If the loop counter is not modified by -- or ++, then the loop condition shall be specified by <=, >=, <, or >.
If the loop is not incremented by units, the stop condition may not occur if == or != is used in the condition.
- Bad example:
for(int i = 1; i != 10; i += 2){} //Infinite loop
- Good example:
for(int i = 1; i < 10; i += 2){} //Eventually stops
8 - The loop counter shall not be modified within the condition or statement.
Modifying the counter may easily cause errors in the loop.
- Bad example:
void foo(int& x_p){ (*x)--;}
...
for(x = 10; foo(&x); ){
//...
}
9 - The loop counter shall be modified by one of: --, ++, -=n, or +=n. The value of n remains constant.
This makes proving the loop termination much easier.
- Bad example:
for(int x = 0; x < 100; x = bar(x)){...}
- Good example:
for(int x = 0; x < 100; x += 10){...}
10 - Loop control variables other than the loop counter should not be modified, with exception of boolean flags.
Same reason as above, with the exception of boolean flags, used to break the loop.
- Bad example:
float v;
for(int x = 0; x < 100 && v < 10; x++){
v = foo();
}
- Good example:
bool f = true;
for(int x = 0; x < 100 && flag; x++){
float v = foo();
f = (v < 10);
}
11 - If a function generates error information, this information shall be tested.
Straightforward. It greatly helps to find errors.
- Bad example:
bool foo(float &x){
bool error = false;
if(x < 0){
error = true;
}else{
x = sqrt(x);
}
return error;
}
...
float x = ...;
foo(x);
- Good example:
...
float x = ...;
bool error = foo(x);
if(error){
//Process error
}