1. 18
  1. 10

    I remember in university someone had a great example of why macros can be error prone. I searched the net but couldn’t find it to link so here it is from memory (forgive me I don’t program C the syntax is probably way off):

    #define SIX 1+5
    #define NINE 8+1
    printf("What do you get if you multiply %d by %d ? %d", SIX, NINE, SIX*NINE);
    

    It made me laugh.

    1. 5

      Please do not advise using macros for constants or functions.

      For constants,

      Unlike variables, you can use macros in contexts where you need a “constant expression,” like array lengths or switch statement cases.

      For these cases, please use enum.

      Having constants actually be “constant expressions” is very useful and hence they should usually be defined as macros.

      Please don’t. Your compiler is smarter than you and a simple static const is enough.

      For functions,

      The code is pasted right in the surrounding code, instead of compiling function call instructions. This can make code faster, as function calls have some overhead.

      No. The compiler is smarter than you.

      They can be type-generic. For example, x + y is valid syntax for any numeric type. If we made that a function, we’d have to declare them as arguments and choose their type, i.e. size and signedness, in advance, which would make it only usable in some contexts.

      Please define functions with specific types, and use _Generic to make a generic macro that uses actually defined functions.

      1. 4

        Lots of this advice has aged poorly, because it was passed down without context. Now people read it the way they read advice on Rust or Python, unaware of two major caveats that frequently degrade its quality:

        • That there isn’t a single authoritative implementation of (generally) reasonable quality that works across the architectures that the single authoritative upstream considers important and is able to support.
        • That part of C’s cancerous spread success laid in its ability to usefully leak quirks of the underlying hardware in a way that enabled you to (mostly) use it as a portable assembler for the boring parts and a non-portable assembler for the interesting parts.

        So lots of C optimization advice floating around the Internet, including e.g. the one that the author quotes about macros vs. inline functions and constants, is mostly about handholding ancient or (even contemporary, sadly) proprietary compilers.

        There’s surprisingly little C optimization advice that’s correct everywhere. Every generation discovers this. Lots of my “grasping C” process consisted of unlearning optimization advice (including “no, the compiler is smarter than you”) that I’d absorbed without realizing it was mostly specific to Watcom and x86, and Watcom was not a bad compiler.

        If anyone’s still learning C today, my advice would be to focus more on understanding common compiler idioms and platform-specific optimizations that compilers use (or not!) and less on “optimization tips”. After the initial adjusting period, reading assembly outputs is usually more productive than reading language tips.

        1. 3

          Well, if the programmer suggests using macros for optimization purposes, the answer is always “no, the compiler is smarter than you.” If the programmer suggests looking at assembly for optimization purposes, the answer is always “the compiler makes dumb decisions.”

        2. 1

          The advice around const is also misleading. You can cast from const T to T, but doing so must be explicit. Some standard library functions must do this (e.g. memchr). It’s also worth noting that, for pointer and array types, it doesn’t mean that the object is immutable, it just means that you promise not to mutate it through this pointer. In the presence of aliasing, the object may still be mutated between two reads through a const pointer.

          1. 1

            Please don’t. Your compiler is smarter than you and a simple static const is enough.

            Not here it isn’t:

            static const int foo = 4;
            // ...
            const uint *arr[foo]; // foo is not a constant
            

            That would work in C++, but not in C. Not in C99 at least.

            1. 2

              I think the way /u/jxy would write that is like this (“For these cases, please use enum.”):

              enum { foo = 4 };
              const int *arr[foo];
              

              https://godbolt.org/z/MT1Eobxfh

              1. 4

                The Apple system headers use this idiom a lot. I had never come across the idea of an unnamed enumeration in C until I read some of them, but they seem to be a good way of getting constants into headers. They also play a lot nicer with FFI tooling than using defines if they’re an expression. Consider the following two lines:

                #define BIT4 (1<<4)
                enum { Bit4 = 1<<4 };
                

                If your FFI uses libclang to parse the header then it can extract the value of Bit4 with a single call, independent of how complex the expression is, because clang can evaluate things that the C specification says are integer constant expressions and exposes this with the clang_getEnumConstantDeclValue function. In contrast, the first requires that you identify that this is a macro that doesn’t take any arguments, then parse it to determine that it doesn’t refer to any external variables, and then evaluate it. In this case, it’s quite simple, but consider if it’s some dependent value:

                #define BIT4 (1<<4)
                #define NEXT_BIT (1<<((sizeof(int)*CHAR_BIT) - __builtin_clz(BIT4)))
                enum
                {
                        Bit4 = 1<<4,
                        NextBit = (1<<((sizeof(int)*CHAR_BIT) - __builtin_clz(Bit4)))
                };
                

                The second one will be parsed by libclang and exposed in the AST. Indeed, if you compile this with -Xclang -ast-dump, then you will see that clang has evaluated the enum constants while creating the AST. This has one additional benefit: when I first wrote this, I missed some brackets and so got a warning from the enum version, whereas the warning would appear on each use with the macro version.

                Now consider trying to evaluate NEXT_BIT for FFI. It depends on the sizeof operator, fortunately on a type and not an expression, but it also depends on a compiler builtin and on two symbols CHAR_BIT and BIT4 that come from the other macros. These are also preprocessor macros and so you first need to evaluate them (fortunately, CHAR_BIT, at least, is a single integer). This introduces a lot of work into the FFI binding generator and a lot of things will correctly handle simple cases and then suddenly fail on more complex ones. The __builtin_clz case is particularly interesting because most will not handle it correctly, yet C code that uses it will compile correctly with clang or gcc.

            2. 1

              For constants,

              Unlike variables, you can use macros in contexts where you need a “constant expression,” like array lengths or switch statement cases.

              For these cases, please use enum.

              I read the same argument in Kernighan’s and Pike’ Practice of Programming, and I get the elegance of it, besides what @david_chisnall already told us.

              But besides it being a C construct, not a pre-processor one, and making it show in the results of ctags(1) (the latter being already an advantage, in my perspective), what is the real technical explanation for this advice? Is it solely these two, or does it improve optimisation passes?

              1. 2

                It’s mostly the same to any C compiler, except that the compiler should error if you define an enum twice, but would only warn if you define a macro twice. For programmers, it saves you from people laughing at your SIX*NINE.

            3. 2

              The advice about -Werror needs qualifications. You should not set -Werror in source releases, because that means a compiler update can cause a build failure, even if your code is bug-free and valid C, if a new warning has been added about a hazardous pattern. However, it is good practice to keep it on while developing.

              In addition to -Wall there’s -Wextra (and -pedantic) which add more warnings. I personally build with all three during development.

              Also, you should not use my_type_t style type names, since everything ending with _t is part of reserved namespace for implementations.

              1. 1

                There are issues with using -Werror though. Take this warning, for example:

                warning: ISO C forbids assignment between function pointer and `void *'
                

                As stated, standard C forbids this, but POSIX requires this. To remove this warning, I need to remove -pedantic. If I had a way to tell the C compiler that I want POSIX semantics, them maybe I would use -Werror -pedantic in development. I also use -Wall -Wextra -pedantic for development.