C++ 시작하기, 전처리문, inline 그리고 네임스페이스
처음 C++을 98규격 프로젝트 기반으로 공부를 해서 결과물 중심으로 학습을 하였습니다. 그래서 지나쳐 온 부분도 많고 새롭게 배워야 할 모던 C++ 부분도 많기에 복습을 하며 다시 배워보려고 합니다. 그리고 추후 게임개발을 하기위해 언리얼 프레임워크도 공부할 생각입니다.
전처리문
코드를 컴파일 하기 전, 처리되는 명령어들로 전처리기 프로그램을 통해 실행됩니다. 전처리문은 #으로 시작해서 줄바꿈으로 구분되며 다양한 기능을 처리합니다.
include
#include <iostream>
다른 네임스페이스 혹은 클래스, 메서드를 사용하기 위해 위 전처리문을 사용합니다. 이 전처리문은 처음 접했을 때에는 다른 헤더 파일 안의 코드를 사용하기 위해 참조하기 위한 전처리문으로 이해했습니다. 하지만 이 내용은 틀렸습니다. include 전처리문은 해당 파일안의 코드 전체를 복사하여 include를 사용한 코드 본문에 붙혀 넣습니다. 즉 본문 코드의 길이가 대폭 늘어나게 되며 성능문제를 야기합니다. 그래서 사용하지 않는 헤더 파일들은 사용을 절대적으로 금지해야 합니다.
#include "filename"
위 include도 동일한 기능을 합니다. 하지만 차이가 있다면 사용자 지정 경로 혹은 현재경로를 탐색하여 파일을 찾습니다. 사용자 지정 경로는 대부분 컴파일러의 -I 옵션을 사용하여 경로를 선택합니다. 반대로 include <>는 C++ 런타임 라이브러리의 지정 위치를 탐색하기 때문에 사용자가 작성한 헤더 파일과 구분지어 탐색할 수 있습니다.
define
위 전처리문을 사용해서 매크로를 만들 수 있습니다. 여기서 매크로는 다양한 일련의 동작을 하나로 압축시켜 사용하는 매크로가 아닌 입력을 출력으로 변환 혹은 치환하는 방식을 말합니다.
#define PI 3.14
#define MY_HEADER_FILE
위 예시에는 2개의 매크로가 선언되었습니다. #define { identifier } { substitution } 형식으로 구성되어 있으며, 두번째 지시자와 같이 substitution이 없는 형태로도 선언될 수 있습니다. 그리고 identifier는 대문자로만 구성하는게 관례입니다. 그러면 선언한 매크로를 사용한 예시를 작성해보겠습니다.
double CircleArea(int radius)
{
return radius * radius * PI;
}
위와 같은 함수는 컴파일 전에 아래와 같이 변환됩니다.
double CircleArea(int radius)
{
return radius * radius * 3.14;
}
지시자를 통해서 PI가 3.14로 치환된 것을 확인 할 수 있습니다. 이런 지시문을 사용하여 다양한 값을 알기 쉬운 식별자로 사용하여 표현할 수 있습니다.
그렇다면 identifier만 있는 매크로는 어떻게 사용할까요?
ifdef endif / ifndef endif
조건부 컴파일 지시자들입니다. 형식은 #ifdef {identifier} 혹은 #ifndef {identifier}로 먼저 조건부 지시자로 identifier의 선언유무를 확인합니다. 그리고 코드를 작성하고 마지막에 #endif를 입력하여 조건부 지시자를 닫습니다.
#define MY_MACRO
#ifdef MY_MACRO
std::cout << "Hello World!" << std::endl;
#endif
위 예시에서는 MY_MACRO 매크로를 정의하여 조건부 컴파일 지시자를 통해 선언 유무를 확인해 중간의 출력문을 컴파일합니다. 즉 조건에 따라 지시자 사이의 코드를 컴파일되지 않을 수 있다는 것입니다. 이 방법을 통해 헤더파일의 중복 선언을 막을 수 있습니다.
#ifndef MY_HEADER
#define MY_HEADER
...
#endif
헤더 파일을 작성할 때 위와 같은 형식으로 작성할 경우 컴파일 전 중복되는 헤더파일을 제외할 수 있습니다. ifndef 지시자는 선언이 되지 않았다면 컴파일 되는 구조로, 이미 동일한 헤더 파일을 이미 포함한 경우 컴파일 되지 않도록 작성되어 있는 것입니다.
inline 함수
함수 호출 오버헤드, 즉 콜스택에 메모리를 적재하는 과정을 거치지 않게 하여 오버헤드를 줄이고 코드의 가독성을 높여주는 함수입니다.
#include <iostream>
inline int add(int a, int b) {
return a + b;
}
int main() {
std::cout << add(3, 5) << std::endl;
}
위 예시와 같이 선언된 경우 add 메서드를 호출하는 것이 아닌 본문을 그대로 가져와 현재 함수에서 실행됩니다. 즉 add(3, 5)가 아닌 3 + 5가 변환되어 실행되는 것입니다. 하지만 inline은 항상 적용되는 것은 아닙니다. 컴파일러에 의해 관리되기 때문에 함수가 복잡하고 크다면, inline으로 선언되도 호출을 당할 수 있습니다.
전처리 매크로와 inline은 변환된다는 점은 비슷하지만, 매크로는 컴파일 전 실행되며, inline은 컴파일 중 실행되는 큰 차이가 있습니다. 그리고 C++17부터 클래스의 메서드와 constexpr 상수식을 사용한 함수는 inline 키워드를 사용하지 않아도 자동적으로 inline 처리를 하게 됩니다.
namespace
네임 스페이스는 리눅스에서도 사용하는 개념으로 소속을 통해 개체들을 충돌을 방지하기 위해 격리 시키는 기능을 담당합니다.
namespace my
{
int var = 10;
}
namespace 키워드를 사용하여 선언하며 뒤에는 식별자를 기입합니다. 네임스페이스 안의 요소는 식별자와 :: 연산자를 통해 사용합니다.
std::cout << my::var << std::endl; // print: 10
namespace는 중첩되서 사용될 수도 있으며, 당연히 사용하려면 모든 식별자를 기입해야합니다.
namespace county
{
namespace city
{
string name = "Seoul";
}
}
std::cout << county::city::name << std::endl; // print: Seoul
중첩 네임스페이스는 주로 사용되는 방법은 inline namespace가 있습니다.
inline namespace
#include <iostream>
namespace Game {
inline namespace Version1 {
void play() {
std::cout << "Playing Game Version 1" << std::endl;
}
}
}
int main() {
Game::play(); // print: Playing Game Version 1
}
inline 키워드를 namespace 앞에 기입하여 사용하며, 내부 네임스페이스를 외부 네임스페이스의 요소로 사용할 수 있습니다. 위 예시와 같이 Game 네임스페이스 안의 내부 네임스페이스인 Version1이 있지만 play 함수를 호출할 경우 Game 식별자만 기입하여 사용할 수 있습니다. 굳이 Game 네임스페이스 안에 play 함수를 선언하면 되는데 내부 네임스페이스를 iline으로 선언하여 함수를 사용하는 이유는 무엇일까요?
namespace Game {
inline namespace Version2 {
void play() {
std::cout << "Playing Game Version 2" << std::endl;
}
}
namespace Version1 {
void play() {
std::cout << "Playing Game Version 1" << std::endl;
}
}
}
int main() {
Game::play(); // print: Playing Game Version 2
Game::Version1::play(); // print: Playing Game Version 1
}
위 예시와 같이 내부 네임스페이스가 다수인 경우 사용할 수 있습니다. 기본으로 사용할 내부 네임스페이스를 inline으로 선언하여 간단하게 현재 버전에 맞는 요소를 외부 네임스페이스로 사용할 수 있으며, 특정 내부스페이스 또한 사용할 수 있습니다.