Make interfaces precisely and strongly typed
Reason
Types are the simplest and best documentation, improve legibility due to their well-defined meaning, and are checked at compile time. Also, precisely typed code is often optimized better.
Example, don't
Consider:
void pass(void* data); // weak and under-qualified type void* is suspicious
Callers are unsure what types are allowed and if the data may be mutated as const is not specified. Note all pointer types implicitly convert to void*, so it is easy for callers to provide this value.
The callee must static_cast data to an unverified type to use it. That is error-prone and verbose.
Only use const void* for passing in data in designs that are indescribable in C++. Consider using a variant or a pointer to base instead.
Alternative: Often, a template parameter can eliminate the void* turning it into a T* or T&. For generic code these Ts can be general or concept constrained template parameters.
Example, bad
Consider:
draw_rect(100, 200, 100, 500); // what do the numbers specify?
draw_rect(p.x, p.y, 10, 20); // what units are 10 and 20 in?
It is clear that the caller is describing a rectangle, but it is unclear what parts they relate to. Also, an int can carry arbitrary forms of information, including values of many units, so we must guess about the meaning of the four ints. Most likely, the first two are an x,y coordinate pair, but what are the last two?
Comments and parameter names can help, but we could be explicit:
void draw_rectangle(Point top_left, Point bottom_right);
void draw_rectangle(Point top_left, Size height_width);
draw_rectangle(p, Point{10, 20}); // two corners
draw_rectangle(p, Size{10, 20}); // one corner and a (height, width) pair
Obviously, we cannot catch all errors through the static type system (e.g., the fact that a first argument is supposed to be a top-left point is left to convention (naming and comments)).
Example, bad
Consider:
set_settings(true, false, 42); // what do the numbers specify?
The parameter types and their values do not communicate what settings are being specified or what those values mean.
This design is more explicit, safe and legible:
alarm_settings s{};
s.enabled = true;
s.displayMode = alarm_settings::mode::spinning_light;
s.frequency = alarm_settings::every_10_seconds;
set_settings(s);
For the case of a set of boolean values consider using a flags enum; a pattern that expresses a set of boolean values.
enable_lamp_options(lamp_option::on | lamp_option::animate_state_transitions);
Example, bad
In the following example, it is not clear from the interface what time_to_blink means: Seconds? Milliseconds?
void blink_led(int time_to_blink) // bad -- the unit is ambiguous
{
// ...
// do something with time_to_blink
// ...
}
void use()
{
blink_led(2);
}
Example, good
std::chrono::duration types help making the unit of time duration explicit.
void blink_led(milliseconds time_to_blink) // good -- the unit is explicit
{
// ...
// do something with time_to_blink
// ...
}
void use()
{
blink_led(1500ms);
}
The function can also be written in such a way that it will accept any time duration unit.
template<class rep, class period>
void blink_led(duration<rep, period> time_to_blink) // good -- accepts any unit
{
// assuming that millisecond is the smallest relevant unit
auto milliseconds_to_blink = duration_cast<milliseconds>(time_to_blink);
// ...
// do something with milliseconds_to_blink
// ...
}
void use()
{
blink_led(2s);
blink_led(1500ms);
}
Enforcement
- (Simple) Report the use of
void*as a parameter or return type. - (Simple) Report the use of more than one
boolparameter. - (Hard to do well) Look for functions that use too many primitive type arguments.