[Advanced C++] 16. Share const global variable, inline variable, static local variable, using, Unnamed namespace & inline namespace

dev.kelvin·2025년 1월 22일
1

Advanced C++

목록 보기
16/74
post-thumbnail

1. Share const global variable

전역 상수 공유

프로그래밍을 하다보면 pi나 gravity와 같은 특정 상수값을 코드 전체에서 사용해야 할 경우가 생긴다

가장 기본적인 방법은 .h에 전역 상수를 namespace안에 만들어 해당 .h를 사용하는 소스파일에서 include 한 후 사용하는 방법이다

	//math.h
    namespace Math
    {
    	constexpr double pi{ 3.141592 };
    }
    
    //main.cpp
    #include "math.h"
    
    Math::pi;

math.h가 방대해지고 내부의 상수 변수 하나를 변경하려고 하면 해당 .h가 include된 모든 소스파일을 전부 recompile해야 한다, 이는 빌드 시간에 굉장히 큰 영향을 미칠 수 있다

만약 상수 값을 적극적으로 변경하거나 추가하는 경우가 많다면 다음과 같은 방법이 더 효율적이다

바로 extern 키워드를 이용하여 전역 상수 변수를 Internal Linkage에서 External Linkage로 변경하는 것이다

	//math.h
    namespace Math
    {
    	extern const double pi;
    }
    
	//math.cpp
    namespace Math
    {
    	extern constexpr double pi{ 3.141592 };
    }
    
    //main.cpp
    #include "math.h" //math.h를 include하여 전부 전방선언한다
    
    Math::pi;

이렇게 하면 pi를 수정한다고 해도 math.cpp만 recompile되고 다른 소스 파일들은 recompile되지 않는다 (math.h를 include했기 때문)

하지만 단점이 존재한다, 우선 상수 전역 변수 구현부에서는 constexpr로 상수 표현식이 들어가지만 전방선언에서는 constexpr을 사용할 수 없으니 상수 표현식이 아닐 수 있다 (그만큼 컴파일 타임 최적화도 불가능할 수 있다)

또한 배열의 크기와 같은 상수를 사용할 때 위와 같은 방식으로는 사용할 수 없다 (math.h를 include하여 main.cpp에서는 단순 전방선언이기 때문에 실제 값은 정의를 타고 봐야한다, 이는 컴파일타임에 확인이 불가능하여 사용할 수 없다 -> 사실 이래서 constexpr변수는 헤더와 소스파일을 분리해서는 안된다, 헤더에서 선언과 정의가 같이 들어가야함)

inline variable

inline variable은 inline function과 같은 방식으로 정의가 동일하다면 여러개의 정의를 가질 수 있는 변수이다 (ODR 위배X)

inline은 기본적으로 External Linkage 속성이기 때문에 Linker에서 볼 수 있다 (중복 제거를 위해 Linker가 봐야함)

inline variable을 통해 위의 상수 전역변수 사용 방식의 일부 단점을 해소할 수 있다

	//math.h
    namespace Math
    {
    	inline constexpr double pi{ 3.141592 };
    }
    
    //main.cpp
    #include "math.h"
    
    Math::pi;

물론 이러한 방식은 math.h의 상수값이 수정되면 math.h가 include된 소스파일 전체에서 recompile이 발생하는 단점이 있다

constexpr 자체를 include할 수 있기 때문에 상수 표현식으로서 사용이 가능하다

만약 전역 상수가 필요할 때는 이 방법을 가장 권장한다 (constexpr은 기본적으로 inline이기 때문에 따로 붙힐 필요는 없다)


2. static local variable

static

C++에서 static은 다양한 의미를 가진다

  1. 전역변수의 생존범위
    전역변수의 생존범위에서의 static의 의미는 전역변수는 프로그램의 시작에 생성되고 종료때 소멸된다는 의미
  2. 전역 식별자
    static 전역변수,함수는 Internal Linkage 속성이 된다

이제 지역변수에 static에 대해 정리해보자

지역변수에서의 static

지역변수는 선언된 { }가 생존범위이다, 이때 지역변수에 static 키워드를 사용하면 생존범위가 정적 생존 범위로 변경된다 (지역변수도 전역변수와 마찬가지로 프로그램 시작 시 생성, 종료 시 소멸됨)

	int foo()
    {
    	int value{ 0 };
        ++value;
        
        return value;
    }
    
    int main()
    {
    	foo();
        foo();
        foo();
    }

위 코드의 결과는 1 1 1이 나오게 된다, value는 foo()에 선언된 지역변수이기 때문에 { }에 맞춰 계속 새롭게 생성, 소멸되기 때문이다

	int foo()
    {
    	static int value{ 0 };
        ++value;
        
        return value;
    }

만약 지역변수를 static으로 지정한다면 결과값은 1, 2 ,3이 나오게 된다

static 지역변수는 초기화가 가능하고 초기화 하지 않으면 0으로 기본 초기화된다 (처음 생성될 때 초기화 되고 그 이후에 재초기화 되지 않는다)

전역변수에 g prefix를 붙이는 것 처럼 static변수에는 s prefix를 붙이는 경우가 많다

이러한 static 지역변수를 사용하는 대표적인 case는 고유 ID생성 case이다

static local const variable

local const나 local constexpr 변수도 static으로 만들 수 있다

한번 초기화할 때 코스트가 높은 객체등에 자주 사용한다 (한번 초기화 해놓고 계속 사용)

non-const static 지역변수 사용 시 주의할 점

함수 내부 로직을 non-const static 지역 변수에 의해 변경되지 않도록 하는게 좋다

예를 들어 non-const static bool 변수로 함수 로직에 영향을 주게 되면 같은 함수를 써도 다른 기능을 처리하게 된다, 이는 프로그래머의 의도와 다르게 동작하기 쉽다


3. using

using

고전 C++에서는 std의 모든 식별자 이름이 global namespace에 존재했다, 하지만 C++의 크기가 점점 커지면서 stadard library의 식별자와 프로그램의 식별자의 식별자 명명 충돌이 자주 발생하게 되었다

이를 해결하기 위해 namespace 시스템이 도입되고 std의 모든 기능이 global namespace에서 std namespace로 옮겨지게 된 것이다

결국 std의 기능에 접근하기 위해 std::???으로 전부 변경이 되어야 했다, 이는 기존 코드에 큰 위험이었다, 그 후 이러한 문제를 using이 해결했다

C++에서 이름은 Qualified Name과 UnQualified Name으로 나눌 수 있다

Qualified Name (한정된 이름)은 이름 앞에 범위를 명시해 해당 이름이 어디에 속해있는지 알려준다
ex) std::cout

UnQualified Name (비한정 이름)은 그냥 이름만 쓰는 경우이다
ex) x, y, cout

namespace에 있는 내용을 매번 ::으로 범위지정하여 사용하는건 좀 불편할 수 있다, 이때 using을 사용한다

using declaration

	#include <iostream>
    
	using std::cout; //std namespace에 있는 cout을 using으로 선언
    cout << "Test";

이러한 using 선언은 컴파일러에게 해당 내용을 사용할 것이라고 알려준다, 위의 코드는 컴파일러에게 std namespace에 있는 cout을 사용할 것이라고 알려주는 것이다

그렇기 때문에 cout만 써도 std::cout으로 컴파일러는 인식한다

이때 만약 cout이 명명충돌이 발생한다면 컴파일 에러가 발생한다

	using std::cout;
   
   	int cout = 30; //redefinition! (error)
    cout << "Test";

이러한 using선언은 선언 지점부터 선언 끝 범위까지 활성화 된다 ({ }안에 선언했으면 선언지점부터 ~ }안에서만 유효함, declaration과 directive 둘다 동일)

물론 std::cout보다 명시적이진 않지만 안전한 편이다

using directive

using declaration과 달리 using directive는 주어진 namespace 내부의 모든 식별자가 해당 using directive의 범위에서 참조될 수 있게 해준다 (using declaration은 namespace에서의 지정한 식별자만 참조가 가능)

	#include <iostream>
    
    int main()
    {
    	using namespace std;
        cout << "HI" << endl;
        
        return 0;
    }

std::cout, std::endl을 명시적으로 using declaration 하지 않고 std namespace의 모든 식별자 참조가 가능해진걸 볼 수 있다

이러한 using directive 방식은 위에 작성했던 std가 namespace로 이동하게 되어 해결책으로 나온 방법이다, 모든 UnQualified Name들을 Qualified Name으로 변경하기 힘들기 때문에 이러한 방법이 나온것이다
(cout 사용처를 전부 std::cout으로 변경하기 힘들기 때문에 using namespace std;로 using directive한 것임)

하지만 이러한 using directive는 지양해야 한다

우선 using directive로 namespace의 모든 식별자들을 참조할 수 있는데 이는 꼭 필요하지 않은 식별자에도 참조할 수 있기 때문이다, 이는 곧 명명충돌로 이어진다 (당장은 명명 충돌이 나지 않아도 추후에 업데이트 시 명명 충돌의 가능성이 있다 (third-party lib을 사용하면 더욱 더)

namespace Foo와 Goo에 동일한 변수명이 있고 이 namespace들을 통째로 using directive했기 때문에 a 식별자가 모호해져 컴파일 에러가 발생한다 (컴파일러는 a를 Foo의 a인지 Goo의 a인지 알 수 없다)


마찬가지로 cout()이라는 함수가 전역에 선언되어 있고 std namespace를 using directive했기 때문에 cout이 모호해진다

이를 명확하게 해주면 다시 컴파일 에러가 발생하지 않는다

이러한 using directive는 명확한 범위지정자 prefix가 없기 때문에 해당 함수가 어떤 lib에 들어있는지 알기 어렵다

using 사용 시 주의할 점

using 구문은 다른 파일의 소스코드에 영향을 미칠 수 있는 범위에 사용하면 좋지 않다
ex) .h파일, global namespace

.h파일에 using 구문이 들어가게 되면 해당 .h파일을 include하는 모든 소스 파일에서 이 using이 사용되게 되기 때문이다 (의도하지 않은 명명충돌, 동작이 일어날 수 있다)

	//A.h
    using namespace std;
    
    //main.cpp
    #include "A.h"
    cout << "Hi" << endl;
    
    //B.cpp
    #include "A.h"
    
    void Foo()
    {
    	int cout = 30;
        cout << "Foo"; //error
    }

또한 #include 이전에 using을 사용하는건 좋지않다

	//FooInt.h
    namespace Foo
    {
        void print(int); //함수 오버로딩
    }
    
    //FooDouble.h
    namespace Foo
    {
        void print(double); //함수 오버로딩
    }
    
    //main.cpp
    #include "FooDouble.h"
    using Foo::print;
    #include "FooInt.h"
    
    int main()
    {
    	print(10); //print(int)가 호출되어야 하지만 print(double)이 호출된다
    }

이는 FooInt.h가 include되기 전에 using Foo::print가 실행되었기 때문에 컴파일러는 FooDouble.h에 있는 print를 사용하게 되기 때문이다

꼭 using directive를 사용해야 한다면 { }으로 범위를 잘 지정해서 사용하는게 좋다

    int main()
    {
        {
            using namespace Foo;
            //여기서만 Foo namespace의 모든 식별자에 접근이 가능함
        }

        {
            using namespace Goo;
            //여기서만 Goo namespace의 모든 식별자에 접근이 가능함
        }
        
        return 0;
    }

사실 애초에 using declaration을 사용하거나 범위지정연산자 ::을 매번 사용한다면 이럴일이 없다


4. Unnamed namespace

Unnamed namespace

namespace를 이름 없이 사용도 가능하다

	namespace
    {
    	void Foo()
        {
        	std::cout << "Foo()" << '\n';
        }
    }
    
    int main()
    {
    	Foo(); //Without namespace prefix
    }

사용할때도 namespace를 명시하지 않아도 사용가능하다

이렇게 Unnamed namespace에 선언된 모든 컨텐츠는 부모 namespace의 일부로 간주된다 (여기서는 global namespace)

지금 보면 딱히 Unnamed namespace가 쓸모 없을 수 있다, 하지만 Unnamed namespace에 선언된 모든 컨텐츠는 Internal Linkage 속성을 가진다 따라서 해당 Unnamed namespace가 있는 파일 외 다른 파일에서는 링크할 수 없다

따라서 Unnamed namespace 내부의 모든 함수들은 static 함수로 정의하는것과 같다

	static void Foo() //이거랑 같음
    {
    }

이러한 Unnamed namespace는 해당 파일에 Internal Linkage 속성의 데이터가 많이 있을때 사용한다 (개별적으로 전부 static 처리를 하는것보다 클러스터링에 좋다 (유사한 속성의 데이터를 그룹화 시키는것))

inline namespace

다음과 같은 코드를 보자

	void Foo()
    {
    	std::cout << "Test1" << std::endl;
    }
    
    int main()
    {
    	Foo();
    }

이 코드는 Test1을 콘솔에 출력한다, 근데 만약 Test2라고 변경하고 싶다면 어떤 방식으로 하는게 좋을까?

다양한 방법으로 Foo1, Foo2, Foo3를 만들고 기능을 수정하여 다양한 버전의 함수를 만드는 방법도 존재한다, 하지만 이는 절대 좋은 방법이 아니다

이럴때 사용하는게 inline namespace이다, 일반적으로 이러한 컨텐츠 버전을 지정하는데 사용되는 namespace이다

	inline namespace V1
    {
    	void Foo()
        {
        	std::cout << "Test1" << std::endl;
        }
    }
    
    namespace V2
    {
    	void Foo()
        {
        	std::cout << "Test2" << std::endl;
        }
    }
    
    int main()
    {
    	V1::Foo(); //Test1
        V2::Foo(); //Test2
        
        Foo(); //Test1
    }
    

우선 inline namespace는 Unnamed namespace와 유사하게 namespace내부의 모든 컨텐츠를 부모 namespace의 일부로 간주한다, 하지만 Linkage에 영향을 미치지는 않는다

따라서 inline namespace V1은 gloabl namespace의 일부로 간주되어 Foo()가 namespace prefix나 ::가 없이 호출이 가능하며 V1의 Foo()가 호출되는것이다

이렇게 특정 버전을 만들어 놓고 기본 버전을 inline namespace에 넣은 후 특정 버전은 namespace::로 접근하여 사용하게 하면 된다

Unnamed namespace는 inline 이 될 수 있고 inline namespace와 Unnamed namespace는 중첩이 가능하다

	namespace V1
    {
    	void Foo()
        {
        	std::cout << "Test1" << std::endl;
        }
    }
    
    inline namespace
    {
    	void Foo()
        {
        	std::cout << "Test2" << std::endl;
        }
    }
    
    int main()
    {
    	V1::Foo(); //Test1        
        Foo(); //Test2
    }
	namespace V1
    {
    	void Foo()
        {
        	std::cout << "Test1" << std::endl;
        }
    }
    
    inline namespace V2
    {
    	namespace
        {
            void Foo()
            {
                std::cout << "Test2" << std::endl;
            }	
        }
    }
    
    int main()
    {
    	V1::Foo(); //Test1        
        Foo(); //Test2
    }
profile
GameDeveloper🎮 Dev C++, DataStructure, Algorithm, UE5, Assembly🛠, Git/Perforce🌏

0개의 댓글