There’s a great keyword in C# called nameof which is used to “stringify” a variable, type, or member. It is used all the time for simple things like checking if a variable is null at the beginning of a method.
public static void PrintIntegers(IEnumerable<int> integers)
{
// NOTE: This can be done easier in newer versions of C# using:
// ArgumentNullException.ThrowIfNull(integers)
if (integers == null)
{
throw new ArgumentNullException(nameof(integers));
}
foreach (var integer : integers)
{
Console.WriteLine($"{integer}");
}
}The nameof keyword is great. If the variable integers ever changes to something like integersToPrint, we would get a compilation error if we didn’t also update our use of the variable when throwing our ArgumentNullExcpetion. This ensures that we don’t have out-of-date strings littering our codebase and making it difficult to figure out what things mean when something goes wrong months down the line. If you use your handy dandy rename feature in an IDE then all usages of the variable get updated anyway and you don’t have to go hunt down old names inside string constants.
I want this same feature in C, which means we are writing macros!
#include <stdio.h>
#include <stddef.h>
#define NAMEOF(x) ((void)sizeof(&(x)), #x)
#define NAMEOF_TYPE(t) ((void)sizeof((t *)NULL), #t)
typedef struct foo {
int a;
int b;
} foo;
int main() {
foo f = { .a = 1, .b = 2 };
printf("The value of '%s' is %d\n",
NAMEOF(f.a),
f.a);
printf("The value of '%s' is %d\n",
NAMEOF(f.b),
f.b);
printf("The type of '%s' is '%s'\n",
NAMEOF(f),
NAMEOF_TYPE(foo));
}
/* Output:
* The value of 'f.a' is 1
* The value of 'f.b' is 2
* The type of 'f' is 'foo'
*/NAMEOF and NAMEOF_TYPE generate a comma separated list of expressions. Multiple expressions can be separated by commas and only the rightmost expression is returned. For NAMEOF, we call sizeof on the address of x. Since sizeof is run at compile time, we’re not actually needing to evaluate the real address of x at runtime. We just want the type checking to occur. Taking the address of x ensures that the argument provided is an lvalue or a function designator. NAMEOF_TYPE works in a similar way being evaluated at compile time. For types, we attempt to cast NULL to a pointer of type t. If either of these initial evaluations cause problems, there will be a compilation error about an unknown variable or type respectively. I wasn’t able to figure out a way to combine the features of NAMEOF and NAMEOF_TYPE like C# does, but this works well for me now. Check out some usage of it in my project, otter.
Leave a Reply