Shared global constants

여러 파일에서 공통으로 사용되는 전역 상수의 선언, 정의 방법에 따른 차이점.


Internal linkage를 갖는 전역 상수

constants.h:

1
2
3
4
5
6
7
8
#pragma once

namespace constants
{
constexpr double PI { 3.141592 };
constexpr float GRAVITY { 9.8 };
// ...
} // namespace constants

main.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "constants.h"
#include <iostream>

int main()
{
std::cout << "Enter a radius: ";
float radius{ 0.f };
std::cin >> radius;

std::cout << "The circumference is: "
<< 2.f * radius * constants::PI
<< std::endl;

return 0;
}

이러한 방식은 constants.h를 포함하는 Translate unit마다 해당 상수의 복사본이 존재한다. 대부분의 상황에서는 이러한 상수들은 컴파일러에 의해 최적화가 되지만(예를 들면 PI의 위치에 그 값인 3.141592를 직접 대입), 크기가 너무 크거나해서 최적화 될 수 없는 경우도 존재한다. 이럴경우 복사본만큼 메모리가 낭비된다.

또한 constants.h의 상수를 수정하면 이 파일을 포함하는 다른 모든 파일들을 다시 컴파일 해야한다.



External linkage를 갖는 전역 상수

위에서 언급한 문제들을 해결하는 방법 중 하나는 extern키워드를 이용하는 것이다. extern으로 정의 된 데이터는 단 한번 초기화되고, 외부에서의 접근도 가능하다.

⚠️이경우 constexpr대신 const를 사용해야 한다. 외부에서 extern데이터에 접근하기 위해서 전방선언을 해야하는데 constexpr은 컴파일타임 상수이므로 전방선언이 불가능하다.


constants.cpp:

1
2
3
4
5
6
7
8
#include "constants.h"

namespace constants
{
extern const double PI { 3.141592 };
extern const float GRAVITY { 9.8 };
// ...
} // namespace constants

constants.h:

1
2
3
4
5
6
7
8
9
#pragma once

namespace constants
{
// forward declarations
extern const double PI;
extern const float GRAVITY;
// ...
} // namespace constants

main.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "constants.h"
#include <iostream>

int main()
{
std::cout << "Enter a radius: ";
float radius{ 0.f };
std::cin >> radius;

std::cout << "The circumference is: "
<< 2.f * radius * constants::PI
<< std::endl;

return 0;
}

이제 전역 상수들은 모두 constants.cpp 내부에서 단 한번 초기화 되고, constants.h를 포함하는 모든 Translate unit은 복사본이 아닌 원본과 링크된다. 또한 constants.cpp가 수정되어도 constants.cpp만 재컴파일 된다.

Internal linkage 버전의 단점을 모두 해결한 것 처럼 보이지만, 또다른 단점이 존재한다. 먼저 값을 초기화하는 constants.cpp에 선언된 상수들만 컴파일 타임 상수로 취급된다. 다른 Translate unit의 상수들은 모두 전방선언 되었기 때문에 컴파일타임에 그 값을 알 수 없고 링크 이후에야 그 값을 알 수 있다. 즉, 이 상수들은 모두 런타임 상수 취급을 받는다. 일반적으로 컴파일타임 상수가 런타임 상수보다 더 자주 최적화되며 당연히 컴파일타임 상수 문법에 사용될 수 없다.



inline으로 선언된 전역 상수

inline variable은 C17에서 새로 소개된 개념이다. C에서 inline키워드는 "여러번 정의될 수 있음"을 내포한다. 그러므로 inline variable은 'One Definition Rule’을 위반하지 않고 여러 파일에서 정의할 수 있다. 전역 inline 변수는 기본적으로 external linkage를 갖는다.

링커가 inline으로 선언된 변수들을 모아 하나의 선언으로 취급하기 때문에 .cpp파일 하나에 선언한 것과 같이 메모리 낭비를 막을 수 있다. 또한 constexpr의 특성을 계속 유지할 수 있기 때문에 컴파일타임 상수의 장점을 모두 활용할 수 있다.

inline variable은 아래 두 가지 조건을 따라야 한다.

  1. 동일한 inline variable의 정의는 모두 같아야 한다. 그렇지 않을경우 Undefined behavior이다.
  2. inline variable은 사용하려는 파일에서 반드시 정의되어야 한다.(전방선언을 할 수 없다.)

constants.h:

1
2
3
4
5
6
7
8
#pragma once

namespace constants
{
inline constexpr double PI { 3.141592 };
inline constexpr float GRAVITY { 9.8 };
// ...
} // namespace constants

main.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "constants.h"
#include <iostream>

int main()
{
std::cout << "Enter a radius: ";
float radius{ 0.f };
std::cin >> radius;

std::cout << "The circumference is: "
<< 2.f * radius * constants::PI
<< std::endl;

return 0;
}

constants.h를 어디서 인클루드하던, 상수들은 단 한번만 인스턴스화된다.
만약 constants.h가 수정된다면 이를 포함하는 다른 모든 파일들이 재컴파일 될 것이다. 만약 자주 변경이 필요한 상수를 사용한다면 별도의 헤더파일에 옮기는 방법이 컴파일 시간을 줄이는데 도움이 될 수 있다.


References

Author

Joyus.Gim

Posted on

2022-04-17

Updated on

2022-07-19

Licensed under

Comments