#Style Question: Is This a Good Use Case for the Preprocessor?

27 messages · Page 1 of 1 (latest)

knotty musk
#

Template metaprogramming is one of the things I miss about C++, so I'm using the C preprocessor to accomplish similar results.
I'm trying to move what would be runtime parameters (in this case, color, move_type, and direction) to compile-time.
My problem is, to have the absolute guarantee of compiletime evaluation, I can't do this:

// various definitions for more specific move types, directions

Bitboard get_pawns_able_to(Color color, MoveType move_type, Direction direction, Board *board) {
  switch (color) {
  case COLOR_WHITE: return get_white_pawns_able_to(move_type, direction, board);
  case COLOR_BLACK: return get_black_pawns_able_to(move_type, direction, board);
  }
}

I must instead make a lot of these:

Bitboard get_white_pawns_able_to_push_left(const Board *board) {
  Bitboard from = board->white;
  Bitboard to = board_empty(board);
  return from & bitboard_shift_southeast(to);
}

// . . . 63 more similar functions . . .

I often hear that the preprocessor should be avoided unless it does something C cannot do.
I never understood this sentiment, as C is capable of doing everything on its own.
Is this a good use case for the C preprocessor?

I do understand why some people may be averse to the idea:

  • No type checking
  • Token concatenation is scary
  • Less readable, albeit smaller

I would argue this approach, simply due to its less tedious nature, minimizes the number of difficult to spot errors I have the potential to make

twilit saffronBOT
#

When your question is answered use !solved or the button below to mark the question as resolved.

Remember to ask specific questions, provide necessary details, and reduce your question to its simplest form. For tips on how to ask a good question use !howto ask.

knotty musk
#

Current implementation for reference```c
#define GET_PAWNS_ABLE_TO(color, move_type, relative_direction, absolute_direction, to)
Bitboard get_##color##pawns_able_to##move_type####relative_direction
(const Board *board)
{ return board->color & bitboard_shift
##absolute_direction(to); }
Bitboard get_masked_##color##pawns_able_to##move_type####relative_direction
(const Board *board, const Bitboard from_mask, const Bitboard to_mask)
{ return board->color & from_mask & bitboard_shift
##absolute_direction(to & to_mask); }

#define GET_PAWNS_ABLE_TO_PUSH(color, absolute_anti_left, absolute_anti_center, absolute_anti_right)
GET_PAWNS_ABLE_TO(color, push, left, absolute_anti_left, board_empty(board))
GET_PAWNS_ABLE_TO(color, push, center, absolute_anti_center, board_empty(board))
GET_PAWNS_ABLE_TO(color, push, right, absolute_anti_right, board_empty(board))

#define GET_PAWNS_ABLE_TO_CAPTURE(color, enemy_color, absolute_anti_left, absolute_anti_center, absolute_anti_right)
GET_PAWNS_ABLE_TO(color, capture, left, absolute_anti_left, board->enemy_color)
GET_PAWNS_ABLE_TO(color, capture, right, absolute_anti_right, board->enemy_color)

#define GET_PAWNS_ABLE_TO_MOVE(color, absolute_anti_left, absolute_anti_center, absolute_anti_right)
GET_PAWNS_ABLE_TO(color, move, left, absolute_anti_left, ~board->color)
GET_PAWNS_ABLE_TO(color, move, center, absolute_anti_center, ~board->color)
GET_PAWNS_ABLE_TO(color, move, right, absolute_anti_right, ~board->color)

GET_PAWNS_ABLE_TO_PUSH(white, southeast, south, southwest);
GET_PAWNS_ABLE_TO_CAPTURE(white, black, southeast, south, southwest)
GET_PAWNS_ABLE_TO_MOVE(white, southeast, south, southwest)
GET_PAWNS_ABLE_TO_PUSH(black, northwest, north, northeast)
GET_PAWNS_ABLE_TO_CAPTURE(black, white, northwest, north, northeast)
GET_PAWNS_ABLE_TO_MOVE(black, northwest, north, northeast)

#undef GET_PAWNS_ABLE_TO_MOVE
#undef GET_PAWNS_ABLE_TO_CAPTURE
#undef GET_PAWNS_ABLE_TO_PUSH
#undef GET_PAWNS_ABLE_TO```

#

Allows me to associate each (color, relative direction) pairing to an absolute direction a handful of times instead of 64 times, seems significantly more manageable

#

Is this what they've been gatekeeping

#

And when I say C++ solves this with template metaprogramming```cpp
template<Color C, MoveType M, Direction D>
Bitboard get_pawns_able_to(const Board *board) {
if constexpr (C == COLOR_WHITE) {
return get_white_pawns_able_to<M, D>(board);
} else if constexpr (C == COLOR_BLACK) {
return get_black_pawns_able_to<M, D>(board);
}

std::unreachable();
}

This also makes the definition more clear, as all resulting functions are expressed as instantiations of the template and not associated with whole new identifiers

Though, (((technically))) `if constexpr` does not guarantee compiletime evaluation either, I think most compilers would do it
knotty wren
#

I'm curious why you're not using C++ for this 😛

#

If you have to use C, I don't see an issue with macros. The only question is how clean you can make them

small breach
#

I never understood this sentiment, as C is capable of doing everything on its own.

I think (and even more in 2026 with AI generation power and IDE auto-complete feature) that the tiny piece of time you gain writing the code using a macro, is not worth it

C is not "capable of everything": there are things C cannot do properly, like handling _Generic and sizeof. those are typical reasons you might often opt for macros to fake polymorphic functions or macros to have array_size utility. there are also things C cannot do related to integer constant and array sizes

#

imo, in your case, the main "issue" is that you're using a design fundamentally based on a union type (the color, the direction, ...)

a function that receives a union, is exponentially complex. this follows from the rule (in functional programming):

A^(B + C) = A^B * A^C

that is, a function from the union of B and C, valued in A, is equivalent to the encoding of two functions, one B->A and one C->A. which explains why you actually blow up your complexity

I'm not into the problem enough, but there are various other techniques you could try to use, like keeping lookup tables (if some outcomes only vary based on combinations, you could encode those outcomes in a table and have a time/space trade-off kind of approach). you can also start by organising your data in structure and have the structure hold function pointers, which looks more like a "dynamic" way of doing

knotty wren
knotty musk
#

I’m not so much concerned with the time writing as I am with ensuring everything is in agreement. With the quantity of functions I’m adding, it’d be very easy to accidentally put a “northwest” where I need a “northeast” and not notice for a while

#

The macro serves primarily to enforce uniformity, secondarily to reduce code size

#

If I wasn’t concerned with overhead, the if-else or the function pointer approach would work, but this is all in an effort to minimize the overhead (these methods appear in a sort of two-player back-and-forth search tree, and save me from having to do several “if” checks to see which player is currently moving, which should be implicitly known at compile time if the first player to move is known)

knotty musk
small breach
knotty musk
#

I feel like that is sort of a saving grace in my case, this is intended to be a one-shot macro that doesn’t get exposed in a header or anything

small breach
#

yeah I think your 64 different cases naturally arise from the complexity involved by unions/enumerations

#

so if you really love that design, I guess now macros are the only solution to your issue

knotty musk
#

Shame they have to take up all those identifiers, I do find the C++ template much nicer in that regard

#

I’ve gotta make the identifiers really long and descriptive and encapsulate them well to minimize their impact on the namespace

knotty musk
# knotty wren If you have to use C, I don't see an issue with macros. The only question is how...

That being said are there any guidelines on the cleanliness of implementation? I had a project that passed the arguments through macro definitions instead of parameters, kinda like this```c
#undef OWN_COLOR
#undef ENEMY_COLOR
#undef ANTI_LEFT
#undef ANTI_CENTER
#under ANTI_RIGHT

#define OWN_COLOR white
#define ENEMY_COLOR
#define ANTI_LEFT southeast
#define ANTI_CENTER south
#define ANTI_RIGHT southwest

#include "get_pawns_able_to.h"

small breach
#

I already saw that style, somewhere. not sure it has a name but it looks known

woven wave
small breach
#

(that's not very different from asking a LLM to do it)

knotty musk
#

Certainly more trustworthy than an LLM

#

If I had to choose between code generated with clear instructions and code generated with forward pass through nondeterministic black box I’d choose the former