1. 11
  1. 1

    Any time I see #define (apart from a header guard), I worry. Preprocessor directives come across as a red flag when reading through a codebase. I don’t know how or why I picked this mentality up.

    1. 2

      It’s a good rule of thumb. I really liked this article: it did a great job of explaining the better alternatives. The big unavoidable one is for controlling anything that itself depends on the preprocessor. For example, guarding includes for specific platforms. In snmalloc, we use the preprocessor for three things:

      • Selecting which platform and architecture abstraction layers to compile in and make default. Each of these is in a separate file and we sometimes want to be able to use more than one in a given deployment (typically for testing) so we include them all, they all have guards to prevent them from being compiled on platforms that don’t have headers that they need, and then have another preprocessor set of cases to define which one is the default. From then on, everything using the PAL / AAL refers to it via the using alias for the default or via the explicit type name. It’s tricky to get rid of this unless you can specify optional files in a module with per-platform configuration.
      • Macros that can’t be functions, for example assertion macros that need the line number (std::source_location will make that go away at some point) or things like __attribute__((always_inline)) for fast paths.
      • Passing configuration variables in. There are a few configuration variables that we want people to be able to provide by #defineing things before including our headers or by passing -D things on the command line. We could provide a configuration object that has these as a template argument everywhere, but that gets a little bit painful to thread through. We do initialise constexpr variables with these values in a header, so most of the code is oblivious to the fact that these are macros.

      I’d love to C++ gain parameterisable modules, with the ability to provide a config object when importing a module that was then used to initialise some module-scope constexpr global and resolve constexpr if blocks. In C++, this is effectively equivalent to threading an extra template parameter (or set of template parameters) though everything that contains a constexpr if using the config object, so you could retrofit it on the existing name mangling support fairly easily. This would avoid problems such as LLVM’s ABI changing depending on whether NDEBUG is defined: the #ifndef NDEBUG blocks would be replaced with if constexpr (llvm_config.debug) {} blocks and any type that or function depended on them would be mangled to include a true or false template parameter, so you’d get link failures if you tried to link release-llvm with something compiled assuming release-llvm.