2

This question is nearly a duplicate, but I figured I'd ask again since it's pretty old and the language may have evolved: Variadic recursive preprocessor macros - is it possible?

I would like to implement a constant ONE_HOT encoding operation and I'm wondering if this is possible using variadic macros.

I would like to compute a constant one-hot encoding of my enum elements and store it in an uint32. Basically, reduce all arguments of ONE_HOT(a, b, c, ...) as (1 << a) | (1 << b) | (1 << c) ....

Option A: Bit-shift each entry when computing.

typedef enum {
  FIRST,
  SECOND,
  THIRD,
  // More entries
  MY_ENUM_NUM_ENTRIES // Guaranteed to never be greater than sizeof(uint32_t)*8 by design.
} MY_ENUM_TYPE;

// In this case, only FIRST and THIRD are "set".
uint32_t expected_entries = (1<<FIRST) | (1<<THIRD);

// Check if set
if(expected_entries & (1<<SECOND)){
  // Do something.
}

Option B: Modify the enum declaration and assign it power-of-2 values.

typedef enum {
  FIRST = (1 << 0),
  SECOND = (1 << 1),
  THIRD = (1 << 2),
  // More entries
  MY_ENUM_NUM_ENTRIES // Downside: This is now broken. Requires a workaround.
} MY_ENUM_TYPE;

// Upside: The one-hot operation is nicer to read and write.
uint32_t expected_entries = FIRST | THIRD;

// Check if set
if(expected_entries & SECOND){
  // Do something.
}

Option C: Variadic Macro?

Is there a way to define my operation such that the one-hot computation is reduced and evaluated at compile-time whenever possible?

  1. I'm aware that recursive macro definitions are discouraged since the pre-processor makes a single-pass.
  2. Is it possible to avoid defining 32 macros, one for each arg count? See the linked question at the start.
#define IS_HOT(encoded, bit) (encoded & (1<<bit))
#define SET_HOT(encoded, bit) (encoded | (1<<bit))
#define CLEAR_HOT(encoded, bit) (encoded & (0xFFFFu^(1<<bit)))

// PSEUDO-CODE.
// I want all values (a, b, c, ...) passed into ONE_HOT to be shifted
// and bitwise OR'd together to form the one-hot encoding.
#define ONE_HOT(a, b, c, ...) (1 << a) | (1 << b) | (1 << c) | ONE_HOT(##__VA_ARGS__)

// END PSEUDO-CODE

typedef enum {
  FIRST,
  SECOND,
  THIRD,
  // More entries
  MY_ENUM_NUM_ENTRIES // Guaranteed to never be greater than sizeof(uint32_t)*8 by design.
  // BONUS: we don't need to redefine our original enum.
} MY_ENUM_TYPE;

// Upside: The one-hot operation is nicer to read and write.
uint32_t expected_entries = ONE_HOT(FIRST, THIRD);
// This is terrible
//   uint32_t expected_entries = SET_HOT(SET_HOT(encoded, FIRST), THIRD);
// Check if set
if(IS_HOT(expected_entries, SECOND)){
  // Do something.
}
康桓瑋
  • 33,481
  • 5
  • 40
  • 90
  • It looks that you want "map" pattern in c preprocessor. See https://github.com/swansontec/map-macro/blob/master/map.h – tstanisl Jan 22 '23 at 09:18
  • That's definitely one way to address the problem. Thanks a lot, @tstanisl! Here's a link to the answer associated with that repo https://stackoverflow.com/a/13459454/21058589 – AlejandroLIF Jan 22 '23 at 15:22
  • 1
    There's some confusion about integer types here. You cannot do `1 << 31` because that invokes undefined behavior. Therefore always use `1u << 31`. Similarly, you cannot use enumeration constants since they are guaranteed to be of type (signed) `int` and therefore cannot fit `1u << 31`. (A better enum system will get rolled out with C23 where you can set the size of the enum upon declaring it.) – Lundin Jan 23 '23 at 13:51
  • That being said, is there a reason that you can't just do `HOT_STUFF( HOT(1) | HOT(3) );` etc, with `#define HOT(n) (1u << n)`? A macro usage like that isn't particularly exotic, a lot of C interfaces expect the caller to do the bit merging with bitwise OR. I don't really understand why you insist on compile-time evaluation since something like `if(IS_HOT(expected_entries, SECOND))` is evaluated in run-time anyway. – Lundin Jan 23 '23 at 13:54
  • Definitely possible to define the simple `HOT(n)` macro and OR everything together exactly like you suggested. My question was a bit broader, covering the possibility of a more generic "reduction" operation. The hot operation is an excuse and a single example of this. Perhaps there could be more clever applications. Regarding the types - definitely something to consider. This would be a legal operation, would it not? `1u << ENUM_VALUE` The operation would yield an `u32` output type, even though the enumerated type (which defines the shift) is underlying `i32`. – AlejandroLIF Jan 25 '23 at 04:19

1 Answers1

1

onehot.h

Implemented with the "Map-Macro" pattern, as suggested.

#ifndef ONEHOT_H
#define ONEHOT_H

// Defining the map-macro operation per
// https://github.com/swansontec/map-macro
#define MAP_OUT

#define EVAL0(...) __VA_ARGS__
#define EVAL1(...) EVAL0 (EVAL0 (__VA_ARGS__))
#define EVAL2(...) EVAL1 (EVAL1 (__VA_ARGS__))
#define EVAL3(...) EVAL2 (EVAL2 (__VA_ARGS__))
#define EVAL4(...) EVAL3 (EVAL3 (__VA_ARGS__))
#define EVAL(...)  EVAL4 (EVAL4 (__VA_ARGS__))

#define MAP_END(...)

#define MAP_GET_END() 0, MAP_END
#define MAP_NEXT0(test, next, ...) next MAP_OUT
#define MAP_NEXT1(test, next) MAP_NEXT0 (test, next, 0)
#define MAP_NEXT(test, next)  MAP_NEXT1 (MAP_GET_END test, next)

#define MAP0(f, x, peek, ...) f(x) MAP_NEXT (peek, MAP1) (f, peek, __VA_ARGS__)
#define MAP1(f, x, peek, ...) f(x) MAP_NEXT (peek, MAP0) (f, peek, __VA_ARGS__)

// Final structure of the map-macro
#define MAP(f, ...) EVAL (MAP1 (f, __VA_ARGS__, (), 0))

// One-Hot Definition
#define ONE_HOT_SHIFT(a) (1u << a) | 
#define ONE_HOT(...) MAP(ONE_HOT_SHIFT, __VA_ARGS__) 0

#define IS_HOT(encoded, bit)    (encoded & (1 << bit))
#define SET_HOT(encoded, bit)   (encoded | (1 << bit))
#define CLEAR_HOT(encoded, bit) (encoded & (0xFFFFu^(1 << bit)))
#define INTERSECT_HOT(a, b)     (a & b)
#define UNION_HOT(a, b)         (a | b)
#define EQUALS_HOT(a, b)        (~(a^b))
#define A_CONTAINS_ALL_B(a, b)  (EQUALS_HOT(INTERSECT_HOT(a, b), b))

#endif