Last year I watched a YouTube video about “no comment code” which, to me, sounded like a terrible idea. A quick web search shows that not commenting code has it’s proponents. Let’s not get into that right now.
While I didn’t agree with the video as a whole I did like one specific idea:
Use types, rather than comments, to show what data is required.
Actually, that one thing sounds kinda great.
Instead of:
// Duration is measured in seconds. void Sleep(float duration);
You can have:
void Sleep(std::chrono::duration duration);
The first requires a comment so you know how use it. That is something that could easily be ignored if,
say, someone just expected duration
to be in milliseconds. The second doesn’t need the comment. Given how
std::chrono::duration
is defined it doesn’t matter whether duration
is given in seconds, milliseconds,
or whatever. However the function is called it will be translated into the correct units when it is used.
That’s not as good as using a comment, it’s better.
Units of measurement
It’s easy to imagine extending this beyond time. There could be types to represent all sorts of quantities that we measure: time, distance, mass, temperature, all the scientific units. Then there are all the ways those can be combined: force, pressure, energy and more. That would be a lot of types to define individually but we don’t have to. We can define base units and then combine them together to create derived units. The details of this are going to be complicated but mp-units shows it can be done. You could end up with something like this:
auto distance = 10 * meters; auto time = 5 * seconds; auto speed = distance / time; std::cout << speed << std::endl; // Prints: 2 meters / seconds
Again, the advantage here is not only in documentation but also that the units that you use don’t matter any more. The Mars Climate Orbiter is a failed Mars mission. It was destroyed because one team used SI units and another team used US customary units. Using these typed measurements different teams could use different units and they would be converted automatically.
Success and failure
It’s common to have functions that return the indication of success or failure via a boolean. It may be obvious that this is the intent or it may not be. It leads to many comments of the form:
// Returns true if successful and false otherwise.
Sometimes an enumeration is used instead of a boolean to make this more explicit:
enum Outcome { Success, Failure };
It could be even better to have a fully formed class:
class Outcome { Outcome(bool value); operator bool() const; ... }; const Outcome Success(true); const Outcome Failure(false);
This doesn’t need a comment about any return value, is easy to create, and can be used directly in, say, an if-statement to control the response to the outcome.
Error
If you want to know more than a simple success or failure then an enumeration or exception handling are the typical ways to go. A custom class is another option. Something like this:
class Error { Error(const std::string& message); operator bool() const; ... }; const Error NoError = Error::GetNoError(); const Error FileNotFound("File not found");
Again this doesn’t need to comment the return value and can be used directly in control flow.
A set of standard errors can be defined alongside the Error
class. However, unlike enums, other
systems can add their own error values and their is no risk of re-using values.
Count
I was working on ByteArray
classes recently.
This allowed bytes to be easily re-interpreted as immediate types, structs and arrays.
The interface was easy for most types but less clear for arrays:
class ByteArray { ... template <typename Type> const Type& Get(uint64_t position); template <typename Type> const std::range<Type>& GetRange(uint64_t position, uint64_t size_in_bytes); ... };
Given that the position was measured in bytes it seemed to make sense that the size should also be measured in bytes. However it was typically used to get a know number of objects. That meant an extra size conversion every time. This can be made explicit.
const auto intsByByte = byteArray.GetRange<uint32_t>(0, Count<byte>(40)); const auto intsByObject = byteArray.GetRange<uint32_t>(0, Count<uint32_t>(10)); assert(intsByByte == intsByObject);
I’m slightly dissatisfied with this as it makes the code seem bulky. However it reduces the chance of someone getting the wrong range. Even if someone hasn’t read the documentation they have to use it correctly. This could also be useful in an interface where several things are being counted and they could be confused.
Quantity
You might also want to have finer discrimination of what is being measured. A typed variant of a floating-point number or even the units of measurement from earlier could know what it is measuring.
const Quantity<Oxygen> oxygen(50 * litres); const Quantity<Hydrogen> hydrogen(100 * litres); const auto water = React(oxygen, hydrogen); std::cout << water << std::endl; // Prints: H2 O * 50 litres
On balance
That’s just a few possibilities that I threw together quickly, I’m sure there are more out there. While I didn’t like the “no comment code” in general that doesn’t mean it doesn’t have something to say. Don’t dismiss and entire set of ideas just because you don’t agree with a few of them.
Leave a Reply