‘Modern’ C

Daniel Andersson
2022-06-17

C has evolved since C++ was based on it and there are often misconceptions about C, where people commonly think that C is just the “subset that is supported by C++”. In 1999 the C standard was revised. The language defined by that version of the standard is known as C99. C99 and later is also often called Modern C. C99 had a lot of usability improvements, one of note is the then new initialization features.

C99 support has been poor on Windows, and Microsoft told people to compile C with the C++ compiler. That is likely a large reason why there are misconceptions about C. You cannot compile C99 code in a C++ compiler, since C++ was based upon an earlier version of C. The newer C standards C11 & C17 has been supported by MicrosoftÍ„’s compiler(MSVC) since 2020.

C99 initialization

C99 added designated initializers and compound literals, which lets you:

#include <stdio.h>
typedef struct {
  float x,y,z;
} inner;

typedef struct {
  inner coord;
  int val[4];
} outer;

inner square_inner(inner item) {
  return (inner){ .x = item.x*item.x, .y = item.y*item.y};
}

int main(void) {
  outer foo = {.val = { [0]=42, [1]=2 },
               .coord = {.y = 1.1f, .x = 3.0f}};
  outer bar = {.val = { 42, 2 },
               .coord = square_inner((inner){2.0f, 3.0f})};
  printf("val[0]='%d', val[1]='%d'\nx: '%f', y: '%f', z: '%f'\n",
          bar.val[0], bar.val[1], bar.coord.x, bar.coord.y, bar.coord.z);
  return 0;
}
// val[0]='42', val[1]='2'
// x: '4.000000', y: '9.000000', z: '0.000000'

C++20 got a more limited version of C99’s designated initializers. For example the initialization must be in declaration order. You cannot do {.z=2, .y=1, .x=4} in C++.

Default parameters

In C++ and other languages you can have default parameters to procedures:

#include <stdio.h>
void print(const char *text, int = 4);

int main(int argc, char *argv[]) {
  const char *text = "Hej";
  // ...
  print(text);
  return 0;
}

void print(const char *text, int count) {
  printf("%s->%d\n", text, count);
}

In C, you can almost do that, if you keep the parameters in a struct:

#include <stdio.h>

typedef struct {
  char *text;
  int val;
} stuff_t;

void print(stuff_t *item) {
  printf("%s->%d\n", item->text, item->val);
}

int main(void) {
  char *text = "Hej";
  // ...
  print(&(stuff_t){text, 4}); 
  
  return 0;
}

In the C case, the default values are set at the call site. If there are multiple call sites, that could create copy paste errors, especially if the parameters are changed. If there are a lot of parameters it is better to have an init function, where default values are only set if that particular value is 0:

#include <stdio.h>
#define DEFAULT(value, default) (((value) == 0) ? (default) : (value))
typedef struct {
  char *text;
  int val;
} stuff_t;

void stuff_init(stuff_t *stuff) {
  stuff->text = DEFAULT(stuff->text, "default");
  stuff->val  = DEFAULT(stuff->val, 42);
}

int main(void) {
  stuff_t stuff = {.val = 4};
  stuff_init(&stuff);
  printf("%s->%d\n", stuff.text, stuff.val);
  return 0;
}

Another way that may be better is to use a variadic macro that has default values that can be overwritten. I have not tried that though. Neither of these options are particularily clean or nice. I am by no means saying that C is the perfect language. I do however think that most newer features in many languages that came after C are actually worth the complexity they incur, but that is a topic for another time.

Replace macros

Many macros can be replaced with inline functions

Defines & #ifdef can often be replaced by enums. enums are ints, so large constants will not work and the preprocessor does not know what they are.

‘Strong’ types

When using typedef you only get an alias and not a new type:

typedef float meter;
typedef float feet;

int main(void) {
  meter foo = 4.0f;
  // ...
  feet bar  = foo;
  return 0;
}

If you instead wrap them in a struct, you get a compiler error when trying to assign variables of different types.

typedef struct {
  float val;
} meter;

typedef struct {
  float val;
} feet;

int main(void) {
  meter foo  = {3.0f};
  meter foo2 = foo;
  //feet bar = {foo}; // error: incompatible types when initializing type 'float' using type 'meter'
  return 0;
}

However, if you do feet bar = {foo.val}; above it will assign something in meters to a variable that is supposed to hold feet, since val is of type float.

Wrapping a float in a struct doesn’t generate identical asm for some reason, maybe it would in higher optimization levels, but for the code above the compiler would just optimize it away anyway. Swapping floats to ints generates the same asm.