Skip to content

Replace include guards with #pragma once#1703

Merged
mossmann merged 2 commits intogreatscottgadgets:mainfrom
antoinevg:antoinevg/pragma-once
Apr 6, 2026
Merged

Replace include guards with #pragma once#1703
mossmann merged 2 commits intogreatscottgadgets:mainfrom
antoinevg:antoinevg/pragma-once

Conversation

@antoinevg
Copy link
Copy Markdown
Member

@antoinevg antoinevg commented Mar 20, 2026

Depends on #1701


This PR removes include guards in the style of:

#ifndef __SOME_FILE_H__
#define __SOME_FILE_H__

...

#endif // __SOME_FILE_H__

…and replaces them with:

#pragma once 

...

@antoinevg antoinevg force-pushed the antoinevg/pragma-once branch 3 times, most recently from 5d87596 to 7349f09 Compare March 23, 2026 12:13
@antoinevg antoinevg force-pushed the antoinevg/pragma-once branch from 7349f09 to 000d2e8 Compare March 24, 2026 07:43
@mossmann mossmann self-requested a review March 24, 2026 14:05
@antoinevg antoinevg force-pushed the antoinevg/pragma-once branch 5 times, most recently from 564d8f5 to 66172a9 Compare March 30, 2026 08:40
@gullradriel
Copy link
Copy Markdown
Contributor

#pragma once tells the compiler "only include this file once", but the compiler has to figure out what "this file" means. It typically does that using the file's path or inode.
If the same header is reachable via two paths (a symlink, a bind mount, a copied include directory), the compiler may see them as two different files and include it twice, defeating the whole point.
Header guards don't have this problem because they work purely in the preprocessor's token space, the guard macro is either defined or it isn't, regardless of how you got to the file.
I would call it an unnecessary AI code bad move.

@mossmann mossmann requested a review from martinling March 30, 2026 16:24
@martinling
Copy link
Copy Markdown
Member

martinling commented Mar 30, 2026

@gullradriel

If the same header is reachable via two paths (a symlink, a bind mount, a copied include directory), the compiler may see them as two different files and include it twice, defeating the whole point.

If you have two headers with the same name, in directories that are both in the compiler's list of include paths, like
inc/a/api.h and inc/b/api.h, with -Iinc/a -Iinc/b passed to the compiler; then #include <api.h> is only ever going to include a/api.h, because that's the api.h found first in the search path. It won't include both, regardless of whether #pragma once or traditional header guards are used.

To include both api.h headers, you'd have to make it possible to distinguish them by the paths used in the #include statements. E.g. pass -Iinc to the compiler, and then write:

#include <a/api.h>
#include <b/api.h>

In which case, it's pretty clear that you're actually intentionally including both headers.

Header guards don't have this problem because they work purely in the preprocessor's token space

That's actually exactly the problem with them. Because there's no guaranteed way to uniquely name them, it's quite likely that if a and b are unrelated projects, both a/api.h and b/api.h might choose the same guard name, such as API_H. The more generic the name of the header, the more likely this is to happen.

In fact, this is exactly the scenario that led me to ask @antoinevg for this change. I was working on our Portapack firmware project that @mossmann mentioned in portapack-mayhem/mayhem-firmware#2957 (comment), and I ended up needing to include both gpio.h from the HackRF source, and gpio.hpp from the PortaPack source. Both of them used the same guard name, __GPIO_H__, making it impossible to use both at once. At that point the only option was to modify one of the projects to prevent the clash.

This is where #pragma once is a better solution, precisely because it doesn't depend on choosing unique names. It also avoids a lot of other mistakes that are common with traditional guards, like:

  • clashes with system headers (officially, all identifiers starting with __ are reserved to the C implementation!)
  • inconsistency about how to name guards within the project (e.g. we have a mix of __NAME_H and __NAME_H__)
  • forgetting to change a guard name when renaming a file, or copying code from one file to another
  • mismatching the #ifndef and the #define, (as I fixed recently in our libopencm3 submodule).

The main issue with #pragma once was that it wasn't always supported by all compilers, but that hasn't been the case for a very long time now - it's been in GCC since 3.4 in 2004, in clang from the start, and in everything else for ages too by now.

@antoinevg antoinevg force-pushed the antoinevg/pragma-once branch from 66172a9 to 5a4ddfb Compare March 31, 2026 09:01
Copy link
Copy Markdown
Member

@mossmann mossmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent! Thank you!

Comment thread firmware/common/delay.h
void delay_us_at_mhz(uint32_t us, uint32_t mhz);

#endif /* __DELAY_H */
#ifdef __cplusplus
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catch

@gullradriel
Copy link
Copy Markdown
Contributor

@gullradriel

If the same header is reachable via two paths (a symlink, a bind mount, a copied include directory), the compiler may see them as two different files and include it twice, defeating the whole point.

If you have two headers with the same name, in directories that are both in the compiler's list of include paths, like inc/a/api.h and inc/b/api.h, with -Iinc/a -Iinc/b passed to the compiler; then #include <api.h> is only ever going to include a/api.h, because that's the api.h found first in the search path. It won't include both, regardless of whether #pragma once or traditional header guards are used.

To include both api.h headers, you'd have to make it possible to distinguish them by the paths used in the #include statements. E.g. pass -Iinc to the compiler, and then write:

#include <a/api.h>
#include <b/api.h>

In which case, it's pretty clear that you're actually intentionally including both headers.

Header guards don't have this problem because they work purely in the preprocessor's token space

That's actually exactly the problem with them. Because there's no guaranteed way to uniquely name them, it's quite likely that if a and b are unrelated projects, both a/api.h and b/api.h might choose the same guard name, such as API_H. The more generic the name of the header, the more likely this is to happen.

In fact, this is exactly the scenario that led me to ask @antoinevg for this change. I was working on our Portapack firmware project that @mossmann mentioned in portapack-mayhem/mayhem-firmware#2957 (comment), and I ended up needing to include both gpio.h from the HackRF source, and gpio.hpp from the PortaPack source. Both of them used the same guard name, __GPIO_H__, making it impossible to use both at once. At that point the only option was to modify one of the projects to prevent the clash.

This is where #pragma once is a better solution, precisely because it doesn't depend on choosing unique names. It also avoids a lot of other mistakes that are common with traditional guards, like:

  • clashes with system headers (officially, all identifiers starting with __ are reserved to the C implementation!)
  • inconsistency about how to name guards within the project (e.g. we have a mix of __NAME_H and __NAME_H__)
  • forgetting to change a guard name when renaming a file, or copying code from one file to another
  • mismatching the #ifndef and the #define, (as I fixed recently in our libopencm3 submodule).

The main issue with #pragma once was that it wasn't always supported by all compilers, but that hasn't been the case for a very long time now - it's been in GCC since 3.4 in 2004, in clang from the start, and in everything else for ages too by now.

No offenses intended. But, from the very same article you are mentioning, carefully read the caveats section. Maybe my case is not everyone's case, I'm working on legacies code bases on servers, with lots of symlinks and source files copies. I did not choose that, I inherited that and fix as I can. And pragma got our asses more than one time. I guess for hackRF code it's ok, but it's not the best move ever, from my point of view. I'm surely getting old, at least for code opinions and view points....

Copy link
Copy Markdown
Member

@martinling martinling left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is missing the same change in the recently-added da7219.h.

@antoinevg antoinevg closed this Apr 2, 2026
@antoinevg antoinevg deleted the antoinevg/pragma-once branch April 2, 2026 07:14
@antoinevg antoinevg restored the antoinevg/pragma-once branch April 2, 2026 07:30
@antoinevg antoinevg reopened this Apr 2, 2026
@antoinevg antoinevg force-pushed the antoinevg/pragma-once branch from 5a4ddfb to 976f91d Compare April 2, 2026 07:33
@antoinevg
Copy link
Copy Markdown
Member Author

This is missing the same change in the recently-added da7219.h.

Whoopsie tx! Fixed and force-pushed.

@antoinevg antoinevg requested a review from martinling April 2, 2026 07:34
@antoinevg antoinevg force-pushed the antoinevg/pragma-once branch from 976f91d to 5f8601b Compare April 2, 2026 07:37
@martinling
Copy link
Copy Markdown
Member

No offenses intended.

None taken!

But, from the very same article you are mentioning, carefully read the caveats section. Maybe my case is not everyone's case, I'm working on legacies code bases on servers, with lots of symlinks and source files copies. I did not choose that, I inherited that and fix as I can. And pragma got our asses more than one time. I guess for hackRF code it's ok, but it's not the best move ever, from my point of view.

I have read that, and the claim that #pragma once "can do the wrong thing" is marked with [citation needed]

Are you able to describe a specific scenario that may cause a problem - with compilers from the last 20 years?

I've been doing a bit of research about this, because it's been feeling to me like "#pragma once is unreliable" might have become one of those bits of folk wisdom that us older folks remember, but for reasons that are no longer quite so clear.

What I've found is that support for it was first added to GCC in 2001 in the venerable 2.95 release, but with a buggy implementation which would sometimes get confused and include things twice. In 2003, it was deprecated and slated for removal, both due to that bug, and the fact it was not yet standardised.

However, the implementation was then fixed in 2004, released in version 3.4 and the deprecation removed.

I've yet to find anything definitive that suggests it's been an actual problem since!

@mossmann mossmann merged commit d1b3739 into greatscottgadgets:main Apr 6, 2026
41 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants