[C++] 이름과 스코프

C++에서는 많은 것들이 이름을 가질 수 있다. 변수는 모두 이름을 가지고, 객체는 변수를 통해서 이름을 가질 수 있다. 타입도 이름이 있고, 함수도 이름이 있다. 람다 함수는 익명 함수라는 번역 그대로 이름이 없지만, 변수에 할당하여 이름을 통해 접근할 수도 있다. 이 이름을 어디서부터 어디까지 사용할 수 있는가를 그 이름의 스코프라고 하는데, C++은 상황에 따라 다른 스코프를 사용한다. 이번 글에서는 C++의 이름이 가지는 스코프에 관해서 설명하도록 하겠다.

block scope

block scope는 선언된 문장부터 선언된 block이 끝날 때까지 사용할 수 있다. 블록 밖에서 선언된 같은 이름이 있으면 이름이 선언되기 전까지는 블록 밖에서 선언된 이름을 그대로 가지고, 선언된 문장부터 새로 선언한 의미를 가진다.

예를 들어 위와 같은 코드가 있을 때 4번째 줄까지 a는 2번째 줄에서 선언된 a이지만, 4번째 줄부터 block이 끝나는 10번째 줄까지는 4번째 줄에서 선언된 a이다. 또한, block scope를 가지는 이름은 같은 block 안에서 다른 의미를 가지기 위해 다시 사용될 수 없다. 즉, 위에서 5번째 줄에서 10번째 줄까지 사이에서는 a를 다른 의미로 사용할 수 없다.

스코프를 가지는 이름의 대표적인 예제로는 함수 안에서 선언된 변수인 지역 변수(local variable)가 있다. 이때 지역 변수와 automatic storage에 저장되는 지역 객체를 혼동하면 안 된다. 스코프는 어디까지나 컴파일 시 그 이름을 어디까지 사용할 수 있는가 하는 것이지 객체의 라이프타임과는 상관없다. 지역 변수 중에서도 static이나 thread_local 같은 storage를 지정해주는 specifier가 붙은 경우 이 객체들은 automatic storage에 저장되지 않는다.

지역 변수 외에도 typedefusing으로 선언되는 type alias, 함수 안에서 선언되는 classenum 같은 경우도 함수 안에서 선언됐다면 block scope를 가진다. 함수의 인자, for 문에서 선언된 변수, catch 된 exception, C++17이라면 if control 문에서 선언된 변수도 block scope를 가지는데, 이들은 해당 문장이 있는 block이 아닌, 문장 다음에 있는 block에 종속된다.

namespace scope

block scope보다 약간 큰 범위를 가지는 것은 namespace scope다. namespace scope는 이름 그대로 namespace에 선언된 모든 이름을 의미한다. 당연히 이 namespace에는 global namespace도 포함되므로 outermost namespace 밖에 선언된 이름도 namespace scope를 가진다. namespace scope를 가지는 이름은 block scope를 가지는 이름과 마찬가지로 선언된 문장부터 접근할 수 있다. 하지만 block scope와는 다르게 선언된 namespace가 끝나더라도 접근 불가능하지 않다. 아래와 같이 ::(scope resolution operator)를 통해서 접근할 수 있다.

enumeration scope

enumeration scope는 C++11에 처음 들어간 기능으로 enum class가 들어가면서 같이 들어갔다. C++11의 enum class는 scoped enum이라고 부르기도 하는데, 이는 enumerator의 이름이 enumeration에 한정되기 때문이다.

같은 스코프 내에서 선언된 이름을 다시 선언할 수 없다는 규칙은 enumeration scope도 마찬가지므로 enumerator의 이름을 중복해서 사용할 수 없다. 하지만 enum class가 선언된 block scope나 namespace scope의 이름을 더럽히지 않기 때문에 해당 스코프에는 중복되는 이름이 있어도 괜찮다. enumerator를 사용할 때는 enumeration의 이름과 합쳐서 scope resolution operator를 사용한다.

function scope

function hoisting이란 선언된 이름이 마치 함수의 시작 부분으로 옮겨진 것처럼 해석되는 경우를 말한다. JavaScript에서는 모든 변수가 function hoisting 되므로 JavaScript를 써본 사람이라면 익숙한 개념일 것이다. 하지만 마치 JavaScript의 특이한 기능인 것처럼 알려졌기 때문에 C++에서도 function scope에는 function hoisting이 사용된다. 사실 function scope를 사용하는 이름은 label 딱 한 경우뿐이고, 대부분의 경우 goto를 사용하는 것을 피하므로 사용할 일이 거의 없다. 어찌 됐든 이 라벨은 같은 함수 안에서 선언됐다면, 그 선언된 위치와 상관없이 어디서나 라벨을 사용할 수 있다.

class scope

class scope는 클래스 정의 안에서 선언된 모든 이름이 가지는 스코프다. 같은 클래스 안에서 선언됐다면, 클래스의 멤버 변수나 멤버 함수는 물론이고, static 멤버, static 함수, nested class나 class 안에서 정의된 enumeration과 타입들도 모두 같은 class scope로 묶인다. 이때 function scope처럼 모든 이름이 hoisting 된 것으로 해석된다. 예를 들어 아래와 같은 함수가 있을 때, S::f 함수 안에서 i를 사용할 때, 아직 S::i는 선언되지 않았기에 func 함수에 지역 변수로 선언된 i를 의미하는 것 같지만, 실제로는 S 클래스의 모든 변수 선언이 hoisting 됐기 때문에 클래스 S 안에서 사용되는 i는 모두 S::i를 의미한다.

function prototype scope

위에서 이미 함수의 인자는 block scope를 가진다고 말했기 때문에 function prototype scope가 따로 존재한다는 것이 이상할 수 있다. 하지만 이 이름을 잘 보면 답을 알 수 있다. function prototype scope는 function declarator에 한정되는 scope이다. function prototype scope에 해당하는 이름. 즉, 함수의 파라미터는 선언된 순간부터 function declarator가 끝나는 순간까지 사용할 수 있다.

따라서 위와 같은 코드가 있을 때 f 함수의 인자로 선언된 abf의 선언이 끝날 때까지 function prototype scope를 가지고, function body에서는 block scope로 사용된다. 이때 선언된 순간부터 사용할 수 있으므로, a의 기본값으로 사용된 b는 첫 번째 줄에서 선언된 constexpr intb이다.

하지만 위와 같은 코드에서는 b의 기본값으로 a를 사용하기 전에 이미 a를 인자로 선언했기 때문에 첫 번째 줄에서 선언한 a가 아닌 첫 번째 인자인 a가 된다. 이때 a가 무엇인지 컴파일 타임에 알 수 없으므로 이는 컴파일 에러를 발생시킨다.

하지만 위처럼 컴파일 타임에 값을 알 수 있는 경우에는 사용할 수 있다. 이때 sizeof에 사용된 a는 첫 번째 줄에서 사용된 int32_t가 아닌, 첫 번째 인자인 int8_t 이므로 기본값은 1이 된다.

template parameter scope

마지막으로 설명할 스코프는 template parameter scope다. 이름 그대로 template parameter의 스코프를 정해주기 위한 것으로 선언된 순간부터 접근 가능하며, 선언을 포함한 가장 작은 template의 구현이 끝나는 시점까지다. template parameter scope인 이름은 해당하는 스코프가 끝날 때까지 다른 이름으로 사용할 수 없다. 즉, template 함수나 template 클래스 안에서 template parameter의 이름을 다른 이름으로 사용할 수 없다. 하지만 template parameter scope 안에 다른 template parameter scope가 있을 때, 바깥 스코프에서 선언된 이름을, 안쪽 스코프에서 다시 선언하여 사용할 수는 있다.

지금까지 C++이 가지는 스코프의 종류에 관해서 설명하였다. 요약하자면 C++은 block scope, namespace scope, enumeration scope, function scope, class scope, function prototype scope, template parameter scope의 7가지 스코프를 가지고 있으며, 이 중 class scope와 function scope는 선언한 위치에 상관없이 스코프 내에서 모든 언제나 의미를 가지지만, 나머지 5개 스코프는 이름이 선언된 순간부터 의미를 가진다. 또한, namespace scope, enumeration scope, class scope의 경우 스코프 밖에서도 ::(scope resolution operator)를 통해서 접근할 수 있다.

댓글

이 블로그의 인기 게시물

[C++] enum class - 안전하고 쓰기 쉬운 enum

Log Aggregator 비교 - Scribe, Flume, Fluentd, logstash

RAII는 무엇인가

[Python] cache 데코레이터로 최적화하기

[Web] SpeechSynthesis - TTS API