[C++] 정의( definition )의 중복을 막는 헤더 가드( header guard )

반응형

정의( definition )의 중복이 발생하는 이유

C++에서는 변수나 함수, 클래스 같은 타입의 정의가 중복되는 것을 허용하지 않습니다.

 

아래의 예문은 이러한 정의가 중복되는 예를 보여줍니다.

#include <iostream> 

int func1( int x){
    return x + 10;
}

int func1( int x){  // 정의의 중복
    return x + 10;
}

int main(){

    int x = func1(10);
    std::cout << x << '\n';
}

▼출력

error: redefinition of 'int func1(int)'

C++을 어느 정도 다루었다면, 위의 코드는 문제가 있다는 것을 바로 알 것입니다.

그런데, 예상외로 위와 같은 정의가 중복될 수 있는 경우가 많이 생긴다는 것입니다.

 

그렇게 되는 것은, 여러 파일에 분리되어 있는 기능을 사용하기 위하여, #include 전처리기 지시자를 이용하여, 포함되는 파일에 정의되어 있는 식별자들을, 그 기능을 사용하는 파일에 삽입하기 때문입니다.

 

다음은 func1 함수를 정의하고 있는 파일 "module1.h"를 보여줍니다.

// module1.h 
int func1(int x){
    return x + 10;
}

 

그리고, 다음은 위의 func1 함수를 사용하여, 다른 함수 func2를 정의하는 파일 "module2.h"입니다.

// module2.h
#include "module1.h"

int func2( int x){
    return func1(x) + 10;   
}

 

그리고, 위의 두 파일을 #include 지시자를 이용하여 포함하는 main 함수가 정의된 "main.cpp" 파일입니다.

// main.cpp
#include <iostream> 
#include "module1.h"	// func1이 정의된 파일
#include "module2.h"	// func2가 정의된 파일

int main(){

    int x = func2(10);
    std::cout << x << '\n';
}

main 함수가 정의된 파일을 컴파일하기 위해서 번역 단위( translation unit )를 만들기 위해, 전처리기( preprocessor )가 #include "module1.h"#include "module2.h" 지시자를 만나게 되면 다음과 같은 코드들을 만들어냅니다.

// 이 파일에 있는 코드들도 모두 가져오지만, 설명을 위해 생략
#include <iostream> 

int func1(int x){	// module1.h에 있는 코드
    return x + 10;
}

int func1(int x){	// module2.h에 있는 코드
    return x + 10;
}

int func2( int x){	// module2.h에 있는 코드
    return func1(x) + 10;   
}

int main(){

    int x = func2(10);
    std::cout << x << '\n';
}

▼출력

error: redefinition of 'int func1(int)'

여기서, 주목할 것은 "module2.h" 파일도 #include "module1.h" 지시자를 사용하여, func1 함수의 정의를 "module2.h" 파일에 포함하고, 이렇게 확장된 "module2.h" 파일을 다시 "main.cpp" 파일이 포함함으로써, func1 함수의 정의가 중복되는 것입니다.

그래서, 위의 아주 간단한 코드들도 컴파일에 실패하게 됩니다.

 

여기서, '그럼 "module1.h" 파일은 "module2.h"가 포함하니까, "main.cpp" 파일에서는 "module2.h" 파일만 #include 하면 되지 않을까?'라고 생각할 수 있을 것입니다.

그리고, 이렇게 하는 것이 불필요한 전처리 과정을 제거하고, 컴파일이나 링크의 시간도 줄이게 되므로 제일 바람직합니다.

그러나, 이것은 우리가 모든 코드를( 안되면 적어도 파일의 포함 관계라도 ) 알아야 가능한 일입니다.

 

실제로, 위에서도 포함된 "iostream" 파일에 std::cin이나 std::cout의 정의가 들어있는 것은 대부분 알고들 있지만, 그 외에 어떤 식별자의 정의가 들어있는지를 모두 알고 있는 사람은 아주 소수일 것입니다.

그런 상태에서 정의의 중복을 막기 위해 파일의 포함 관계를 따지는 일은, C++ 프로그래밍 과정을 피곤하게 만들 뿐만 아니라, 아예 불가능한 일일지도 모릅니다.

 

그래서, C++을 사용하는 사람들은 이 문제를 해결하기 위한 도구를 도입하였습니다.

 

 

헤더 가드( header guard )

헤더 가드는 몇 개의 전처리기 지사자들( preprocessor directives )들 조합하여, 조건부 컴파일 코드들을 만들어 내는 전처리 구문입니다.

이 헤더 가드의 목적은 이미 정의된 식별자가 다시 정의되는 코드의 포함( 대부분의 경우 #include 지시자를 사용하여 )을 자동으로 방어하기 위합니다.

그래서, 헤더 가드를 include guard라고도 부릅니다.

 

다음은 이전 항목의 "module1.h" 파일에 헤더 가드를 설치한 코드를 보여줍니다.

이 헤더 가드는 전처리기 지사자인 #ifndef#define, 그리고 #endif로 이루어지는 조건부 컴파일 전처리기 구문입니다.

// module1.h
#ifndef MODULE_1_H	// 헤더 가드
#define MODULE_1_H

int func1(int x){
    return x + 10;
}

#endif

전처리가 위의 전처리지 지시자를 만나면, 먼저 식별자 MODULE_1_H가 전처리를 수행하는 파일 내에 정의되어 있는지 확인을 합니다.

그래서, 만약 MODULE_1_H가 정의되어 있지 않으면, 그다음 코드로 검색을 하게 되고, #define을 만나게 되어, 식별자 MODULE_1_H를 정의하게 됩니다.

반대로, 만약 MODULE_1_H가 이미 정의되어 있으면, 전처리기는 #ifndef와 짝을 이루는 #endif 지시자를 만날 때까지, 그 사이에 있는 모든 코드들을 실제 컴파일하게 될 번역 단위( translation unit )에서 제외합니다.

 

이렇게 조건부 컴파일 구문을 사용하여, 정의된 코드가 다시 정의되는 일을 방어하는 것이 헤더 가드의 역할입니다.

 

헤더 가드의 식별자

헤드 가드에 사용되는 식별자에 적용되는 규칙은 없습니다.

그래서, 다른 식별자와 구별만 된다면, 얼마든지 원하는 식별자를 사용할 수 있습니다.

하지만, 일반적으로 많이 사용되는 관습과 같은 방식이 몇 가지 있습니다.

 

기본적으로 식별자를 위한 문자열을 파일의 이름을 기반으로 하여 만들고, 그 문자열은 대문자로 합니다.

그리고, 파일 명의 컴마(.)나 사이의 공백은 밑줄( _ )로 대체하는 방식입니다.

위의 MODULE_1_H도 이러한 규칙을 기반으로 만들어진 식별자입니다.

 

그런데, 이와 같은 방법도 식별자의 중복을 막지 못하는 경우가 있습니다.

#include "folder1/module1.h"
#include "folder2/module1.h"

만약, 위와 같이 같은 파일이 폴더 folder1folder2에 들어있고, 이 두 파일을 동시에 포함하게 되면, 헤드 가드를 사용함에도 불구하고 같은 코드를 두 번 포함하게 되는 문제를 해결할 수 없습니다.

 

그래서, 좀 더 복잡한 방식의 헤드 가드 식별자를 만들기도 합니다.

예를 들면, 파일 명과 파일의 경로를 합하거나, 파일 명과 파일이 만들어진 날짜를 합하는 방법, 또는 파일 명과 UUID를 이루는 32개의 숫자를 합하는 방법 등이 있습니다.


다시, 헤더 가드의 얘기로 돌아와, 이러한 헤더 가드를 모든 헤더 파일( 꼭, 확장자가 h로 끝나는 파일뿐만 아니라, 다른 파일에 포함되는 파일들 모두에 대하여 )에 만드는 것은 아주 권장할 만하고 할 수 있습니다.

 

참고로, gcc 컴파일러가 사용하는 C++ 표준 라이브러리의 "iostream" 파일을 보면, 다음과 같은 헤더 가드를 사용하고 있음을 볼 수 있습니다.

#ifndef _GLIBCXX_IOSTREAM
#define _GLIBCXX_IOSTREAM 1

...

#endif /* _GLIBCXX_IOSTREAM */

 

이제, 이전 항목의 func1 함수의 정의가 중복되는 문제는 해결하기 위해, "module2.h" 파일에도 헤더 가드를 설치합니다.

// module2.h
#ifndef MODULE_2_H  // 헤더 가드
#define MODULE_2_H

#include "module1.h"

int func2( int x){
    return func1(x) + 10;   
}

#endif

그리고, 다시 main 함수를 컴파일하면, 아무 문제 없이 컴파일이 되는 것을 볼 수 있습니다.

#include <iostream> 
#include "module1.h"
#include "module2.h"

int main(){

    int x = func2(10);
    std::cout << x << '\n';
}

이것은 "module2.h" 파일에서도 "module1.h"를 포함하지만, 먼저 사용된 #include "module1.h" 지시문에 의해서 MODULE_1_H 식별자가 정의되어, 두 번째 포함되는 func1 함수 코드가 컴파일 대상에 제거되기 때문입니다.

 

이를 실제 전처리기가 보는 코드로 직접 변경하면, 이해가 더 쉽게 될 것입니다.

#include <iostream>	// 이 파일 포함은 설명을 위해 건너뜀

#ifndef MODULE_1_H
#define MODULE_1_H	// MODULE_1_H 정의

int func1(int x){
    return x + 10;
}

#endif	// #include "module1.h"의 끝

#ifndef MODULE_2_H  
#define MODULE_2_H

#ifndef MODULE_1_H	// 이때는 MODULE_1_H가 이미 정의되어 있음
#define MODULE_1_H

int func1(int x){	// 이 코드는 컴파일에서 제외됨
    return x + 10;
}

#endif	// #include "module1.h"의 끝

int func2( int x){
    return func1(x) + 10;   
}

#endif	// #include "module2.h"의 끝

int main(){

    int x = func2(10);
    std::cout << x << '\n';
}

위는 전처리기가 #include 지시자를 처리했을 때의 코드입니다.

여기서, 두 번째 func1 함수의 정의는 MODULE_1_H 식별자가 이미 정의되어 있으므로, #ifndef 지시자로 인하여 번역 단위( translation unit )에 포함될 수 없습니다.

따라서, 함수의 정의들은 더 이상 중복되지 않고, 이것으로 문제 해결입니다.

 

그런데, 위와 같이 헤더 가드를 사용해서 문제는 일단 해결했지만, 위의 코드는 잠재적인 위험을 안고 있다는 걸 알아야 됩니다.

 

함수의 정의 중복

헤더 가드( header guard )를 설치한 위의 "module1.h" 파일을 다른 cpp 파일에서 포함하는 경우도 있을 수 있습니다.

예를 들면, "module1.cpp" 파일에서 "module1.h" 파일을 포함하는 아래와 같은 경우를 생각해 보죠.

// module1.cpp
#include "module1.h"

int func3( int x){
    return func1(x) + 20;
}

이 경우는 각각의 파일별로 수행되는 컴파일은 제대로 되지만, 컴파일의 결과로 생성되는 목적 파일( object file )들을 결합하는 링크 단계에서 실패합니다.

 

이렇게 되는 것은, #define 지시자가 정의하는 식별자의 영향 범위가, 그 지시자가 들어있는 파일에 제한되기 때문입니다.

따라서, 헤더 가드가 제대로 동작하는 범위도 각각의 파일에 제한된다는 것을 알 수 있습니다.

 

결국, "main.cpp" 파일과 "module1.cpp" 파일에 같은 func1 함수의 정의가 존재하게 되고, 이것이 링크 단계를 실패하게 만드는 것입니다. ( 다시 환기시키자면, 컴파일은 파일 단위로 수행됩니다. )

 

이 문제를 해결하는 방법은 여러 파일에 포함되게 되는 헤더 파일( 위의 경우 "module1.h" 파일 )에는 전방 선언( forward declaration )만을 하고, 함수의 실제 정의 코드를 cpp 파일에서 제공하는 것입니다.

 

[C++] 식별자를 알려주는 전방 선언( forward declaration )

전방 선언( forward declaration )식별자( identifier )란 변수, 함수, 타입 등의 이름을 말합니다.int a = 0; // a: 변수 식별자int add( int, int ); // add: 함수 식별자class CSomething; // CSomething: 타입 식별자 이러한

codingbonfire.tistory.com

 

아래는 이 방식을 적용한 모든 코드를 보여줍니다.

( 여기에는 "module2.h"의 함수 정의를 "module2.cpp" 파일에 옮기는 것도 포함했습니다. )

// module1.h
#ifndef MODULE_1_H	// 헤더 가드
#define MODULE_1_H

int func1(int x);   // 전방 선언만 제공

#endif

// module1.cpp
#include "module1.h"

int func1(int x){   // 함수의 정의
    return x + 10;
}

int func3( int x){
    return func1(x) + 20;
}

// module2.h
#ifndef MODULE_2_H  // 헤더 가드
#define MODULE_2_H

int func2( int x);  // 전방 선언

#endif

// module2.cpp
#include "module1.h"
#include "module2.h"

int func2( int x){
    return func1(x) + 10;   
}

// main.cpp
#include <iostream> 
#include "module1.h"
#include "module2.h"

int main(){

    int x = func2(10);
    std::cout << x << '\n';
}

▼출력

30

여기서 말하고자 하는 것은, 여러 곳에 포함될 수 있는 헤더 파일엔 전방 선언만 넣고, 실제 정의코드는 cpp와 같은 다른 파일에 넣어 정의의 중복이 발생하는 것을 예방하는 것이 바람직하다는 것입니다.

 

참고로, 전방 선언( forward declaration )은 컴파일러에게 식별자의 존재 여부를 알려주는 기능을 할 뿐이므로, 선언의 중복은 문제를 만들지 않습니다.

#include "module1.h"	// 같은 선언은 문제를 발생하지 않습니다.
#include "module1.h"
#include "module1.h"

 

#pragma once

#pragma once는 헤더 가드와 같은 역할을 하는 전처리기 지시자입니다.

 

위의 "module1.h" 파일을 이 지시자를 사용하여 다음과 같이 변경할 수 있습니다.

// module1.h
#pragma once

int func1(int x);   // 전방 선언만 제공

이 방식의 장점은 보다시피 소스 코드를 복잡하게 만들지 않는다는 것입니다.

실제로 조건부 컴파일의 조건을 다중으로 만들게 되면 #endif 지시자가 파일 끝에 여러 번 겹쳐서 나타나는 것을 쉽게 볼 수 있습니다.

그래서, 요즘엔 헤더 가드를 이 지시자로 대체해서 자동으로 제공하는 IDE 환경도 많이 있다고 합니다.

 

하지만, 이 #pragma 전처리기 지시자는 C++의 표준이 아니기 때문에, 컴파일러의 구현 방식에 따라 그 기능이 조금씩 다를 수도 있고, 아예 이를 지원하지 않는 컴파일러도 있을 수 있다는 점은 고려해야 합니다.

 

정리

  • 헤더 가드( header guard )의 사용으로 정의의 중복을 예방할 수 있습니다.
  • 여러 파일에 포함되는 헤더 파일엔 전방 선언만 사용하고, 식별자의 정의는 cpp 파일에 위치시켜야 합니다.

 

이 글과 관련 있는 글들

 

 

 

 

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유