10 Coding styles for a cleaner C code

In this post we are going to see multiple conventions that enhances code readability, making it easier for both the original author and other team members to understand and maintain the codebase. 

Simple coding style plays a crucial role in the development of efficient and maintainable software. By adhering to a simple coding style, developers ensure that their code is clear, readable, and easy to understand. This simplicity enhances collaboration within the development team, as well as aids in code reviews and debugging processes. A simple coding style also reduces the likelihood of introducing bugs and makes it easier to identify and fix issues when they arise. Additionally, simplicity in coding leads to more maintainable code, as it is easier to modify, refactor, and extend in the future. This is key for code quality, team productivity, and contributes to the long-term success of software projects.

C coding style

Over the years I've tried multiple coding style in different projects. Some of them looked appealing at the beginning, but after working for some time using them, I realized that they were not adding clarity to the code. So after some trial and error, I ended up with the following list of conventions for my C coding style:

typedef

Use typedef for defining user types. That way you avoid the repetition of the struct keyword everywhere, leading to a much cleaner and easier to read code.

Compare this:


  // Clearer
  typedef struct User {
    int number;
  } User;
  
  void test1(User* user) {
    User* tmp = user;
  }
  
  void test2(User* user) {
    User* tmp = user;
  }

to this:


  // More cumbersome
  struct User {
    int number;
  };
  
  void test1(struct User* user) {
    struct User* tmp = user;
  }
  
  void test2(struct User* user) {
    struct User* tmp = user;
  }

Braces

For the case of braces I've come up with a couple of conventions:

Opening brace

Keeping the opening brace on the same line as the function name, condition or loop keyword, reduces the amount of lines needed to see the whole content of a function. That way, it is easier to fit a function in one screen:

Compare this:


  int main(int argc, char* argv[])
  {
    if (argc == 1)
    {
      for (int i=0; i<5; ++i)
      {
        print("%d\n", i);
      }
    }
    else
    {
      for (int i=0; i<10; ++i)
      {
        print("%d\n", i);
      }
    }
  }

to this:


  int main(int argc, char* argv[]) {
    if (argc == 1) {
      for (int i=0; i<5; ++i) {
        print("%d\n", i);
      }
    } else {
      for (int i=0; i<10; ++i) {
        print("%d\n", i);
      }
    }
  }

Always braces

There are some coding styles that advice you to not use braces for the cases of conditions and loops containing one line only. Although it might look cleaner, there is a problem with this approach. If you refactor your code, and by mistake you indent a line after the condition together with the condition line, it is difficult to see exactly what statements are going to be executed if the condition is met:


  void function() {
    int x = 1;
    if (x == 2)
      x++
    printf("%d\n", x);
  }

If you change the identation of the printf statement, it is difficult to see whether its execution is included or not in the condition:


  void function() {
    int x = 1;
    if (x == 2)
      x++
      printf("%d\n", x);
  }

However, if we use braces instead, there is no doubt about what statements are included in the execution of the condition:


  void function() {
    int x = 1;
    if (x == 2) {
      x++
    }
    printf("%d\n", x);
  }

Functions

Snake case is my preferred naming convention for function names as it enhances readability and ensures clear separation between words, allowing for easier understanding and improved code comprehension.


  void filter_user() {
  }
  
  void print_email() {
  }

Types

For the case of types I prefer Pascal case as it promotes clarity and consistency by capitalizing the first letter of each word, resulting in more descriptive and distinguishable type declarations.


  typedef struct TreeNode TreeNode;
  
  typedef struct HashMap HashMap;

Static

The static keyword defines a function to be visible only within that translation unit. Therefore, it can be used for defining private elements. I like to define the static keyword on the upper line above the line where an element is declared because that way we can detach the visibility specification from the rest of the definition, making it easier to read and understand:

  User* new_User() {
  }
  
  void delete_User() {
  }
  
  static
  void User_filter_name(User* u) {
  }

Methods

Object reference argument

In Object Oriented programming, methods correspond to the functions exposed by an Object. That translates to functions whose first argument is a reference to the Object where the method is executed onto. In this case I like the convention defined by the Go language: use a one letter lowercase name for the argument corresponding to the Object reference. It would be equivalent to the implicit argument called this in languages like Java.

Type prefix

Apart from that, I use a type prefix for methods, so that it is clear that those functions are linked to Objects of that type.

Taking these two conventions into account, it would result in something like the following:

  User* new_User() {
  }
  
  void delete_User() {
  }
  
  static
  void User_filter_name(User* u) {
  }

Free functions

For the case of functions that are not methods of any Object, they do not need any prefix, as they are not exposed by Objects of a particular type:

  static
  int find_split(int x) {
  }

Function Types

Function types are needed for callbacks, polymorphism, etc. But if you use a long naming convention, it can lead to cumbersome method definitions. That is why I use just Fn as suffix for function type names. It follows the Pascal Case convention for names, an it is as compact as possible:

  typedef void (*ListPrintFn)(void* item);
  
  void List_print(List* l, ListPrintFn print);

Return multiple values

Go language has support for returning multiple values from a function. And although in C you can use pointer arguments to simulate returning multiple values, I prefer to use a struct with as many fields as required, in case you need multiple values from a function. That way we end up having a uniform and coherent convention for arguments and return values:
  • Multiple inputs: arguments
  • One output: returned value

  typedef struct Value {
    void*  data;
    size_t size;
  } Value;
  
  Value Container_get(Container* c, int index);

Small names

This is a convention that requires experience and some experimentation. It is something that could be a candidate for refactoring. The key is to strive for compact and meaningful names that help you read and understand the code as quick as possible.

Compare this:


  int calcAverageOfArray(Array* a);

to this:


  int Array_avg(Array* a);

Popular posts from this blog

How to setup NeoVim configuration file

WebAssembly (Wasm): Fixing the Flaws of Applets

How to query Outlook from PowerShell