Search code examples
cmacrospreprocessor

Specialized macro expansion based on argument type in C


I have a function which has a uint32_t * as its argument which actually points to a 64-bit value. I want to have a macro around it which accepts uint64_t as input.

Can the macro expand differently based on its input argument type?

The macro below works fine for my case; however, it's not efficient:

void func(uint32_t* a);

#define func_wrapper(a) do { \
    uint64_t aa = a; func((uint32_t*) &aa); \
} while(0)

For example, case 1 is not as efficient as case 3 below.

uint64_t x = 12;
func_wrapper(x)        // case 1
func_wrapper(12)       // case 2
func((uint32_t*) &x);  // case 3

Is there a way to define a macro which expands to

#define func_wrapper(a) { \
    func((uint32_t*) &(a)); \
}

when the argument is not a literal and expands to

#define func_wrapper(a) { \
    uint64_t aa = a; func((uint32_t*) &(aa)); \
}

when it is?


Solution

  • C macros are handled entirely during a preprocessing stage that lacks type information, and cannot expand differently based on language concepts like types. The C language provides very few tools for polymorphism, which seems to be what you are actually asking for.

    It's also worth saying that accessing a value with effective type of uint64_t through a pointer of type uint32_t* breaks C's type-aliasing rules. This is a very bad idea and can lead to all sorts of undefined behavior, especially in the presence of high levels of compiler optimization.

    Now on to the underlying assumption in your question, I'd like to address this statement:

    For example, case 1 is not as efficient as case 3

    Here is your code after macro expansion, where I've also fixed the type-aliasing violation:

    void func(uint64_t* a);
    
    uint64_t x = 12;
    uint64_t aa = x; func((uint64_t*) &aa); // case 1
    func((uint64_t*) &x);                   // case 3
    

    You should not assume that case 3 is more efficient at runtime than case 1. C optimizers are capable of some very aggressive optimization, and the number of instructions/cycles in the final optimized executable has only a weak correlation with the number of statements in the C language. In this example, if the compiler can prove that func does not modify its argument, then it can apply copy propagation and remove the aa temporary variable entirely. Assuming the property is true, you can potentially help the analysis do this by declaring func with a const qualifier, ie:

    void func(const uint64_t* a);
    

    However even in the absence of any optimization whatsoever, it's unlikely you'd be able to measure a real performance difference between any of these on a modern superscalar processor.