| Using this Standard. If you want to make a local copy of this standard and use it as your own you are perfectly free to do so. That's why I made it! If you find any errors or make any improvements please email me the changes so I can merge them in. Recent Changes. |
The use of the word "should" directs projects in tailoring a project-specific standard, in that the project must include, exclude, or tailor the requirement, as appropriate.
The use of the word "may" is similar to "should", in that it designates optional requirements.
"C++ Coding Standard" refers to this document whereas "C++ ANSI Standard" refers to the standard C++ language definition.
In any case, once finalized hopefully people will play the adult and understand that this standard is reasonable, and has been found reasonable by many other programmers, and therefore is worthy of being followed even with personal reservations.
Failing willing cooperation it can be made a requirement that this standard must be followed to pass a code inspection.
Failing that the only solution is a massive tickling party on the offending party.
+---------+
| START |
+---------+
|
V
YES +------------+ NO
+---------------| DOES THE |---------------+
| | DAMN THING | |
V | WORK? | V
+------------+ +------------+ +--------------+ NO
| DON'T FUCK | | DID YOU FUCK |-----+
| WITH IT | | WITH IT? | |
+------------+ +--------------+ |
| | |
| | YES |
| V |
| +------+ +-------------+ +---------------+ |
| | HIDE | NO | DOES ANYONE |<------| YOU DUMBSHIT! | |
| | IT |<----| KNOW? | +---------------+ |
| +------+ +-------------+ |
| | | |
| | V |
| | +-------------+ +-------------+ |
| | | YOU POOR | YES | WILL YOU | |
| | | BASTARD |<------| CATCH HELL? |<-----+
| | +-------------+ +-------------+
| | | |
| | | | NO
| | V V
| V +-------------+ +------------+
+-------------->| STOP |<------| SHITCAN IT |
+-------------+ +------------+
Leaders:
#1 " x = 1"
#2 " x != 1"
That's when a Project Leader is required. Unless you want to flip a coin.
Oh yea - one more thing. Project leaders: TAKE the blame when things go wrong and SHARE the credit when things go right.
Ain't easy - but it's the way I try to run my life.
A name is the result of a long deep thought process about the ecology it lives in. Only a programmer who understands the system as a whole can create a name that "fits" with the system. If the name is appropriate everything fits together naturally, relationships are clear, meaning is derivable, and reasoning from common human expectations works as expected.
If you find all your names could be Thing and DoIt then you should probably revisit your design.
Class Names
For example: RetryMax to mean the maximum number of retries, RetryCnt to mean the current retry count.
For example: IsHitRetryLimit.
Take for example NetworkABCKey. Notice how the C from ABC and K from key are confused. Some people don't mind this and others just hate it so you'll find different policies in different code so you never know what to call something.
class FluidOz // NOT FluidOZ class NetworkAbcKey // NOT NetworkABCKey
class NameOneTwo class Name
class JjLinkList
{
}
class NameOneTwo
{
public:
int DoIt();
void HandleError();
}
class NameOneTwo
{
public:
int VarAbc();
int ErrorNumber();
private:
int mVarAbc;
int mErrorNumber;
String* mpName;
}
class NameOneTwo
{
public:
int StartYourEngines(
Engine& rSomeEngine,
Engine& rAnotherEngine);
}
int
NameOneTwo::HandleError(int errorNumber)
{
int error= OsErr();
Time time_of_error;
ErrorProcessor error_processor;
}
String* pName= new String; String* pName, name, address; // note, only pName is a pointer.
class Test
{
public:
void DoSomething(StatusInfo& rStatus);
StatusInfo& rStatus();
const StatusInfo& Status() const;
private:
StatusInfo& mrStatus;
}
Logger gLog;
Logger* gpLog;
const int A_GLOBAL_CONSTANT= 5;
class Test
{
public:
private:
static StatusInfo msStatus;
}
typedef uint16 ModuleType; typedef uint32 SystemType;
Some subtle errors can occur when macro names and enum labels use the same name.
#define MAX(a,b) blah #define IS_ERR(err) blah
int
some_bloody_function()
{
}
enum PinStateType
{
PIN_OFF,
PIN_ON
}
enum PinStateType If PIN was not prepended a conflict
{ would occur as OFF and ON are probably
PIN_OFF, already defined.
PIN_ON
};
enum { STATE_ERR, STATE_OPEN, STATE_RUNNING, STATE_DYING};
If your class needs a constructor, make sure to provide one. You need one if during the operation of the class it creates something or does something that needs to be undone when the object dies. This includes creating memory, opening file descriptors, opening transactions etc.
If the default constructor is sufficient add a comment indicating that the compiler-generated version will be used.
If your default constructor has one or more optional arguments, add a comment indicating that it still functions as the default constructor.
If your class is intended to be derived from by other classes then make the destructor virtual.
If your class is copyable, either define a copy constructor and assignment operator or add a comment indicating that the compiler-generated versions will be used.
If your class objects should not be copied, make the copy constructor and assignment operator private and don't define bodies for them. If you don't know whether the class objects should be copyable, then assume not unless and until the copy operations are needed.
If your class is assignable, either define a assignment operator or add a comment indicating that the compiler-generated versions will be used.
If your objects should not be assigned, make the assignment operator private and don't define bodies for them. If you don't know whether the class objects should be assignable, then assume not.
class Planet
{
public:
// The following is the default constructor if
// no arguments are supplied:
//
Planet(int radius= 5);
// Use compiler-generated copy constructor, assignment, and destructor.
// Planet(const Planet&);
// Planet& operator=(const Planet&);
// ~Planet();
};
if (condition) while (condition)
{ {
... ...
} }
if (condition) { while (condition) {
... ...
} }
There are more reasons than psychological for preferring the first style. If you use an editor (such as vi) that supports brace matching, the first is a much better style. Why? Let's say you have a large block of code and want to know where the block ends. You move to the first brace hit a key and the editor finds the matching brace. Example:
if (very_long_condition && second_very_long_condition)
{
...
}
else if (...)
{
..
}
To move from block to block you just need to use cursor down and your
brace matching key. No need to move to the end of the line to match a brace
then jerk back and forth.
void
func()
{
if (something bad)
{
if (another thing bad)
{
while (more input)
{
}
}
}
}
if (condition)
{
}
while (condition)
{
}
strcpy(s, s1);
return 1;
/**
* A one line description of the class.
*
* #include "XX.h" <BR>
* -llib
*
* A longer description.
*
* @see something
*/
#ifndef XX_h
#define XX_h
// SYSTEM INCLUDES
//
// PROJECT INCLUDES
//
// LOCAL INCLUDES
//
// FORWARD REFERENCES
//
class XX
{
public:
// LIFECYCLE
/**
* Default constructor.
*/
XX(void);
/**
* Copy constructor.
*
* @param from The value to copy to this object.
*/
XX(const XX& from);
/**
* Destructor.
*/
~XX(void);
// OPERATORS
/**
* Assignment operator.
*
* @param from THe value to assign to this object.
*
* @return A reference to this object.
*/
XX& operator=(XX& from);
// OPERATIONS
// ACCESS
// INQUIRY
protected:
private:
};
// INLINE METHODS
//
// EXTERNAL REFERENCES
//
#endif // _XX_h_
/** * Assignment operator. * * @param val The value to assign to this object. * * @return A reference to this object. */ XX& operator=(XX& val);
/** * Copy one string to another. * * PRECONDITION
* REQUIRE(from != 0) * REQUIRE(to != 0) * * WARNING
* The to buffer must be long enough to hold * the entire from buffer. * * EXAMPLES
** strcpy(somebuf, "test") ** * @param from The string to copy. * @param to The buffer to copy the string to. * * @return void */ void strcpy(const char* from, char* to);
int AnyMethod(
int arg1,
int arg2,
int arg3,
int arg4);
#ifndef XX_h #define XX_h // SYSTEM INCLUDES // #includeNotice how the comments for include statements align on the third X.// standard IO interface #include // HASA string interface
{
// Block1 (meaningful comment about Block1)
... some code
{
// Block2 (meaningful comment about Block2)
... some code
} // End Block2
} // End Block1
This may make block matching much easier to spot when you don't have an
intelligent editor.
Create an Open() method for an object which completes construction. Open() should be called after object instantiation.
class Device
{
public:
Device() { /* initialize and other stuff */ }
int Open() { return FAIL; }
};
Device dev;
if (FAIL == dev.Open()) exit(1);
The logic might look like:
Sql::~Sql()
{
delete connection;
delete buffer;
}
Let's say an exception is thrown while deleting the database connection. Will the buffer be deleted? No. Exceptions are basically non-local gotos with stack cleanup. The code for deleting the buffer will never be executed creating a gaping resource leak.
Special care must be taken to catch exceptions which may occur during object destruction. Special care must also be taken to fully destruct an object when it throws an exception.
#include "XX.h" // class implemented
/////////////////////////////// PUBLIC ///////////////////////////////////////
//============================= LIFECYCLE ====================================
XX::XX()
{
}// XX
XX::XX(const XX&)
{
}// XX
XX::~XX()
{
}// ~XX
//============================= OPERATORS ====================================
XX&
XX::operator=(XX&);
{
return *this;
}// =
//============================= OPERATIONS ===================================
//============================= ACESS ===================================
//============================= INQUIRY ===================================
/////////////////////////////// PROTECTED ///////////////////////////////////
/////////////////////////////// PRIVATE ///////////////////////////////////
The moral is make your functions reentrant by not using static variables in a function. Besides, every machine has 128MB of RAM now so we don't worry about buffer space any more :-)
In general enums are preferred to #define as enums are understood by the debugger.
Be aware enums are not of a guaranteed size. So if you have a type that can take a known range of values and it is transported in a message you can't use an enum as the type. Use the correct integer size and use constants or #define. Casting between integers and enums is very error prone as you could cast a value not in the enum.
class Variables
{
public:
static const int A_VARIABLE;
static const int B_VARIABLE;
static const int C_VARIABLE;
}
#ifndef ClassName_h #define ClassName_h #endif // ClassName_hReplace ClassName with the name of the class contained in the file. Use the exact class name. Some standards say use all upper case. This is a mistake because someone could actually name a class the same as yours but using all upper letters. If the files end up be included together one file will prevent the other from being included and you will be one very confused puppy. It has happened!
Most standards put a leading _ and trailing _. This is no longer valid as the C++ standard reserves leading _ to compiler writers.
When the include file is not for a class then the file name should be used as the guard name.
if (condition) // Comment
{
}
else if (condition) // Comment
{
}
else // Comment
{
}
If you have else if statements then it is usually a good idea to
always have an else block for finding unhandled cases. Maybe put a log message
in the else even if there is no corrective action taken.
if ( 6 == errorNum ) ...
One reason is that if you leave out one of the = signs, the compiler will find the error for you. A second reason is that it puts the value you are looking for right up front where you can find it instead of buried at the end of your expression. It takes a little time to get used to this format, but then it really gets useful.
switch (...)
{
case 1:
...
// FALL THROUGH
case 2:
{
int v;
...
}
break;
default:
}
for (...)
{
while (...)
{
...
if (disaster)
goto error;
}
}
...
error:
clean up the mess
When a goto is necessary the accompanying label should be alone on a line and to the left of the code that follows. The goto should be commented (possibly in the block header) as to its utility and purpose.
Continue and break like goto should be used sparingly as they are magic in code. With a simple spell the reader is beamed to god knows where for some usually undocumented reason.
The two main problems with continue are:
Consider the following example where both problems occur:
while (TRUE)
{
...
// A lot of code
...
if (/* some condition */) {
continue;
}
...
// A lot of code
...
if ( i++ > STOP_VALUE) break;
}
Note: "A lot of code" is necessary in order that the problem cannot be
caught easily by the programmer.
From the above example, a further rule may be given: Mixing continue with break in the same loop is a sure way to disaster.
(condition) ? funct1() : func2();
or
(condition)
? long statement
: another long statement;
DWORD mDword DWORD* mpDword char* mpChar char mChar mDword = 0; mpDword = NULL; mpChar = 0; mChar = NULL;
#define MAX(x,y) (((x) > (y) ? (x) : (y)) // Get the maximum
The macro above can be replaced for integers with the following inline function with no loss of efficiency:
inline int
max(int x, int y)
{
return (x > y ? x : y);
}
MAX(f(x),z++);
It's better to define one method, Init(), that initializes all possible attributes. Init() should be called first from every constructor.
class Test
{
public:
Test()
{
Init(); // Call to common object initializer
}
Test(int val)
{
Init(); // Call to common object initializer
mVal= val;
}
private:
int mVal;
String* mpName;
void Init()
{
mVal = 0;
mpName= 0;
}
}
Since the number of member variables is small, this might be better
written as:
class Test
{
public:
Test(int val = 0, String* name = 0)
: mVal(val), mpName(name) {}
private:
int mVal;
String* mpName;
};
while (*dest++ = *src++)
; // VOID
if (FAIL != f())is better than
if (f())even though FAIL may have the value 0 which C considers to be false. An explicit test will help you out later when somebody decides that a failure return should be -1 instead of 0. Explicit comparison should be used even if the comparison value will never change; e.g., if (!(bufsize % sizeof(int))) should be written instead as if ((bufsize % sizeof(int)) == 0) to reflect the numeric (not boolean) nature of the test. A frequent trouble spot is using strcmp to test for string equality, where the result should never ever be defaulted. The preferred approach is to define a macro STREQ.
#define STREQ(a, b) (strcmp((a), (b)) == 0)
Or better yet use an inline method:
inline bool
StringEqual(char* a, char* b)
{
(strcmp(a, b) == 0) ? return true : return false;
Or more compactly:
return strcmp(a, b) == 0;
}
Note, this is just an example, you should really use the standard library string type for doing the comparison.
The non-zero test is often defaulted for predicates and other functions or expressions which meet the following restrictions:
The form of boolean most accurately matching the new standard is:
typedef int bool; #define TRUE 1 #define FALSE 0 or const int TRUE = 1; const int FALSE = 0;Note, the standard defines the names true and false not TRUE and FALSE. The all caps versions are used to not clash if the standard versions are available.
Even with these declarations, do not check a boolean value for equality with 1 (TRUE, YES, etc.); instead test for inequality with 0 (FALSE, NO, etc.). Most functions are guaranteed to return 0 if false, but only non-zero if true. Thus,
if (TRUE == func()) { ...
must be written
if (FALSE != func()) { ...
while (EOF != (c = getchar()))
{
process the character
}
The ++ and -- operators count as assignment statements. So, for many purposes, do functions with side effects. Using embedded assignment statements to improve run-time performance is also possible. However, one should consider the tradeoff between increased speed and decreased maintainability that results when embedded assignments are used in artificial places. For example,
a = b + c; d = a + r;should not be replaced by
d = (a = b + c) + r;even though the latter may save one cycle. In the long run the time difference between the two will decrease as the optimizer gains maturity, while the difference in ease of maintenance will increase as the human memory of what's going on in the latter piece of code begins to fade.
Developing a common framework takes a lot of up front design effort. When this effort is not made, for whatever reasons, there are several techniques one can use to encourage reuse:
If you need a piece of code email to the group asking if someone has already done it. The results can be surprising.
In most large groups individuals have no idea what other people are doing. You may even find someone is looking for something to do and will volunteer to do the code for you. There's always a gold mine out there if people work together.
One reason for this is because people don't like making small libraries. There's something about small libraries that doesn't feel right. Get over it. The computer doesn't care how many libraries you have.
If you have code that can be reused and can't be placed in an existing library then make a new library. Libraries don't stay small for long if people are really thinking about reuse.
If you are afraid of having to update makefiles when libraries are recomposed or added then don't include libraries in your makefiles, include the idea of services. Base level makefiles define services that are each composed of a set of libraries. Higher level makefiles specify the services they want. When the libraries for a service change only the lower level makefiles will have to change.
In an ideal world a programmer could go to a web page, browse or search a list of packaged libraries, taking what they need. If you can set up such a system where programmers voluntarily maintain such a system, great. If you have a librarian in charge of detecting reusability, even better.
Another approach is to automatically generate a repository from the source code. This is done by using common class, method, library, and subsystem headers that can double as man pages and repository entries.
These headers are structured in such a way as they can be parsed and extracted. They are not useless like normal headers. So take time to fill them out. If you do it right once no more documentation may be necessary. See Class Layout for more information.
// :TODO: tmh 960810: possible performance problem // We should really use a hash table here but for now we'll // use a linear search. // :KLUDGE: tmh 960810: possible unsafe type cast // We need a cast here to recover the derived type. It should // probably use a virtual method or template.
class SunWorkstation
{
public:
void UpVolume(int amount) { mSound.Up(amount); }
SoundCard mSound;
private:
GraphicsCard mGraphics;
}
SunWorksation sun;
Do : sun.UpVolume(1);
Don't: sun.mSound.Up(1);
An ABC is an abstraction of a common form such that it can be used to build more specific forms. An ABC is a common interface that is reusable across a broad range of similar classes. By specifying a common interface as long as a class conforming to that interface is used it doesn't really matter what is the type of the derived type. This breaks code dependencies. New classes, conforming to the interface, can be substituted in at will without breaking code. In C++ interfaces are specified by using base classes with virtual methods.
The above is a bit rambling because it's a hard idea to convey. So let's use an example: We are doing a GUI where things jump around on the screen. One approach is to do something like:
class Frog
{
public:
void Jump();
}
class Bean
{
public:
void Jump();
}
The GUI folks could instantiate each object and call the Jump method of
each object. The Jump method of each object contains the implementation of
jumping behavior for that type of object. Obviously frogs and beans jump
differently even though both can jump.
Unfortunately the owner of Bean didn't like the word Jump so they changed the method name to Leap. This broke the code in the GUI and one whole week was lost.
Then someone wanted to see a horse jump so a Horse class was added:
class Horse
{
public:
void Jump();
}
The GUI people had to change their code again to add Horse.
Then someone updated Horse so that its Jump behavior was slightly different. Unfortunately this caused a total recompile of the GUI code and they were pissed.
Someone got the bright idea of trying to remove all the above dependencies using abstract base classes. They made one base class that specified an interface for jumping things:
class Jumpable
{
public:
virtual void Jump() = 0;
}
Jumpable is a base class because other classes need to derive from it so
they can get Jumpable's interface. It's an abstract base class because one or
more of its methods has the = 0 notation which means the method is a
pure virtual method. Pure virtual methods must be implemented by
derived classes. The compiler checks.
Not all methods in an ABC must be pure virtual, some may have an implementation. This is especially true when creating a base class encapsulating a process common to a lot of objects. For example, devices that must be opened, diagnostics run, booted, executed, and then closed on a certain event may create an ABC called Device that has a method called LifeCycle which calls all other methods in turn thus running through all phases of a device's life. Each device phase would have a pure virtual method in the base class requiring implementation by more specific devices. This way the process of using a device is made common but the specifics of a device are hidden behind a common interface.
Back to Jumpable. All the classes were changed to derive from Jumpable:
class Frog : public Jumpable
{
public:
virtual void Jump() { ... }
}
etc ...
We see an immediate benefit: we know all classes derived from Jumpable
must have a Jump method. No one can go changing the name to Leap without
the compiler complaining. One dependency broken.
Another benefit is that we can pass Jumpable objects to the GUI, not specific objects like Horse or Frog:
class Gui
{
public:
void MakeJump(Jumpable*);
}
Gui gui;
Frog* pFrog= new Frog;
gui.MakeJump(pFrog);
Notice Gui doesn't even know it's making a frog jump, it just has a
jumpable thing, that's all it cares about. When Gui calls the Jump method it
will get the implementation for Frog's Jump method. Another dependency down. Gui
doesn't have to know what kind of objects are jumping.
We also removed the recompile dependency. Because Gui doesn't contain any Frog objects it will not be recompiled when Frog changes.
Any project brings together people of widely varying skills, knowledge, and experience. Even if everyone on a project is a genius you will still fail because people will endlessly talk past each other because there is no common language and processes binding the project together. All you'll get is massive fights, burnout, and little progress. If you send your group to training they may not come back seasoned experts but at least your group will all be on the same page; a team.
There are many popular methodologies out there. The point is to do some research, pick a method, train your people on it, and use it. Take a look at the top of this page for links to various methodologies.
You may find the CRC (class responsibility cards) approach to teasing out a design useful. Many others have. It is an informal approach encouraging team cooperation and focusing on objects doing things rather than objects having attributes. There's even a whole book on it: Using CRC Cards by Nancy M. Wilkinson.
An individual use case may have a name (although it is typically not a simple name). Its meaning is often written as an informal text description of the external actors and the sequences of events between objects that make up the transaction. Use cases can include other use cases as part of their behaviour.
Have as many use cases as needed to describe what a system needs to accomplish.
My guess is UML will win out for marketing reasons. But it is good to have some competition going.
All classes derived from a base class should be interchangeable when used as a base class.The idea is users of a class should be able to count on similar behavior from all classes that derive from a base class. No special code should be necessary to qualify an object before using it. If you think about it violating LSP is also violating the Open/Closed principle because the code would have to be modified every time a derived class was added. It's also related to dependency management using abstract base classes.
For example, if the Jump method of a Frog object implementing the Jumpable interface actually makes a call and orders pizza we can say its implementation is not in the spirit of Jump and probably all other objects implementing Jump. Before calling a Jump method a programmer would now have to check for the Frog type so it wouldn't screw up the system. We don't want this in programs. We want to use base classes and feel comfortable we will get consistent behaviour.
LSP is a very restrictive idea. It constrains implementors quite a bit. In general people support LSP and have LSP as a goal.
In practice the Open/Closed principle simply means making good use of our old friends abstraction and polymorphism. Abstraction to factor out common processes and ideas. Inheritance to create an interface that must be adhered to by derived classes. In C++ we are talking about using abstract base classes . A lot.
The contract is enforced in languages like Eiffel by pre and post condition statements that are actually part of the language. In other languages a bit of faith is needed.
Design by contract when coupled with language based verification mechanisms is a very powerful idea. It makes programming more like assembling spec'd parts.
ClassName.h
ClassName.cc // or whatever the extension is: cpp, c++
ClassName_section.Csection is some name that identifies why the code is chunked together. The class name and section name are separated by '_'.
if (abool= bbool) { ... }
Does the programmer really mean assignment here? Often yes, but
usually no. The solution is to just not do it, an inverse Nike philosophy.
Instead use explicit tests and avoid assignment with an implicit test. The
recommended form is to do the assignment before doing the test:
abool= bbool;
if (abool) { ... }
For more information see Const Correctness in the C++ FAQ.
#ifdef DEBUG
temporary_debugger_break();
#endif
Someone else might compile the code with turned-of debug info like: cc -c lurker.cpp -DDEBUG=0Alway use #if, if you have to use the preprocessor. This works fine, and does the right thing, even if DEBUG is not defined at all (!)
#if DEBUG
temporary_debugger_break();
#endif
If you really need to test whether a symbol is defined or not, test it
with the defined() construct, which allows you to add more things later to the
conditional without editing text that's already in the program: #if !defined(USER_NAME) #define USER_NAME "john smith" #endif
void
example()
{
great looking code
#if 0
lots of code
#endif
more code
}
You can't use /**/ style comments because comments can't contain comments and surely a large block of your code will contain a comment, won't it?
Don't use #ifdef as someone can unknowingly trigger ifdefs from the compiler command line.
RDI separates producers and consumers on a distributed scale. Event producers and consumers don't have to know about each other at all. Consumers can drop out of the event stream by deregistering for events. New consumers can register for events at anytime. Event producers can drop out with no ill effect to event consumers, the consumer just won't get any more events. It is a good idea for producers to have an "I'm going down event" so consumers can react intelligently.
Logically the dispatch system is a central entity. The implementation however can be quite different. For a highly distributed system a truly centralized event dispatcher would be a performance bottleneck and a single point of failure. Think of event dispatchers as being a lot of different processes cast about on various machines for redundancy purposes. Event processors communicate amongst each other to distribute knowledge about event consumers and producers. Much like a routing protocol distributes routing information to its peers.
RDI works equally well in the small, in processes and single workstations. Parts of the system can register as event consumers and event producers making for a very flexible system. Complex decisions in a system are expressed as event registrations and deregistrations. No further level of cooperation required.
More expressive event filters can also be used. The above proposal filters events on some unique ID. Often you want events filtered on more complex criteria, much like a database query. For this to work the system has to understand all data formats. This is easy if you use a common format like attribute value pairs. Otherwise each filter needs code understanding packet formats. Compiling in filter code to each dispatcher is one approach. Creating a downloadable generic stack based filter language has been used with success on other projects, being both simple and efficient.
To see why ask yourself:
class X
{
public:
int GetAge() const { return mAge; }
void SetAge(int age) { mAge= age; }
private:
int mAge;
}
The problem with Get/Set is twofold:
class X
{
public:
int Age() const { return mAge; }
void Age(int age) { mAge= age; }
private:
int mAge;
}
Similar to Get/Set but cleaner. Use this approach when not using the
Attributes as Objects approach.
class X
{
public:
int Age() const { return mAge; }
int& rAge() { return mAge; }
const String& Name() const { return mName; }
String& rName() { return mName; }
private:
int mAge;
String mName;
}
The above two attribute examples shows the strength and weakness of the
Attributes as Objects approach.
When using an int type, which is not a real object, the int is set directly because rAge() returns a reference. The object can do no checking of the value or do any representation reformatting. For many simple attributes, however, these are not horrible restrictions. A way around this problem is to use a class wrapper around base types like int.
When an object is returned as reference its = operator is invoked to complete the assignment. For example:
X x; x.rName()= "test";This approach is also more consistent with the object philosophy: the object should do it. An object's = operator can do all the checks for the assignment and it's done once in one place, in the object, where it belongs. It's also clean from a name perspective.
When possible use this approach to attribute access.
A layering violation simply means we have dependency between layers that is not controlled by a well defined interface. When one of the layers changes code could break. We don't want code to break so we want layers to work only with other adjacent layers.
Sometimes we need to jump layers for performance reasons. This is fine, but we should know we are doing it and document appropriately.
Delegation is an alternative to using inheritance for implementation purposes. One can use inheritance to define an interface and delegation to implement the interface.
Some people feel delegation is a more robust form of OO than using implementation inheritance. Delegation encourages the formation of abstract class interfaces and HASA relationships. Both of which encourage reuse and dependency breaking.
class TestTaker
{
public:
void WriteDownAnswer() { mPaidTestTaker.WriteDownAnswer(); }
private:
PaidTestTaker mPaidTestTaker;
}
In this example a test taker delegates actually answering the question to
a paid test taker. Not ethical but a definite example of delegation!
My god he's questioning code reviews, he's not an engineer!
Not really, it's the form of code reviews and how they fit into normally late chaotic projects is what is being questioned.
First, code reviews are way too late to do much of anything useful. What needs reviewing are requirements and design. This is where you will get more bang for the buck.
Get all relevant people in a room. Lock them in. Go over the class design and requirements until the former is good and the latter is being met. Having all the relevant people in the room makes this process a deep fruitful one as questions can be immediately answered and issues immediately explored. Usually only a couple of such meetings are necessary.
If the above process is done well coding will take care of itself. If you find problems in the code review the best you can usually do is a rewrite after someone has sunk a ton of time and effort into making the code "work."
You will still want to do a code review, just do it offline. Have a couple people you trust read the code in question and simply make comments to the programmer. Then the programmer and reviewers can discuss issues and work them out. Email and quick pointed discussions work well. This approach meets the goals and doesn't take the time of 6 people to do it.
Some issues to keep in mind:
Make a web page or document or whatever. New programmers shouldn't have to go around begging for build secrets from the old timers.
Programmers generally resist bug tracking, yet when used correctly it can really help a project:
FYI, it's not a good idea to reward people by the number of bugs they fix :-)
Source code control should be linked to the bug tracking system. During the part of a project where source is frozen before a release only checkins accompanied by a valid bug ID should be accepted. And when code is changed to fix a bug the bug ID should be included in the checkin comments.
Face it, if you don't own a piece of code you can't possibly be in a position to change it. There's too much context. Assumptions seemingly reasonable to you may be totally wrong. If you need a change simply ask the responsible person to change it. Or ask them if it is OK to make such-n-such a change. If they say OK then go ahead, otherwise holster your editor.
Every rule has exceptions. If it's 3 in the morning and you need to make a change to make a deliverable then you have to do it. If someone is on vacation and no one has been assigned their module then you have to do it. If you make changes in other people's code try and use the same style they have adopted.
Programmers need to mark with comments code that is particularly sensitive to change. If code in one area requires changes to code in an another area then say so. If changing data formats will cause conflicts with persistent stores or remote message sending then say so. If you are trying to minimize memory usage or achieve some other end then say so. Not everyone is as brilliant as you.
The worst sin is to flit through the system changing bits of code to match your coding style. If someone isn't coding to the standards then ask them or ask your manager to ask them to code to the standards. Use common courtesy.
Code with common responsibility should be treated with care. Resist making radical changes as the conflicts will be hard to resolve. Put comments in the file on how the file should be extended so everyone will follow the same rules. Try and use a common structure in all common files so people don't have to guess on where to find things and how to make changes. Checkin changes as soon as possible so conflicts don't build up.
As an aside, module responsibilities must also be assigned for bug tracking purposes.
Process automation also frees up developers to do real work because they don't have to babysit builds and other project time sinks.
This is the best way to maintain a clean build. Make sure the list of all errors for a build is available for everyone to see so everyone can see everyone elses errors. The goal is replace a blaim culture with a culture that tries to get things right and fixes them when they are wrong. Immediate feedback makes this possible.
This feature like the automated error assignment makes problems immediately visible and immediately correctable, all without a lot of blame and shame.
For some reason an odd split occurred in early C++ compilers around what C++ source files should be called. C header files always use the .h and C source files always use the .c extension. What should we use for C++?
The short answer is as long as everyone on your project agrees it doesn't really matter. The build environment should be able to invoke the right compiler for any extension. Historically speaking here have been the options:
/* * aheader.h */ int x = 0;
extern "C" int strncpy(...);
extern "C" int my_great_function();
extern "C"
{
int strncpy(...);
int my_great_function();
};
extern "C" void
a_c_function_in_cplusplus(int a)
{
}
#ifdef __cplusplus extern "C" some_function(); #else extern some_function(); #endif
if (22 == foo) { start_thermo_nuclear_war(); }
else if (19 == foo) { refund_lotso_money(); }
else if (16 == foo) { infinite_loop(); }
else { cry_cause_im_lost(); }
In the above example what do 22 and 19 mean? If there was a number change
or the numbers were just plain wrong how would you know? Instead of magic numbers use a real name that means something. You can use #define or constants or enums as names. Which one is a design choice. For example:
#define PRESIDENT_WENT_CRAZY (22)
const int WE_GOOFED= 19;
enum
{
THEY_DIDNT_PAY= 16
};
if (PRESIDENT_WENT_CRAZY == foo) { start_thermo_nuclear_war(); }
else if (WE_GOOFED == foo) { refund_lotso_money(); }
else if (THEY_DIDNT_PAY == foo) { infinite_loop(); }
else { happy_days_i_know_why_im_here(); }
Now isn't that better?
Robert Martin put OO in perspective:
If people don't know C++ and OO then they will likely fail and blame their
tools. A good craftsperson doesn't blame their tools. Get training. Hire at
least one experienced person as guide/mentor.
The streams library is large and slow. You are better off making a "fake"
streams library by overloading the << operator. If you have a lot of
memory then use streams, they are convenient and useful.
Code using templates can suffer from extreme code bloat. This is pretty
much a function of your compiler as templates can be efficiently used when
done correctly. Test your compiler for it how handles templates. If it doesn't
make a copy per file for each template then you are in business. Templates
have good time efficiency so they would be nice to use.
You can fix the template code bloat problem by using explicit
instantiation. Actually, even if the compiler generates one copy per source
file. This, however, is often too much programmer work to expect on a large
project, so be careful. Many linkers are smart enough to strip away all but
one of the copies.
Another issue to consider is template complexity. Templates can be complex
for those new to C++. Bugs in templates are very hard to find and may
overwhelm the patience of users.
Embedded applications are usually interrupt driven and multi-threaded. Test
that exceptions are thread safe. Many compilers support exceptions, but not
thread safe exceptions. And you probably don't want to call code in an
interrupt that throws exceptions.
When you think through your design and come up with good abstractions you
will be shocked at how little code and how little time it takes to implement
new features.
Don't use your embedded OSs features directly. Create a layer that
encapsulates OS functions and use those encapsulations. Most feature like
tasks, interrupts, semaphores, message queues, messages, etc. are common to
all systems. With good encapsulations it's quite possible to have the same
code compile for Solaris, VxWorks, Windows, and other systems. It just takes a
little thought.
A lot of systems create a ROM and download code later over the network that
is linked against the ROM. Something to remember is linkers will try and
include only code that is used. So your ROM may not contain code that loaded
code expects to be there. You need to include all functions in your ROM.
Most embedded systems have a command line interface which usually requires
C linkage, then they may have an SNMP interface, and they may have some sort
of other friendly interface. Design this up front to be common across all
code. It will make your life much easier. C functions require access to global
pointers so they can use objects. The singleton pattern makes this easier.
Come up with common naming conventions. A decent one is:
Make your debug and error system first so everyone writing code will use
it. It's very hard to retrofit code with debug output and intelligent use of
error codes. If you have some way to write system assert errors to NVRAM,
disk, or some other form of persistent storage so you can recover it on the
next reboot.
Think how you'll share memory buffers between ISR code and task level code.
Think how fast your default memory allocator is, it is probably slow. Think if
your processor supports purify! Think how you'll track memory corruption and
leakage.
You need to design up front how you are going to handle watchdog functions
and test that the system is still running and not corrupted.
When using memory mapped I/O make sure that you declare the input port
variables as volatile, (some compilers do this automatically), since the value
can change without notice, and the optimizer could eliminate what looks like a
redundant access to that variable. Not using volatile leads to some very
obscure bugs. If you suspect problems in this area take a look at the
generated code to make sure read-only assumptions are being made.
Sometimes the keyword volatile is ifdef'd out for portability reasons.
Check that what you think is volatile is really declared as volatile.
Promise of OO
OO has been hyped to the extent you'd figure it would
solve world hunger and usher in a new era of world peace. Not! OO is an
approach, a philosophy, it's not a recipe which blindly followed yields quality.
You can't use OO and C++ on Embedded Systems
Oh yes you can. I've used
C++ on several embedded systems as have many others. And if you can't why not?
Please don't give in to vague feelings and prejudice. An attitude best shown
with a short exchange: Rube: Our packet driver is slow. We're only getting 100 packets per second.
Me : Good thing you didn't do it in C++ huh?
Rube: Oh yah, it would have been really slow then!
Me : (smiled secretly to myself)
My initial response was prompted by a general unacceptance of C++ in the
project and blaming C++ for all problems. Of course all the parts written in C
and assembly had no problems :-) Embedded systems shops tend to be hardware
driven companies and tend not to know much about software development, thus any
new fangled concepts like OO and C++ are ridiculed without verbally accessible
reasons. Counter arguments like code that is fast and small and reusable don't
make a dent. Examples like improving the speed of a driver by inlining certain
methods and not hacking the code to death gently roll into the bit bucket.
Techniques
Of course C++ can be a disaster for an embedded system when
used incorrectly, which of course is true of any tool. Here's some ideas to use
C++ safely in an embedded system:
The two extremes are thin classes versus thick classes. Thin classes are minimalist classes. Thin classes have as few methods as possible. The expectation is users will derive their own class from the thin class adding any needed methods.
While thin classes may seem "clean" they really aren't. You can't do much with a thin class. Its main purpose is setting up a type. Since thin classes have so little functionality many programmers in a project will create derived classes with everyone adding basically the same methods. This leads to code duplication and maintenance problems which is part of the reason we use objects in the first place. The obvious solution is to push methods up to the base class. Push enough methods up to the base class and you get thick classes.
Thick classes have a lot of methods. If you can think of it a thick class will have it. Why is this a problem? It may not be. If the methods are directly related to the class then there's no real problem with the class containing them. The problem is people get lazy and start adding methods to a class that are related to the class in some willow wispy way, but would be better factored out into another class. Judgment comes into play again.
Thick classes have other problems. As classes get larger they may become harder to understand. They also become harder to debug as interactions become less predictable. And when a method is changed that you don't use or care about your code will still have to be recompiled, possibly retested, and rereleased.
The real thing to remember when it comes to alignment is to put the biggest data members first, and smaller members later, and to pad with char[] so that the same structure would be used no matter whether the compiler was in "naturally aligned" or "packed" mode.
For the Mac there's no blanket "always on four byte boundaries" rule -- rather, the rule is "alignment is natural, but never bigger than 4 bytes, unless the member is a double and first in the struct in which case it is 8". And that rule was inherited from PowerOpen/AIX.
© Copyright 1995-1999. Todd Hoff. All rights reserved.