We have previously looked at how we can add comments to our code using //
and /* */
.
Whilst knowing how to comment is useful, there is a more nuanced topic, around knowing when to comment.
When other developers (or our future selves) want to get an idea what the classes can do, we're generally going to explore in one of two ways:
We can infer quite a lot in this way. The header files are going to list all the functions and their method signatures.
Intellisense is going to find our public variables and functions, and let us know what arguments they expect.
This is useful, but semetimes won't have all the information we might need. Lets look at this function, for example:
bool TakeDamage(float Percent);
What is the bool
that this function returns? And what is the Percent
parameter? Is it the percentage of the character's maximum health? Or current health? Or something else?
We can improve intellisense and our documentation further by adding comments to our header file. By adding a simple comment above the method signature, our code can be described in plain english.
/* Take a percentage of the character's maximum
* health as damage. Returns true if the damage
* was Lethal
*/
bool TakeDamage(float Percent){};
Most IDEs will also show this comment when we use our classes, or hover over calls to our function:
Comments can be even more powerful when we follow a specific pattern. If we format our comments in a structured style, other systems will be able to read and understand our comments at a programmatic level.
Take this style of commenting, for example:
/**
* Take damage based on the character's maximum health.
* @param Percent - The percentage of the character's maximum health to inflict as damage
* @return Whether or not the damage was lethal
*/
bool TakeDamage(float Percent);
If we now view our tooltip in our editor, it is likely to have been formatted in a more structured way.
This style of comment is called JavaDoc. It features many more properties than those demonstrated here. JavaDoc comments can include things like like:
And much more.
When we create comments in a specific manner, it can be read and understood not just by humans, but also by other tools. As we've seen, this includes our IDE, but there are many more use cases.
For example, Doxygen can use comments written in this style to automatically create documentation websites for our projects.
Sometimes we want to write comments above lines of code, but to not have those comments be interpreted as anything more than a simple comment.
Typically, a comment that uses three slashes - ///
- will not appear in our editor tooltips.
/// This comment will not appear in tooltips
int Number { 4 };
Another situation where we should consider adding comments is if our implementation is complex, or "unusual". If we think another developer will struggle to understand why a section of code was set up in a specif way, it can be helpful to add comments to it:
void TrackEvent(Event& Event) {
// This event type crashes our analytics service
// Temporarily ignoring it while we investigate
if (Event.Type == EventTypes::StartLevel) {
return;
}
Send(Event);
}
Another good place to add comments is to code that is simply too complex for most people to understand. An explanatory comment, often with a link to provide more information, can be very helpful.
// This is an implementation of the Fast Inverse Square Root algorithm
// See https://en.wikipedia.org/wiki/Fast_inverse_square_root
float InvSqrt(float number) {
const float y {
std::bit_cast<float>(
0x5f3759df - (std::bit_cast<std::uint32_t>(number) >> 1)
)
};
return y * (1.5f - (number * 0.5f * y * y));
}
Whilst comments are often useful, they can often be misused as a way to mitigate messy code.
Ideally, our code should require as few comments as possible. We should strive to make our code readable, rather than having messy code and explanatory comments.
If code is complex or messy, our first instinct should be to refactor it rather than explain it with comments.
This goal is sometimes referred to as creating self documenting code.
Self documenting code has at least two advantages over comments:
The rest of this section outlines the 3 most common ways we can replace comments with simply better code.
Often, a comment that describes what a variable does can be replaced by simply improving the variable name. Before:
// The aggression level
int Level { 0 };
After:
int AggressionLevel { 0 };
Additionally, we often have literals in our code that warrant an explanatory comment. These literals are sometimes called "magic strings" or "magic numbers" - seemingly arbitrary values that have some special properties.
// Unicode character for a smiling emoji
Chat.Send("U+1F600");
// This URL returns our special offers
HTTP::Get("https://192.63.27.92");
We can make that relationship concrete and self-documenting by just creating variables.
string SmilingEmoji { "U+1F600" };
Chat.Send(SmilingEmoji);
string SpecialOffers { "https://192.63.27.92" }
HTTP::Get(SpecialOffers);
Another common source of comments is documenting properties associated with types. We might have a variable, or a set of variables that have some semantic meaning that is not immediately clear.
Instead of needing comments to document that, we can often use new types. Before:
// x, y and z positions in the world
float x { 0 };
float y { 0 };
float z { 0 };
After:
struct Position { float x; float y; float z; }
Position WorldPosition { 0, 0, 0 };
Enum types are often useful for this purpose, too. Before:
// 0 is friendly, 1 is neutral, 2 is hostile
int AggressionLevel { 0 };
After:
enum class Aggression { Friendly, Neutral, Hostile };
auto AggressionLevel { Aggression::Friendly };
Where an existing type does not exactly match our requirements, we may be tempted to reuse it with some comments:
// Returns ability damage as a std::pair
// first is damage, second is crit chance
std::pair<int, float> GetDamageValues();
Instead, we should consider just introduce a new, self-documenting type:
struct AbilityDamage { int Damage, float CritChance };
AbilityDamage GetDamageValues();
Even if a more generic type does match our requirements, we sometimes feel a comment is warranted to specifically explain what that type contains:
// This vector only contains the abilities that have already been learned
std::vector<Ability> GetAbilities();
Instead, we can just do this with a type alias:
using LearnedAbilities = std::vector<Ability>;
LearnedAbilities GetAbilities();
When we create a complex function, composed of multiple parts, our first instinct is often to describe what is going on in that function by introducing a lot of comments.
That might look something like this:
bool Attack(Character* Target) {
// Make sure we have a target, and it is valid to attack it
if (!Target) return false;
if (Target.Hostility == Aggression::Friendly) return false;
// Target is attackable, but we need to make sure it is in range
float [x1, y1, z1] = MyPosition;
float [x2, y2, z2] = Target->Position;
float x = x1 - x2;
float y = y1 - y2;
float z = z1 - z2;
// The square root of this values will be the distance
float Distance { (x*x) + (y*y) + (z*z) };
if (sqrt(Distance) <= 10) {
// Attack was successful
Target->TakeDamage();
return true;
}
// Attack was unsuccessful
return false;
}
Instead, we should first check if the complexity can be reduced, typically by decomposing it into separate functions.
Breaking a big problem into smaller functions makes our code simpler, and gives us the opportunity to replace our comments with descriptive function names instead.
As an additional benefit, quite often these smaller, more generic functions can be useful in multiple places. This allows us to reuse them to make future work, and reducing the amount of code we have to write and maintain.
For the above function, decomposing it into smaller functions might look something like this:
bool Attack(Character* Target) {
if (!isValidTarget(Target)) return false;
Target->TakeDamage();
return true;
}
private:
bool isValidTarget(const Character* Target) {
if (!Target) return false;
if (Target.Hostility == Aggression::Friendly)
return false;
if (CalculateDistance(Target->Position) > 10)
return false;
return true;
}
float CalculateDistance(Position TargetPosition) {
float [x1, y1, z1] = MyPosition;
float [x2, y2, z2] = TargetPosition;
float x = x1 - x2;
float y = y1 - y2;
float z = z1 - z2;
return sqrt((x*x) + (y*y) + (z*z));
}
Become a software engineer with C++. Starting from the basics, we guide you step by step along the way