DEV Community

Joshua Matthews
Joshua Matthews

Posted on

Named Params in C

Table of Contents:

Introduction

In a language like python you have the concept of **kwargs as a function parameter. This special double * syntax allows you to pass in named arguments captured as a dictionary, like the example below:

def print_item(**kwargs):
    # kwargs is a dictionary
    print(f"Item name: {kwargs['name']}")
    if "kind" in kwargs:
        print(f"Kind: {kwargs['kind']}")
    print(f"Quantity: {kwargs['quantity']}")
    print(f"Liters: {kwargs['liters']}")

print_item(name="Milk", quantity=2, liters=1)
# output:
# Item name: Milk
# Quantity: 2
# Liters: 1
Enter fullscreen mode Exit fullscreen mode

This allows for a convenient way to pass arguments by their name in any order you'd like and have optional parameters without complicating the function signature.

What if we could do this in C? (spoiler: there is a way)

Implementation in C

Through the magic of the preprocessor we can accomplish this effect, optional parameters and all.

First lets define our item structure.

struct item_t {
    const char *name;
    const char *kind;
    int quantity;
    float liters;
};
Enter fullscreen mode Exit fullscreen mode

Next we'll define our print function like you would normally in C.

// name it with `__t` for the concrete function.
void print_item__t(struct item_t item) {
    printf("Item name: %s\n", item.name);
    if (item.kind != NULL) {
        printf("Kind: %s\n", item.kind);
    }
    printf("Quantity: %d\n", item.quantity);
    printf("Liters: %f\n", item.liters);
}
Enter fullscreen mode Exit fullscreen mode

Now we can write the magically preprocessor part.

// name the macro what we want and use `...` to signify va_args
// Then call our function with a structure with default values
// and the va_args at the end to overwrite any values.
#define print_item(...)            \
    print_item__t((struct item_t){ \
        .name = "default",         \
        .kind = NULL,              \
        .quantity = 0,             \
        .liters = 0,               \
        __VA_ARGS__                \
    })
Enter fullscreen mode Exit fullscreen mode

So lets break down this macro.

  1. First we define the macro name and specify we are using va_args with ... as the parameter.
#define print_item(...)            \
Enter fullscreen mode Exit fullscreen mode
  1. Next we call the function with a structure of our item_t with default values.
    print_item__t((struct item_t){ \
        .name = "default",         \
        .kind = NULL,              \
        .quantity = 0,             \
        .liters = 0,               \
Enter fullscreen mode Exit fullscreen mode
  1. Last we pass in the __VA_ARGS__ values at the end of our default structure. This works because C allows the later defined properties to overwrite earlier ones.
        .liters = 0,               \
        __VA_ARGS__                \
    })
Enter fullscreen mode Exit fullscreen mode

Here's how all of this would be structured in actual files.

  • item.h
#ifndef ITEM_H
#define ITEM_H

struct item_t {
    const char *name;
    const char *kind;
    int quantity;
    float liters;
};

void print_item__t(struct item_t item);

#define print_item(...)            \
    print_item__t((struct item_t){ \
        .name = "default",         \
        .kind = NULL,              \
        .quantity = 0,             \
        .liters = 0,               \
        __VA_ARGS__                \
    })

#endif
Enter fullscreen mode Exit fullscreen mode
  • item.c
#include "item.h"
#include <stdio.h>

void print_item__t(struct item_t item) {
    printf("Item name: %s\n", item.name);
    if (item.kind != NULL) {
        printf("Kind: %s\n", item.kind);
    }
    printf("Quantity: %d\n", item.quantity);
    printf("Liters: %f\n", item.liters);
}
Enter fullscreen mode Exit fullscreen mode

Using our Implementation

Now we can use our macro similar to the python example in the introduction.

#include "item.h"

int main(void) {
    print_item(.name = "Milk", .quantity = 2, .liters = 1);
    return 0;
}
// output:
// Item name: Milk
// Quantity: 2
// Liters: 1
Enter fullscreen mode Exit fullscreen mode

Conclusion

Through preprocessor magic we can achieve a syntax convenience with calling functions that have struct parameters as options. This can be extremely useful for functions with lots of optional parameters and\or default values. However, preprocessor magic is something that is either loved or hated so you may want to consider if it's worth implementing in your project or not.

Top comments (8)

Collapse
 
lolpopgames profile image
LolPopGames

You made a small mistake with the comments at the end (instead of // or /* you used #)

I really liked the idea of ​​making parameters optional, even though I know you mentioned creating a Python-like way to interact with parameters

This method allows for optional parameters, and if you think about it further, you can implement mandatory parameters as well


Speaking of C syntax tricks, I once played around with using the dot "similar to how namespaces are used."

My favorite example is a base64 library:

char *__b64__encode(const uint8_t *bytes, size_t size);
uint8_t *__b64__decode(const char *b64_str, size_t *decoded_size);

struct __b64
{
    char *(*encode)(const uint8_t *, size_t);
    uint8_t *(*decode)(const char *, size_t *);
};

#define b64 (struct __b64) { \
    .encode = &__b64__encode, \
    .decode = &__b64__decode, \
}
Enter fullscreen mode Exit fullscreen mode

And now we can use b64.encode and b64.decode that looks cool

Collapse
 
pauljlucas profile image
Paul J. Lucas
  1. You generally shouldn't pass structures by value. You can pass by pointer just as easily and more efficiently.
  2. Some compilers will warn that you are specifying members more than once.
Collapse
 
jmatth11 profile image
Joshua Matthews

I figured since we were doing silly preprocesses magic to achieve a Python-like syntax, it would be understood it’s not going to be “the most efficient” way.

As far point 2, this is a good call out. clang warns by default and gcc warns about it with -Wextra

Collapse
 
pauljlucas profile image
Paul J. Lucas

But you can do it efficiently. The technique you describe has been around for decades and I'm sure is used in real-world projects, so might as well show an efficient implementation.

As far as the warning, more specifically, it's controlled by the -Woverride-init warning. You can suppress the warning by use of _Pragma:

#define print_item(...) \
  _Pragma("GCC diagnostic push") \
  _Pragma("GCC diagnostic ignored \"-Woverride-init\"") \
  print_item__t( (struct item_t){ .name = "default", __VA_ARGS__ } ) \
  _Pragma("GCC diagnostic pop")
Enter fullscreen mode Exit fullscreen mode

Or something like that.

Thread Thread
 
jmatth11 profile image
Joshua Matthews

What would an efficient implementation look like?

Generating a temp variable in the macro?
I am curious what you would do differently

Thread Thread
 
pauljlucas profile image
Paul J. Lucas

I already said in my original point 1: pass by pointer:

#define print_item(...) \
  print_item__t( &(struct item_t){ ... } )

void print_item__t( struct item_t const *item ) {
  ...
}
Enter fullscreen mode Exit fullscreen mode

C allows you to put & in front of a structure literal; then just use it in the function via pointer.

You always have to construct a temporary. In your original code, a temporary is constructed then copied into the function's parameter since it's by value. In my version, the same original temporary is created, but no copy is made.

Thread Thread
 
jmatth11 profile image
Joshua Matthews • Edited

Oh gotcha, it does feel weird to take a pointer of a non-bound temporary. (I can’t really explain why it feels weird other than personal preference)
But that should be on the implementer if they hold on to the pointer longer than the function.

I guess I thought you were referencing something more than that approach. But that makes sense! I misunderstood your original comment

Thread Thread
 
pauljlucas profile image
Paul J. Lucas

FYI, the lifetime of such a temporary is the scope in which it's defined, so:

struct item_t const *g;

void f1( struct item_t const *p ) {
  g = p;
}

void f2() {
  if ( true ) {
    f1( &(struct item_t){ .name = "x" } );
    puts( g->name );  // defined behavior
  }
  puts( g->name );    // undefined behavior
}
Enter fullscreen mode Exit fullscreen mode

The lifetime of the temporary persists until the } of the if, so inside for the first puts, it's fine; but for the second puts, it's undefined.

Granted, you should likely never set such a global pointer to a temporary regardless; but it's just to illustrate the rule.