Hacker News new | past | comments | ask | show | jobs | submit login

If you want to debug preprocessor-generated code, -gdwarf-4 -g3 usually does the trick with GCC and Clang. If it doesn't, you can just gcc -E it and run it through clang-format or something, and compile and debug the expanded version. It's really not a big deal. You can have a Makefile rule to do this automatically, for example https://github.com/alpha123/yu/blob/master/Makefile#L70-L103

Saying the C preprocessor can "inject bugs" and make code that "can't be easily traced" is deceptive and wrong. It is, for all intents and purposes, a solved problem. That Makefile rule could be 3x shorter if I didn't go for some extra niceties like not expanding system headers. (As usual, 20% of the result takes 80% of the work). The preprocessor doesn't "inject bugs" anyway, unless you write very careless macros. Modern C, with things like __typeof, _Generic, and GCC/Clang's statement expressions means your macros can be totally safe against overwriting variables, double-evaluation, type errors, and more. You can, of course, still do silly things, but C isn't about protecting the programmer anyway, and that's totally fine if you're prepared for it.

Perhaps in C++ the preprocessor is ‘pure evil’, but in C it's a very useful part of the language.




I work in the embedded space and use a compiler from Green Hills.

The preprocessor in either C or C++ is evil for the simple matter you can't debug it when the thing you're debugging is in production, symbols are stripped, and the disassemble resembles nothing you can marry up to actual C or C++ code. In simple cases, maybe, but in not so simple cases it's just evil.

A modern compiler will optimized out the global consts anyways too...


If you have a modern compiler then it supports outputting the pre-processor result.

If you have a modern toolchain stack unwinding is already friendly to macro expansion.

A production build does not mean you don't have symbols, it just means you don't have symbols in the executable. Stash your build symbols somewhere so you can symbolize the stacks offline. Macros don't change this. They are consistent output for the same input.

Macros are not evil. Abusable, yes, but they also solve various sets of problems just better than any other solution.


Outputting the pre-processor result is not that helpful, you're still stuck with deciphering things.

macro expansion is useless, again, for same reason you can't marry it to actual code. If you can do this now, would be at most curious (not interested) as it's evil to begin with.

A production build in the embedded world, typically means you don't have symbols. Often they are stripped to save space and unless you have a USB stick or something you carry with you with map files, you're still toast.

And there are a whole host of other reasons why they are just pure evil:

1. Global scope only. 2. Unexpected results rather than error messages - ISO25259 won't even allow you to do this if that tells you anything. 3. Can't use sizeof 4. no type checking. Every compiler I've used, including GCC, won't warn about if an int is compared to unsigned. 5. can't take the address 6. worst, substituted value need not even be legal in the context the #define is created because it is evaluated at each point it is referenced allowing to another evil problem of being able to reference objects that are not declared yet.

I could go no... and I haven't even touched on strings yet.

with so many problems, I have a hard time justifying or championing their use.


> 1. Global scope only.

So are functions, by that rationale. If you want to control scope of variables, pass in macro parameters.

> 2. Unexpected results rather than error messages

When have min() or max() produced unexpected results rather than error messages? They're macros, you know.

> Every compiler I've used, including GCC, won't warn about if an int is compared to unsigned.

-Wconversion

> 5. can't take the address

You can't take the address of a struct definition or a typedef, either. Macros are not replacements for functions; they're code generators.

> 6. worst, substituted value need not even be legal in the context the #define is created because it is evaluated at each point it is referenced allowing to another evil problem of being able to reference objects that are not declared yet.

I don't even... You do know that, following the preprocessing phase, the compiler actually checks the generated code for legality, right? And since when is forward referencing evil?


The vast, vast majority of your "evil" list is either just wrong or unrelated. I don't think you really know what macros are or how/when to use them.


What does the comment about stack unwinding refer to?

I've always found macros pretty horrid for debugging because you can't step through them properly. I just tried with gcc 6.0 and gdb 7.11 and it still seems to be unsupported, 20-odd years after the first time I rued the lack of this functionality. At this rate I strongly suspect this will never, ever work. (Another black mark against macros that expand to anything significant, in my view.)


DWARF 3 and 4 can contain information about macros, and GDB can expand them (macro exp foo(bar,...)). Compile with the somewhat under-mentioned flags -gdwarf-4 -g3 to retain macro definitions. This lets you debug expansions easily, but doesn't map already-compiled macro expansions back to their macro definitions.

If you want to step through execution of a function defined by a macro, preprocess with -E -P and compile that. For more advanced (and pleasant) macro debugging, you can do a little trick:

1. Create an e.g. include/dummy directory and create empty versions of whatever headers you don't want to expand.

2. Run your files through $CC -E -P -Iinclude/dummy

3. clang-format is your friend for making the output much nicer.

4. Compile with explicit -include of the real versions of files in include/dummy.

5. Debug normally.

It takes a little bit of effort to get right, but after that you can just throw it in a Makefile and forget about it (e.g. https://github.com/alpha123/yu/blob/master/Makefile#L96-L126).


Can you give an example of a problem which the preprocessor solves better than modern C++?


The X-macro pattern comes to mind:

  #define LIST_ERROR_TYPES(X) \
    X(OK, "") \
    X(ERR_OOM, "out of memory") \
    X(ERR_BAD_STRING_ENCODING, "string was invalid utf-8") \
    ...

  #define DEF_ENUM(ident,_) ident,
  typedef enum { LIST_ERROR_TYPES(DEF_ENUM) } err_t;

  const char *get_err_msg(err_t err) {
  #define IDENT2MSG(ident,msg) case ident: return msg;
    switch (err) {
    LIST_ERROR_TYPES(IDENT2MSG)
    default: return "unknown error";
    }
  }
Which turns out to be useful for more than just error messages (which is still useful in C++ if you're avoiding exceptions). Recently, for example, I did something like that to map a small fixed set of object properties to database columns.

If you want to see some actual code, I use X-macros, as well as other macros, heavily in the test suite for my (very) WIP programming language: https://github.com/alpha123/yu/tree/master/test

There's some other interesting and generic macros that I frequently use in there too: https://github.com/alpha123/yu/blob/master/src/yu_common.h


I use preprocessor macros for what I guess I'll call "hyper-local extraction" in an attempt to be as DRY as possible. I don't know how modern C++ would handle this better. It looks a bit like this:

    #define a_ frob(-1,0,0); grob("a"); blah(...);
    #define b_ frob(1,0,0); grob("b"); blah(...);
    #define c_ frob(0,-1,0); grob("c"); blah(...);
    #define d_ frob(0,-1,1); grob("d"); blah(...);
    #define e_ frob(0,0,-1); grob("e"); blah(...);
    #define f_ frob(0,1,1); grob("f"); blah(...);
    
    if (normal_order) {
      a_ b_ c_ d_ e_ f_;
      c_ b_ a_ f_ e_ d_;
    } else if (special_order_1) {
      b_ c_ d_ e_ a_;
      d_ e_ c_ b_ a_ f_;
    } else if (...) {
      // ... and so on ...
    }
    
    #undef a_
    #undef b_
    #undef c_
    #undef d_
    #undef e_
    #undef f_


Modern C++ could use lambdas:

    auto a = [] {frob(-1,0,0); grob("a"); blah(...);};
    auto b = [] {frob(1,0,0); grob("b"); blah(...);};
    auto c = [] {frob(0,-1,0); grob("c"); blah(...);};
    ...

    if (normal_order) {
      a(); b(); c(); ...
    } else if (...) {
      b(); c(); a(); ...
    } ...


I guess that's pretty good.


Code generation, for example:

   #define DISALLOW_COPY_AND_ASSIGN(TypeName) \
     TypeName(const TypeName&) = delete;      \
     void operator=(const TypeName&) = delete
Or for things like compile-time injection of wrappers that you need to be completely gone in production versions. An example would be wrapping all OpenGL calls with glGetError() checks in debug builds.

Modern C++ did not replace macros nor did it even try. some uses of macros were made obsolete (like min/max), but most were not.


Bit off topic, but I also work in embedded (automotive) with Green Hills compiler & Multi2000 debugger. After using GCC and GDB, Green Hills tools are so much inferior that it has become the biggest issue on project.

On top of it all, they require constant(!) connection to license server and will throw annoying popups if being disconnected for few minutes. It's so frustrating to constantly being punished by DRM even if I'm paying customer.

Sorry for rant, I just needed to vent a bit.


At my last job we used the Green Hills MULTI toolchain as well. We had USB dongle licensing; no license server was needed, just the little USB stick.

And yeah, I hated using it too. Awful stuff. We went to great lengths to allow a testing version of the firmware to be built using Visual C++ and executed locally, just so we could avoid using the Green Hills stuff whenever possible.


oh, I so agree with you. Their tools do SUCK! I constant lose connectivity with their probes and their paid support is atrocious and they can't even answer simple questions about their own products. Horrible, horrible experience. If you can avoid Green Hills, take it from a 20 year veteran, you should do so!




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: