Any fool can write code that a computer can understand. Good programmers write code that humans can understand. - Martin Fowler
Without exception, a style guide is an absolute requirement for any long-lived software project. The most obvious advantage is readability. Consistent formatting and naming guidelines reduce the reader’s requisite cognitive load. Beyond readability, style guides enforce conceptual integrity1. Defining preferred approaches to common tasks reduces bugs and creates a consistent experience for both end users and developers. Stated differently, style guides ensure that studying a single portion of the code provides insight into all the code.
A style guide is also a form of documentation. They record important decisions for posterity. When there are disagreements, the style guide is the source of truth. However, it’s important to avoid the common blunder of becoming beholden to one. They are living documents that mature and change with the project. It’s completely inappropriate to defend an inefficient practice with a style guide. It’s normal and healthy for them to evolve.
There are many established style guides that can simply be adopted2 and augmented on an as-needed basis. Whenever possible, this is the preferred approach. Pedantic arguments over things like brace placement and naming conventions have wasted countless engineering hours over the years. The truth is that such details simply aren’t that important. The primary utility of enforcing style is consistency. The beauty of adopting a popular style guide is that you get all the benefits of uniformity without the arduous task of defining standards. As an added bonus, many popular style guides come complete with tooling.
Maintaining style compliance is next to impossible without auto-enforcement tooling because it places an unreasonable burden on developers. Time spent configuring such tools pays massive dividends over the life of a project. Luckily, there are some great open source tools specifically for this purpose. The choice of a popular style guide makes configuration trivial. A thorough treatment of the tools such as linters and auto formatters is well out of scope. However, this project provides a good example of best practices.
One last salient point is that style guides provide guidelines which are not to be confused with mandates. Imposing Draconian constraints is detrimental to productivity. It is perfectly acceptable to deviate from style rules provided there is good reason. Personal preference is not a good reason. Any deviations must be appropriately documented with code comments.
The following section defines the style guide for this project.
Key Takeaways
- Style guides are an absolute requirement for long-lived software projects.
- Style guides enhance readability and enforce conceptual integrity.
- Style guides are a type of documentation that record important decisions for posterity.
- Adopting a popular established style guide is preferred over creating one from scratch.
- Maintaining style compliance is next to impossible without auto-enforcement tooling.
Style Guide
Following the guidance in the previous section, this project adopts the Google C++ Style guide. One reason for choosing this particular guide is that there are three good open source tools for auto-enforcing the rules: clang-format, cpplint, and clang-tidy3. The listing below documents the configuration of each tool.
- clang-format
- Configurable via a .clang-format file. The tool has a
feature for auto-generation of configuration files for many popular
style guides. The bash statement below demonstrates how to create a
Goggle C++ style guide configuration file. This project uses all the
default settings.
clang-format -style=google -dump-config > .clang-format
- Configurable via a .clang-format file. The tool has a
feature for auto-generation of configuration files for many popular
style guides. The bash statement below demonstrates how to create a
Goggle C++ style guide configuration file. This project uses all the
default settings.
- cpplint
- Configurable via a CPPLINT.cfg file.
- The default behavior is strongly preferred; however, there are a few
exceptions because this project uses C rather than C++. The deviations
are documented below.
- Disabled Use reinterpret_cast<int*>(…) instead [readability/casting] because it’s not possible to use C++ style casts in pure C code.
- Disabled Include the directory when naming .h files [build/include_subdir] because the build system does not support the paradigm.
- Disabled Do not use variable-length arrays. Use an appropriately named (‘k’ followed by CamelCase) compile-time constant for the size. [runtime/arrays] because the code uses VLAs for the sake of brevity.
- clang-tidy
- Configurable via .clang-format file. The tool has a
feature for auto-generation of the configuration file using the command
below.
clang-tidy --checks=clang-diagnostic-*,clang-analyzer-*,google-*,bugprone-*,readablity-*,modernize-*,misc-*,cert-*,-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,-misc-no-recursion --dump-config > .clang-tidy
- By default, clang-tidy generates warnings. The following
checks are
configured to manifest as errors4:
- google-*
- bugprone-*
- clang-analyzer-*
- The default behavior is strongly preferred; however, there are a few
exceptions which are documented below.
- Disabled security.insecureAPI.DeprecatedOrUnsafeBufferHandling because C11 Annex K is poorly supported. Also, the C standards committee lists the clang address sanitizer as a viable alternate solution5.
- Disabled misc-no-recursion because several of the implementations utilize recursion. Recursion provides an elegant means of demonstrating algorithms; however, it causes stack overflow exceptions when the input is sufficiently large. For this project, it is not an issue; however, use in production scenarios should be thoroughly scrutinized.
- Configurable via .clang-format file. The tool has a
feature for auto-generation of the configuration file using the command
below.
Style Guide Augmentation
As mentioned previously, using the adopted style guide’s default rules is highly recommended. There are instances, however, where it does not address project-specific concerns. Furthermore, the guide expects consumers to choose between options for a few select settings (e.g. column width and include guards). The purpose of this section is to document project-specific style guide augmentations.
Columns
No line should exceed 80 columns wide
Non-Compliant
static void this_is_a_really_long_function_name_with_args(unsigned int param1, unsigned int param2) {
...
}
Compliant
static void this_is_a_really_long_function_name_with_args(unsigned int param1,
unsigned int param2) {
...
}
Disadvantages
- Loss of space when using large tab widths. (This is a non-issue for this project because the default tab-width for clang-format is 4)
- Requires line breaks in unnatural locations in some extreme circumstances. (This is an accepted downside.)
Advantages
- Easily display c, h, and test files side by side on any reasonably sized monitor
- Snippets are easier to include in external documents
Include Guards
Use the #pragma once
preprocessor directive to generate include guards.
Non-Compliant
#ifndef FILENAME_H
#define FILENAME_H
... file content
#endif /* !FILENAME_H */
Compliant
# pragma once
... file content
Disadvantages
#pragma once
is non-standard; therefore, some compilers may not support it. Additionally, it could change. (This is of little concern because clang is the only supported compiler for this project.)
Advantages
- Less code
- Avoids naming collisions
- Improves compilation speed
Error Handling
Every function should return a ResultCode
enum
value as defined in
result_code.h.
Non-Compliant
void divide(int dividend, int divisor) {
if( divisor == 0){
fprintf(stderr, "Division by zero! Exiting...\n");
exit(EXIT_FAILURE);
}
return dividend/divisor;
}
SomeObject* SomeObject_Create() {
SomeObject* obj = malloc(sizeof(SomeObject));
if (obj == NULL) return NULL;
obj.init_value = 1;
return obj;
}
Compliant
ResultCode divide(int dividend, int divisor, int* result) {
if( divisor == 0){
return kDivideByZero;
}
*result = dividend / divisor;
return kSuccess;
}
ResultCode SomeObject_Create(SomeObject** result) {
SomeObject* obj = malloc(sizeof(SomeObject));
if (obj == NULL) return kFailedMemoryAllocation;
obj.init_value = 1;
*result = obj;
return kSuccess;
}
Disadvantages
- Does not provide file names and line numbers
- Does not provide a means for conveying contextual information. This is
somewhat mitigated by the
Result_ErrorMessage
function that allows consumers to print\log errors if they wish.
Advantages
- Gives consumers the flexibility to handle errors in whatever way works best for them
- Eliminates unwanted error messages during test runs.
- Allows tests to simulate error conditions without program termination.
-
“conceptual integrity is the most important consideration in system design.” The Mythical Man-Month by Fred Brooks ↩
-
For example, Google has some really great open source style guides: http://google.github.io/styleguide/ ↩
-
The Source Code page covers setup and use of the tools, so it is omitted here. ↩
-
It’s a laudable practice to address all warnings; however, such a goal is often unattainable. A warning treated as an error requires amelioration before passing the CI process. Carefully consider which warnings are configured as such because false positives can be the bane of a developer’s existence. ↩
-
See the “Alternate Solutions” section: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1969.htm ↩