내 포함 가드가 재귀 적 포함 및 다중 기호 정의를 방지하지 않는 이유는 무엇입니까?
에 대한 두 가지 일반적인 질문 에는 경비원이 포함됩니다 .
첫 번째 질문 :
상호 재귀 적 포함 으로부터 헤더 파일을 보호하는 가드가 포함되지 않는 이유는 무엇 입니까? 다음과 같이 쓸 때마다 분명히 존재하지 않는 기호에 대한 오류가 발생하거나 심지어 이상한 구문 오류가 발생합니다.
"아"
#ifndef A_H #define A_H #include "b.h" ... #endif // A_H
"bh"
#ifndef B_H #define B_H #include "a.h" ... #endif // B_H
"main.cpp"
#include "a.h" int main() { ... }
"main.cpp"를 컴파일하는 중에 오류가 발생하는 이유는 무엇입니까? 문제를 해결하려면 어떻게해야합니까?
두 번째 질문 :
여러 정의를 방지 하는 가드를 포함하지 않는 이유는 무엇 입니까? 예를 들어, 내 프로젝트에 동일한 헤더를 포함하는 두 개의 파일이 포함되어있는 경우 링커가 일부 심볼이 여러 번 정의된다는 불만을 표시하는 경우가 있습니다. 예를 들면 :
"header.h"
#ifndef HEADER_H #define HEADER_H int f() { return 0; } #endif // HEADER_H
"source1.cpp"
#include "header.h" ...
"source2.cpp"
#include "header.h" ...
왜 이런 일이 발생합니까? 문제를 해결하려면 어떻게해야합니까?
첫 번째 질문 :
상호 재귀 적 포함 으로부터 헤더 파일을 보호하는 가드가 포함되지 않는 이유는 무엇 입니까?
그들은입니다 .
그들이 돕지 않는 것은 상호 포함 헤더의 데이터 구조 정의 간의 종속성 입니다. 이것이 의미하는 바를 알아보기 위해 기본 시나리오부터 시작하여 포함 경비원이 상호 포함에 도움이되는 이유를 살펴 보겠습니다.
상호 포함 a.h
및 b.h
헤더 파일에 사소한 내용이 있다고 가정합니다 . 즉, 질문 텍스트의 코드 섹션에있는 줄임표가 빈 문자열로 대체됩니다. 이 상황에서 main.cpp
행복하게 컴파일됩니다. 그리고 이것은 당신의 포함 경비원 덕분입니다!
확실하지 않은 경우 제거해보십시오.
//================================================
// a.h
#include "b.h"
//================================================
// b.h
#include "a.h"
//================================================
// main.cpp
//
// Good luck getting this to compile...
#include "a.h"
int main()
{
...
}
컴파일러가 포함 깊이 제한에 도달하면 오류를보고 함을 알 수 있습니다. 이 제한은 구현에 따라 다릅니다. C ++ 11 표준의 단락 16.2 / 6에 따라 :
#include 전처리 지시문은 구현 정의 중첩 한계까지 다른 파일의 #include 지시문으로 인해 읽은 소스 파일에 나타날 수 있습니다 .
그래서 무슨 일이야 ?
- 구문 분석 할 때
main.cpp
전처리 기는 지시문을 충족합니다#include "a.h"
. 이 지시문은 헤더 파일a.h
을 처리하고, 처리 결과를 취하고, 문자열#include "a.h"
을 해당 결과로 대체 하도록 전처리기에 지시합니다 . - 처리하는 동안
a.h
전처리 기는 지시문을 충족하며#include "b.h"
동일한 메커니즘이 적용됩니다. 전처리 기는 헤더 파일을b.h
처리하고 처리 결과를 가져와#include
지시문을 해당 결과로 대체합니다 . - 처리 할 때
b.h
지시문#include "a.h"
은 전처리기에 지시문 을 처리a.h
하고 결과로 대체하도록 지시합니다. - 전처리 기는
a.h
다시 구문 분석을 시작 하고#include "b.h"
지시문을 다시 충족 하며 잠재적으로 무한 재귀 프로세스를 설정합니다. 중요한 중첩 수준에 도달하면 컴파일러에서 오류를보고합니다.
그러나 include 가드가있는 경우 4 단계에서 무한 재귀가 설정되지 않습니다. 이유를 살펴 보겠습니다.
- ( 이전과 동일 ) 구문 분석 할 때
main.cpp
전처리 기는 지시문을 충족합니다#include "a.h"
. 이것은 전처리기에 헤더 파일a.h
을 처리하고, 그 처리의 결과를 취하고, 그 결과로 문자열#include "a.h"
을 대체하도록 지시합니다 . - 처리하는 동안
a.h
전처리 기는 지시문을 충족합니다#ifndef A_H
. 매크로A_H
가 아직 정의되지 않았으므로 다음 텍스트를 계속 처리합니다. 후속 지시문 (#defines A_H
)은 매크로를 정의합니다A_H
. 그런 다음 전처리 기는 지시문을 충족합니다#include "b.h"
. 전처리 기는 이제 헤더 파일을b.h
처리하고 처리 결과를 가져와#include
지시문을 해당 결과로 대체합니다 . - 처리 할 때
b.h
전처리 기는 지시문을 충족합니다#ifndef B_H
. 매크로B_H
가 아직 정의되지 않았으므로 다음 텍스트를 계속 처리합니다. 후속 지시문 (#defines B_H
)은 매크로를 정의합니다B_H
. 그런 다음 지시문#include "a.h"
은 전처리기에 지시문 을 처리a.h
하고 전처리 결과로 대체하도록#include
지시합니다 .b.h
a.h
- 컴파일러는 전처리를
a.h
다시 시작 하고#ifndef A_H
지시문을 다시 충족 합니다. 그러나 이전 전처리 과정에서 매크로A_H
가 정의되었습니다. 따라서 컴파일러는 일치하는#endif
지시문을 찾을 때까지 이번에는 다음 텍스트를 건너 뛰고이 처리의 출력은 빈 문자열입니다 (#endif
물론 지시문 뒤에 아무것도 없다고 가정 ). 따라서 전처리 기는의#include "a.h"
지시문을b.h
빈 문자열로 교체하고의 원래#include
지시문을 바꿀 때까지 실행을 추적합니다main.cpp
.
따라서 포함 가드는 상호 포함으로부터 보호 합니다. 그러나 상호 포함 파일에서 클래스 정의 간의 종속성 에는 도움이되지 않습니다 .
//================================================
// a.h
#ifndef A_H
#define A_H
#include "b.h"
struct A
{
};
#endif // A_H
//================================================
// b.h
#ifndef B_H
#define B_H
#include "a.h"
struct B
{
A* pA;
};
#endif // B_H
//================================================
// main.cpp
//
// Good luck getting this to compile...
#include "a.h"
int main()
{
...
}
Given the above headers, main.cpp
will not compile.
Why is this happening?
To see what's going on, it is enough to go through steps 1-4 again.
It is easy to see that the first three steps and most of the fourth step are unaffected by this change (just read through them to get convinced). However, something different happens at the end of step 4: after replacing the #include "a.h"
directive in b.h
with the empty string, the preprocessor will start parsing the content of b.h
and, in particular, the definition of B
. Unfortunately, the definition of B
mentions class A
, which has never been met before exactly because of the inclusion guards!
Declaring a member variable of a type which has not been previously declared is, of course, an error, and the compiler will politely point that out.
What do I need to do to solve my problem?
You need forward declarations.
In fact, the definition of class A
is not required in order to define class B
, because a pointer to A
is being declared as a member variable, and not an object of type A
. Since pointers have fixed size, the compiler won't need to know the exact layout of A
nor to compute its size in order to properly define class B
. Hence, it is enough to forward-declare class A
in b.h
and make the compiler aware of its existence:
//================================================
// b.h
#ifndef B_H
#define B_H
// Forward declaration of A: no need to #include "a.h"
struct A;
struct B
{
A* pA;
};
#endif // B_H
Your main.cpp
will now certainly compile. A couple of remarks:
- Not only breaking the mutual inclusion by replacing the
#include
directive with a forward declaration inb.h
was enough to effectively express the dependency ofB
onA
: using forward declarations whenever possible/practical is also considered to be a good programming practice, because it helps avoiding unnecessary inclusions, thus reducing the overall compilation time. However, after eliminating the mutual inclusion,main.cpp
will have to be modified to#include
botha.h
andb.h
(if the latter is needed at all), becauseb.h
is no more indirectly#include
d througha.h
; - While a forward declaration of class
A
is enough for the compiler to declare pointers to that class (or to use it in any other context where incomplete types are acceptable), dereferencing pointers toA
(for instance to invoke a member function) or computing its size are illegal operations on incomplete types: if that is needed, the full definition ofA
needs to be available to the compiler, which means the header file that defines it must be included. This is why class definitions and the implementation of their member functions are usually split into a header file and an implementation file for that class (class templates are an exception to this rule): implementation files, which are never#include
d by other files in the project, can safely#include
all the necessary headers to make definitions visible. Header files, on the other hand, won't#include
other header files unless they really need to do so (for instance, to make the definition of a base class visible), and will use forward-declarations whenever possible/practical.
SECOND QUESTION:
Why aren't include guards preventing multiple definitions?
They are.
What they are not protecting you from is multiple definitions in separate translation units. This is also explained in this Q&A on StackOverflow.
Too see that, try removing the include guards and compiling the following, modified version of source1.cpp
(or source2.cpp
, for what it matters):
//================================================
// source1.cpp
//
// Good luck getting this to compile...
#include "header.h"
#include "header.h"
int main()
{
...
}
The compiler will certainly complain here about f()
being redefined. That's obvious: its definition is being included twice! However, the above source1.cpp
will compile without problems when header.h
contains the proper include guards. That's expected.
Still, even when the include guards are present and the compiler will stop bothering you with error message, the linker will insist on the fact that multiple definitions being found when merging the object code obtained from the compilation of source1.cpp
and source2.cpp
, and will refuse to generate your executable.
Why is this happening?
Basically, each .cpp
file (the technical term in this context is translation unit) in your project is compiled separately and independently. When parsing a .cpp
file, the preprocessor will process all the #include
directives and expand all macro invocations it encounters, and the output of this pure text processing will be given in input to the compiler for translating it into object code. Once the compiler is done with producing the object code for one translation unit, it will proceed with the next one, and all the macro definitions that have been encountered while processing the previous translation unit will be forgotten.
In fact, compiling a project with n
translation units (.cpp
files) is like executing the same program (the compiler) n
times, each time with a different input: different executions of the same program won't share the state of the previous program execution(s). Thus, each translation is performed independently and the preprocessor symbols encountered while compiling one translation unit will not be remembered when compiling other translation units (if you think about it for a moment, you will easily realize that this is actually a desirable behavior).
Therefore, even though include guards help you preventing recursive mutual inclusions and redundant inclusions of the same header in one translation unit, they can't detect whether the same definition is included in different translation unit.
Yet, when merging the object code generated from the compilation of all the .cpp
files of your project, the linker will see that the same symbol is defined more than once, and since this violates the One Definition Rule. Per Paragraph 3.2/3 of the C++11 Standard:
Every program shall contain exactly one definition of every non-inline function or variable that is odr-used in that program; no diagnostic required. The definition can appear explicitly in the program, it can be found in the standard or a user-defined library, or (when appropriate) it is implicitly defined (see 12.1, 12.4 and 12.8). An inline function shall be defined in every translation unit in which it is odr-used.
Hence, the linker will emit an error and refuse to generate the executable of your program.
What do I need to do to solve my problem?
If you want to keep your function definition in a header file that is #include
d by multiple translation units (notice, that no problem will arise if your header is #include
d just by one translation unit), you need to use the inline
keyword.
Otherwise, you need to keep only the declaration of your function in header.h
, putting its definition (body) into one separate .cpp
file only (this is the classical approach).
The inline
keyword represents a non-binding request to the compiler to inline the function's body directly at the call site, rather than setting up a stack frame for a regular function call. Although the compiler doesn't have to fulfill your request, the inline
keyword does succeed in telling the linker to tolerate multiple symbol definitions. According to Paragraph 3.2/5 of the C++11 Standard:
There can be more than one definition of a class type (Clause 9), enumeration type (7.2), inline function with external linkage (7.1.2), class template (Clause 14), non-static function template (14.5.6), static data member of a class template (14.5.1.3), member function of a class template (14.5.1.1), or template specialization for which some template parameters are not specified (14.7, 14.5.5) in a program provided that each definition appears in a different translation unit, and provided the definitions satisfy the following requirements [...]
The above Paragraph basically lists all the definitions which are commonly put in header files, because they can be safely included in multiple translation units. All other definitions with external linkage, instead, belong in source files.
Using the static
keyword instead of the inline
keyword also results in suppressing linker errors by giving your function internal linkage, thus making each translation unit hold a private copy of that function (and of its local static variables). However, this eventually results in a larger executable, and the use of inline
should be preferred in general.
An alternative way of achieving the same result as with the static
keyword is to put function f()
in an unnamed namespace. Per Paragraph 3.5/4 of the C++11 Standard:
An unnamed namespace or a namespace declared directly or indirectly within an unnamed namespace has internal linkage. All other namespaces have external linkage. A name having namespace scope that has not been given internal linkage above has the same linkage as the enclosing namespace if it is the name of:
— a variable; or
— a function; or
— a named class (Clause 9), or an unnamed class defined in a typedef declaration in which the class has the typedef name for linkage purposes (7.1.3); or
— a named enumeration (7.2), or an unnamed enumeration defined in a typedef declaration in which the enumeration has the typedef name for linkage purposes (7.1.3); or
— an enumerator belonging to an enumeration with linkage; or
— a template.
For the same reason mentioned above, the inline
keyword should be preferred.
First of all you should be 100% sure that you have no duplicates in "include guards".
With this command
grep -rh "#ifndef" * 2>&1 | uniq -c | sort -rn | awk '{print $1 " " $3}' | grep -v "^1\ "
you will 1) highlight all include guards, get unique row with counter per include name, sort the results, print only counter and include name and remove the ones that are really unique.
HINT: this is equivalent to get the list of duplicated include names
'Development Tip' 카테고리의 다른 글
Android는 모든 것 위에 뷰를 오버레이합니까? (0) | 2020.11.04 |
---|---|
Rails에서 파일 업로드 (0) | 2020.11.04 |
angularjs에서 제출시 유효성 검사 오류 메시지 표시 (0) | 2020.11.04 |
haskell의 attoparsec 또는 parsec (0) | 2020.11.04 |
명시 적 유형 매개 변수를 사용하여 템플릿 멤버 함수를 호출하려고하면 오류가 발생하는 이유는 무엇입니까? (0) | 2020.11.04 |