Programming tips
This page contains several (more or less) general programming tips and tricks useful in creation of any project. They are targeted at both beginners and advanced programmers.
Majority of these tips results from practical experience.
Page Contents
- 1 Use constants instead of “magic numbers”
- 2 Passing a complex type as a function parameter
- 3 Usage of the “auto” keyword
- 4 Inline methods
- 5 Saving intermediary results
- 6 Iterator optimization
- 7 Early exit on error (a.k.a. avoid nested code if possible)
- 8 Intentionally invalid variable values
- 9 Safer conditionals
- 10 Memory allocation
Use constants instead of “magic numbers”
If you need to use a fixed number value somewhere in the program, it is almost always better to define a constant for it.
Bad
int points[100]; for (int i = 0; i < 100; ++i) { ... }
Good
// In case of usage in a class, // consider making the constant a static attribute. const int NUMBER_OF_POINTS = 100; int points[NUMBER_OF_POINTS]; for (int i = 0; i < NUMBER_OF_POINTS; ++i) { ... }
Code that works with an array or anything else is often spread across multiple files, therefore the second approach has several advantages:
- smaller risk of typos
- increased clarity
- easier/faster changes at a later time
Passing a complex type as a function parameter
The following code segment works as expected but it is highly ineffective.
Bad
int getMaximum(std::vector<int> param) { ... }
When you call this function, the whole input vector will be duplicated and only its copy will be passed as a parameter. At the end of the scope, the copy will be deleted.
The main disadvantage is the fact that the function’s efficiency (and the amount of memory used to store the copy) depends on the size of the vector at run-time.
The recommended way is to use a pointer or a reference to the required type:
Good
int getMaximum(std::vector<int>& param) { ... }
Usage of the “auto” keyword
One of the additions to the new C++ standard, C++11, similar to the var
keyword in C#.
Allows the programmer to avoid complicated type definitions and let compiler automatically determine the variable type. It can only be used if the type of the variable can be deduced from the assigned value.
// Create a new iterator specifying its type std::vector<int>::iterator iter1 = numbers.begin(); // Create a new iterator using the auto keyword auto iter2 = numbers.begin(); // Usage in a for-cycle for (auto it = numbers.begin(); it != numbers.end() ++it) { // ... }
Note: Support of C++11 features among the compilers varies. For example, the auto
keyword is available in Microsoft Visual Studio 2010, GCC 4.4 and their later releases.
Inline methods
Many classes contain very short methods which are used to provide controlled access to the protected member attributes (get-set methods) or perform some other, basic operation.
The definition of these methods is often specified directly in the class header, e.g.:
class Mesh { private: std::vector<osg::Vec3> _vertices; ... public: const osg::Vec3& getVertex(int index) const { return _vertices[index]; } ... };
However, if we decide to cycle through all vertices and get their positions, the CPU will waste a very high amount of cycles managing the call stack and the execution might be (noticeably) inefficient. For this reason, it is better to use the inline
keyword. The keyword will recommend the compiler to insert its code directly to places where it would normally be called.
This approach is of course far faster, even though the final code might be longer due to the duplication. It is therefore better to use it in case of shorter methods.
inline const osg::Vec3& getVertex(int index) const { return _vertices[index]; }
Saving intermediary results
Example of a very bad code:
for (int a = 0; a < getPolygonCount(); a++) { glNormal3f(getVertex(getPolygon(a).v1).normal.x, getVertex(getPolygon(a).v1).normal.y, getVertex(getPolygon(a).v1).normal.z); glVertex3f(getVertex(getPolygon(a).v1).coordinates.x, getVertex(getPolygon(a).v1).coordinates.y, getVertex(getPolygon(a).v1).coordinates.z); glNormal3f(getVertex(getPolygon(a).v2).normal.x, getVertex(getPolygon(a).v2).normal.y, getVertex(getPolygon(a).v2).normal.z); glVertex3f(getVertex(getPolygon(a).v2).coordinates.x, getVertex(getPolygon(a).v2).coordinates.y, getVertex(getPolygon(a).v2).coordinates.z); glNormal3f(getVertex(getPolygon(a).v3).normal.x, getVertex(getPolygon(a).v3).normal.y, getVertex(getPolygon(a).v3).normal.z); glVertex3f(getVertex(getPolygon(a).v3).coordinates.x, getVertex(getPolygon(a).v3).coordinates.y, getVertex(getPolygon(a).v3).coordinates.z); }
Main problem of the code above is that the getPolygon()
function is called eighteen times with the same parameter and getVertex()
is called six times with the same parameter.
If we save the intermediary result to a temporary variable, we will end up with shorter and more efficient code which is far easier to maintain.
Example of a good code:
Polygon *p = NULL; Vertex *v = NULL; for (int a = 0; a < getPolygonCount(); a++) { p = &getPolygon(a); v = &getVertex(p->v1); glNormal3f(v->normal.x, v->normal.y, v->normal.z); glVertex3f(v->coordinates.x, v->coordinates.y, v->coordinates.z); v = &getVertex(p->v2); glNormal3f(v->normal.x, v->normal.y, v->normal.z); glVertex3f(v->coordinates.x, v->coordinates.y, v->coordinates.z); v = &getVertex(p->v3); glNormal3f(v->normal.x, v->normal.y, v->normal.z); glVertex3f(v->coordinates.x, v->coordinates.y, v->coordinates.z); }
We may further replace variables v1
, v2
and v3
by a fixed-sized array, which will produce even better code:
Polygon *p = NULL; Vertex *v = NULL; for (int a = 0; a < getPolygonCount(); a++) { p = &getPolygon(a); for (int b = 0; b < 3; b++) { v = &getVertex(p->v[b]); glNormal3f(v->normal.x, v->normal.y, v->normal.z); glVertex3f(v->coordinates.x, v->coordinates.y, v->coordinates.z); } }
It might seem that the code would be slowed down by the existence for-cycle, but it is not the case. The compiler will actually perform a loop unwinding optimization (thanks to the small and fixed number of cycles) so the cycle itself will not be present in the final code.
Iterator optimization
Slower variant
for (auto iter = vertices.begin(); iter != vertices.end(); iter++) { ... }
Faster variant
for (auto iter = vertices.begin(); iter != vertices.end(); ++iter) { ... }
In case of the post-increment operator, the value of the variable is increased by one but the operator is forced (due to the C++ specification) to return the previous value. Therefore a new temporary variable (copy of the iterator before the increment) has to be created, returned as a result – and immediately deleted since it’s not used.
On the other hand, the pre-increment operator increases the value of iterator and returns its value without a need to create a temporary variable – which yields a faster performance, especially in case of many elements.
Early exit on error (a.k.a. avoid nested code if possible)
This one is more of a recommendation – both snippets of code do the same computation. However, the first option results in a code which might be harder to comprehend and more prone to errors in case of later edits. There even might be some cases where the main code won’t fit on single screen due to heavy indentation.
It is therefore recommended to deal with any errors or invalid arguments as soon as possible and only put the code which performs the main function after all the necessary errors have been dealt with.
Before
void function() { if (expression_1) { if (expression_2) { // Main // block // of // code // which // might // be // quite // long } else { std::cout << "Error 2" << std::endl; } } else { std::cout << "Error 1" << std::endl; } }
After
void function() { if (!expression_1) { std::cout << "Error 1" << std::endl; return; } if (!expression_2) { std::cout << "Error 2" << std::endl; return; } // Main // block // of // code // which // might // be // quite // long }
Intentionally invalid variable values
If you have a variable which keeps a certain value, it is often useful to define a number that will represent an invalid value. This number is usually 0
, -1
, 0xFFFFFFFF
, MAXINT
or any other appropriate value (class std::numeric_limits might provide an inspiration) which is available in the given representation and the value itself either does not make sense or its occurrence is highly unlikely.
You may further store this value as a constant (e.g. as a constant member variable in a class)
#include <limits> void function() { const float INVALID_VALUE = std::numeric_limits<float>::infinity(); // Rest of the code // ... }
Of course, it is entirely possible to remember a “not filled”/”not initialized” state (e.g. bool isValid;
), but the invalid value makes the code management simpler and decreases risk of errors caused by a of code which mistakenly changes only one of the variables.
Safer conditionals
Usage of a comparison operator ==
in conditional expressions always bears a risk of a typo which might result in an undesired usage of an assignment operator =
instead. Assignment operator =
returns the value of a variable on the left side as a result, so the error might be easily overlooked.
// Conditional if (x == 3) { ... } // In case of a typo, 3 is assigned to x which is then returned as a result // => the expression is always evaluated as 'true'. if (x = 3) { ... }
However, we may utilize the fact that the operator ==
is commutative and swap the expressions. This will allow us to easily detect any typo on a syntactic level – 3
is an r-value which doesn’t have a memory location and therefore cannot be assigned to. The typo will be immediately detected by a compiler and result in a syntax error.
// Equal conditional if (3 == x) { ... } // 3 is an r-value and cannot be assigned to // => syntax error detected by a compiler if (3 = x) { ...}
Memory allocation
While working with OSG classes, you should always use smart pointers (template class osg::ref_ptr
).
If you allocate any memory which is not managed by smart pointers, you should always write new-delete in pairs. As soon as you allocate the memory, you should also write an appropriate block of code which will release it.
Moreover, bear in mind that the allocation process might fail (especially if you require a large amount of memory).
In case of a failure, operators new
and new []
throw an exception of type std::bad_alloc
which can be caught in a regular try-catch block.
If you don’t want to deal with exceptions, you may allocate the memory with an argument std::nothrow
and check the pointer afterwards.
// Option #1 - catching std::bad_alloc const unsigned int BUFFER_SIZE = 1000000; try { int * buffer = new int[BUFFER_SIZE * sizeof(int)]; } catch(const std::bad_alloc&) { std::cout << "Failed to allocate " << BUFFER_SIZE * sizeof(int) << " bytes." << std::endl; buffer = NULL; }
// Option #2 - using std::nothrow const unsigned int BUFFER_SIZE = 1000000; int * buffer = new (std::nothrow) int[BUFFER_SIZE * sizeof(int)]; if(!buffer) std::cout << "Failed to allocate " << BUFFER_SIZE * sizeof(int) << " bytes." << std::endl;