1

As I understand, when compiling a compilation unit, the compiler's preprocessor translates #include directives by expanding the contents of the header file1 specified between the < and > (or ") tokens into the current compilation unit.

It is also my understanding, that most compilers support the #pragma once directive guarding against multiply defined symbols as a result of multiple inclusion of the same header. The same effect can be produced by following the include guard idiom.

My question is two-fold:

  1. Is it legal for a compiler to completely ignore an #include directive if it has previously encountered a #pragma once directive or include guard pattern in this header?
  2. Specifically with Microsoft' compiler is there any difference in this regard whether a header contains a #pragma once directive or an include guard pattern? The documentation suggests that they are handled the same, though some user feels very strongly that I am wrong, so I am confused and want clarification.

1 I'm glossing over the fact, that headers need not necessarily be files altogether.

IInspectable
  • 46,945
  • 8
  • 85
  • 181
  • I seem to remember something about Clang storing whether a header uses an applicable include guard for this purpose, but I could be wrong. – chris Jun 13 '21 at 07:29
  • I'm pretty sure that *all* major compilers recognize include guard patterns, and handle them the same as `#pragma once`. The question is whether this is legal, and whether MSVC makes a difference. – IInspectable Jun 13 '21 at 07:36
  • A potential issue with `#pragma once` is it uses the file path as the key for the file. On some systems it is possible to have multiple (none relative) paths to the same file. I am not sure how this is handled. – Richard Critten Jun 13 '21 at 07:43
  • @ric `#pragma once` is non-standard. Any compiler that implements it will do the best it can to identify the "file" (note, that it need not even be a file). Regardless, the question isn't how difficult it is to implement that optimization. The question is whether that optimization is strictly legal, and when MSVC uses it. – IInspectable Jun 13 '21 at 07:46
  • @RichardCritten I believe there is a unique absolute path. There could be a bunch of pathes via symbolic links or relative paths but absolute path is unique. – ALX23z Jun 13 '21 at 07:46
  • @IInspectable How can we judge legality if `#pragma once` is non-standard? Any behavior is "legal". Besides, it's impossible to distinguish between *"compiler skipped `#include`"* and *"compiler processed `#include`, but discarded file contents because of `#pragma once`"*. – HolyBlackCat Jun 13 '21 at 07:50
  • 1
    Regarding (2), since that user didn't provide any proof or explanation, I would just assume them to be wrong. – HolyBlackCat Jun 13 '21 at 07:53
  • @RichardCritten on such systems, the compiler may not distinguish between "two different paths to the same file" and "two different files with the same content". This precludes the optimisation, but the net result is the same with or without the optimisation. – n. m. could be an AI Jun 13 '21 at 08:04
  • Does the "as-if" come into play here if we can't tell the difference? – Richard Critten Jun 13 '21 at 08:05
  • 1
    @ALX23z The absolute path is not unique. There are also hard links. Any two hard links to the same file are equivalent, none of them is "primary" or "canonical" or anything. – n. m. could be an AI Jun 13 '21 at 08:06
  • @RichardCritten sure it does. – n. m. could be an AI Jun 13 '21 at 08:07
  • @RichardCritten I think what you might talk about is if you have something like `#include "someheader.h"` or `#include "../someheader.h"` where the compiler first looks in the local directory to resolve the name, then you could have (throughout the complete include tree) multiple files with the same name (and content), and then `#pragma once` could fail. But honestly, then something else is also wrong in the project. – t.niese Jun 13 '21 at 08:25
  • @t.niese in complex projects using simlinks (or hard links) it is possible to include the same file more than once (by accident) with different absolute paths. – Richard Critten Jun 13 '21 at 08:28
  • @hol A feature need not be standard to answer the question whether implementing said feature is legal. – IInspectable Jun 13 '21 at 08:30
  • 1
    The C++ standard specifically permits any behaviour of the `#pragma` directive, as long as it is documented. So any feature implemented with a `#pragma` is legal if you can read about it in the user guide. It is legal to implement `#pragma once` that does nothing or that blows up your computer, the compiler writer just needs to document it. – n. m. could be an AI Jun 13 '21 at 08:34
  • @RichardCrittenWell yes. But then you can also other problems. Let's say you have two versions of one library, both with definitions in the header, with different implementations. One compilation unit includes the header of the one version, on the compilation unit the header of the other one, then the final linked project encounters undefined behavior. Neither `#pragma once` nor include guards protect you against that, either. So `in complex projects using simlinks (or hard links) it is possible to include the same file more than once` can lead to problems either way in future. – t.niese Jun 13 '21 at 08:34
  • `#pragma once` is not defined by the C++ standard at all. `#pragma`s, according to the standard, have implementation-defined meaning, and `#pragma once` is no different. So, by the letter of the standard, an implementation (aka compiler, library, etc) may do anything it likes when it sees `#pragma once` as long as that behaviour is documented (in documentation for that implementation). There is certainly no requirement that two compilers do the same thing with `#pragma once`. – Peter Jun 13 '21 at 09:54
  • The question asks for `#pragma once` **as well as** include guards. Not a single comment nor proposed answer acknowledges that. – IInspectable Jun 13 '21 at 10:01

5 Answers5

3

It the compiled program cannot tell whether the compiler has ignored a header file or not, it is legal under the as-if rule to either ignore or not ignore it.

If ignoring a file results in a program that has observable behaviour different from a program produced by processing all files normally, or ignoring a file results in an invalid program whereas processing it normally does not, then it is not legal to ignore such file. Doing so is a compiler bug.

Compiler writers seem to be confident that ignoring a once-seen file that has proper include guards in place can have no effect on the resulting program, otherwise compilers would not be doing this optimisation. It is possible that they are all wrong though, and there is a counterexample that no one has found to date. It is also possible that non-existence of such counterexample is a theorem that no one has bothered to prove, as it seems intuitively obvious.

n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243
2

I think you can treat #pragma once as compiler language extension like for instance #pragma omp parallel that can make a loop execute in parallel causing all kinds of UB if it is not written correctly.

The standard says it is ok for pragma directive to cause implementation-defined non-conforming result:

Pragma directive [cpp.pragma] ... causes the implementation to behave in an implementation-defined manner. The behavior might cause translation to fail or cause the translator or the resulting program to behave in a non-conforming manner. Any pragma that is not recognized by the implementation is ignored.

Regarding MSVC behavior you can think of it skipping the header based on its normalized path.For instance you can trick the compiler with symlinks:

test/test.h

#pragma once

static int x = 2;

Create symlink "test-link" to "test" directory:

mklink /d test-link test

Then in main.cpp:

#include "test/test.h"
#include "test/test.h"
#include "test/../test/test.h"

is ok. but

#include "test/test.h"
#include "test-link/test.h"

causes

error C2374: 'x': redefinition; multiple initialization

which would not happen in case of include guards.

dewaffled
  • 2,850
  • 2
  • 17
  • 30
  • That's not addressing the question. The question is whether it is legal to completely skip a header, by way of an include guard or otherwise. – IInspectable Jun 13 '21 at 09:05
  • Any implementation-defined behavior is legal when pragma is used. I have added an example demonstrating you can think of MSVC behavior as of skipping the header. – dewaffled Jun 13 '21 at 09:17
  • The question is also asking for the include guard pattern. – IInspectable Jun 13 '21 at 10:02
  • The guard pattern is just a pattern and you have sited the required behavior, so technically it is not legal. But the compiler can possibly have such optimizations if the observable behavior does not change. Even without such include guard optimizations the compiler for instance not required to re-read included file from the disk every time it is included but can cache it in memory. So you cannot do crazy things like modifying the header after it has been included and expect the modified version to be picked on next include (even without any include guards) – dewaffled Jun 13 '21 at 10:36
  • Why would it be technically illegal? Which part of the language specification is this optimization in conflict with? – IInspectable Jun 13 '21 at 10:42
  • You wrote it yourself in the question: the include directive is required to be replaced by the content of the included file. If it is sometimes not and the observed result is the same then the optimization is ok, but the result must be guaranteed to be exactly the same. – dewaffled Jun 13 '21 at 11:27
  • A language implementation is a giant application of the as-if rule. I'm specifically looking for language rules that would make this particular optimization illegal. This just reads like guess-work (*"it is illegal, but it could be ok"*). That's not really useful. – IInspectable Jun 13 '21 at 11:45
2
  1. Is it legal for a compiler to completely ignore an #include directive if it has previously encountered a #pragma once directive or include guard pattern in this header?

That depends on how #pramga once is defined and implemented by the compilers. It is after all a none standard feature.

But, all compilers I know that support #pramga once treat it like a non-mutable unique include guard that wraps around the complete file.

After the preprocessor resolved the include path for an include, it can check if that file was already included and if #pargma once exists for that file. If both conditions are true, it is safe to not include the file anymore, because it would follow the as-if rule, as the compiler vendor is in full controller over how the #pramga once is implemented and can ensure that the lock guard is unique, non-mutable, and wraps the whole file, and due to that a repeated inclusion of that same wile would result in an empty content that is included.

So with that respect, if they didn't make an implementation error it is safe to then ignore the include.

There is the argument against the usage of #pragma once that says that the compiler might treat the same file as different files due to symlinks and hard links. That would result in accidentally including the same file multiple times, but that won't affect the part of whether it is safe to ignore it if the compile identified it as the same file.

  1. Specifically with Microsoft' compiler is there any difference in this regard whether a header contains a #pragma once directive or an include guard pattern? The documentation suggests that they are handled the same, though some user feels very strongly that I am wrong, so I am confused and want clarification.

If no #pragma once is used it becomes more complicated. The preprocessor needs to first check if the lock guard wraps around all contents:

#ifndef SOME_GUARD_NAME_H
#define SOME_GUARD_NAME_H
// all content of the file
#endif

Or if it is something like this:

// some content before the guard
#ifndef SOME_GUARD_NAME_H
#define SOME_GUARD_NAME_H
// some content
#else
// some more content
#endif
// some other content after the guard

And it needs to keep track of whether the SOME_GUARD_NAME_H was already defined in another file or if #undef was called by another file.

So in that case it can only ignore the content of the file if it can ensure that all relevant defines are the same and/or if the evaluation of the macros results in an empty file.

Spectric
  • 30,714
  • 6
  • 20
  • 43
t.niese
  • 39,256
  • 9
  • 74
  • 101
  • very interesting that how i view now in test - `#ifndef SOME_GUARD_NAME_H` and `#if !defined(SOME_GUARD_NAME_H)` have different effect – RbMm Jun 13 '21 at 09:55
1

Is it legal for a compiler to completely ignore an #include directive if it has previously encountered a #pragma once directive or include guard pattern in this header?

Of course it is! It is even legal for the compiler to ignore all your source files and header files so long as behavior of the generated code is the same as if it processed everything. That's how pre-compiled headers and object files work - anything that hasn't changed can be safely ignored. Similarly, if the compiler can prove that including and not including the file are going to have exactly the same behavior, the compiler may ignore the file, regardless of the pre-processor directives.

Specifically with Microsoft' compiler is there any difference in this regard whether a header contains a #pragma once directive or an include guard pattern?

The documentation is pretty clear on that. They are identical assuming the compiler manages to identify the idiom and you haven't #undefed the macro. I've never experienced any bugs related to that either. #pragma once is safer though. I have had an instance where two headers had the same include guard and debugging that wasn't a nice experience.

Aykhan Hagverdili
  • 28,141
  • 6
  • 41
  • 93
-1

#pragma once obviously refers to the file as a whole

The use of #pragma once can reduce build times, as the compiler won't open and read the file again after the first #include of the file in the translation unit.

really - if not to file - for what it can be related ?

the conditional compilation , so called guard idiom related not to file but to block of code. really - where, how stated that some condition related to file ?! it related to block beginning with #if* and ended with #endif. compiler anyway need include this file again.

let do some tests. also here will be very useful cl(msvc) compiler option /showIncludes

let create header.h

// header.h
#ifndef HEADER_H_ 
#define HEADER_H_
int g_a = 0;
#endif 

and then

#include "header.h"
#include "header.h"

only once in log

1>Note: including file: .\header.h

so header.h really included only once here.

but if do this

// header.h 
#if !defined HEADER_H_
#define HEADER_H_
int g_a = 0;
#endif 

or this

#if !defined(HEADER_H_)
#define HEADER_H_
int g_a = 0;
#endif 

and

#include "header.h"
#include "header.h"

already 2 lines in log - header.h included 2 time.

1>Note: including file: .\header.h
1>Note: including file: .\header.h

so #ifndef HEADER_H_ have different effect compare #if !defined(HEADER_H_)

or if do

// header.h
#ifndef HEADER_H_ 
#define HEADER_H_
int g_a = 0;
#endif 
#define XYZ

or

// header.h
#if __LINE__ // any not empty statement
#endif

#ifndef HEADER_H_ 
#define HEADER_H_
int g_a = 0;
#endif 

and

#include "header.h"
#include "header.h"

already

1>Note: including file: .\header.h
1>Note: including file: .\header.h

again 2 lines in log - header.h included 2 time.

so if exist any not empty ( comments, sequences of whitespace characters (space, tab, new-line)) statement outside first conditional block - file already included more times.

of course possible and do next

// header.h
#include "header.h"
#undef HEADER_H_
#include "header.h"

in this case

1>Note: including file: .\header.h
1>Note: including file: .\header.h
1>.\header.h(4): error C2374: 'g_a': redefinition; multiple initialization
1>.\header.h(4): note: see declaration of g_a'

and of course in case

// header.h 
#pragma once
int g_b = 0;

and

#include "header.h"
#include "header.h"

only single line

1>Note: including file: .\header.h

so based on tests can make next conclusion - if cl(msvc) - view that file have pattern

#ifndef macro // but not #if !defined macro
#define macro
// all code only here
#endif

macro associates with the file and then as long as it is not undefined - the file will not be included more. this is implicit optimization by specific compiler. and very fragile. any not white space or comment statement break it. even despite documented that #ifndef HEADER_H_ quivalently to #if !defined HEADER_H_ - by fact this is not true.

RbMm
  • 31,280
  • 3
  • 35
  • 56