2017-08-13

c++ static

 C++을 처음 배울 때 사람들이 많이 헷갈리는 것 중 하나가 static이다. 사실 이건 배우는 사람의 문제도 아니고 가르치는 방법의 문제도 아니다. 그저 C++에서 static을 다양한 용도로 사용하고 있기 때문이다.

static storage

 static 키워드를 사용하는 한 가지 목적은 지역 변수에 할당할 객체를 static storage로 만들기 위해서다. C++에서 block scope에 선언되는 변수가 가지는 객체는 기본적으로 automatic storage에 선언되어 block이 끝나면 자동으로 소멸한다. 하지만 간혹 특정한 범위 내에서만 접근할 수 있으면서 프로그램이 끝날 때까지 살아있는 객체, 다시 말해 지칭하는 변수는 block scope에 선언됐지만, static storage duration을 가지는 객체가 필요할 때가 있다. 이럴 때 static 한정자를 사용하면 된다. block scope에 선언되는 지역 변수에 static 한정자를 붙이면 이 변수가 가리키는 객체는 static storage duration을 가지고, block이 종료돼도 소멸하지 않는다.

internal linkage

 static 한정자는 namespace scope에 선언되는 변수에도 사용될 수 있다. 하지만 namespace scope에 선언된 변수가 가지는 객체는 기본적으로 static storage에 할당된다. 따라서 namespace scope에 선언된 변수에 붙은 static 한정자는 객체가 할당될 storage에 영향을 주지 않는다. 여기서 static은 다른 의미를 가진다.
 static 한정자는 namespace scope에서는 변수뿐 아니라 함수 선언에도 붙일 수 있다. 이 경우 선언된 변수나 함수의 linkage를 internal linkage로 바꿔서 translation unit 밖으로 나가지 않도록 해준다.

static member

 C++에서 static이 가지는 마지막 의미는 클래스의 멤버 변수나 함수를 전역 멤버 변수와 전역 함수로 만드는 것이다.
 클래스의 전역 멤버 변수는 프로그램이 종료될 때까지 살아있는 객체로, static storage에 할당된다. 전역 멤버 변수는 클래스와 독립적인 생명 주기를 가지기 때문에 전역 멤버 변수가 될 타입은 선언하는 시점에서 complete type일 필요가 없다. 전방 선언된 타입을 이용해 선언하더라도 정의하는 시점에서 complete type을 알기만 하면 된다.
 전역 멤버 함수는 클래스와는 관련 있지만, 객체와 연관되지 않는 함수를 말한다. 즉, 자기 자신(this)을 포인터로 넘겨받지 않으며, virtual function이 되거나 함수의 선언에 const를 붙일 수도 없다.
 또한, 전역 멤버 변수나 전역 멤버 함수는 그 클래스와 같은 linkage를 가진다. 즉, block scope에 선언된 클래스의 전역 함수나 전역 변수는 linkage를 가지지 않고, unnamed namespace 안에 선언된 클래스의 전역 함수나 전역 변수는 internal linkage를 가진다.

 static storage, internal linkage, static member 이 세 가지가 C++에서 static 키워드가 가지는 의미이다. 사실 이 3가지는 완전히 독립된 개념인데 같은 키워드를 사용하였기에 사람들을 헷갈리게 한다.
 C++ 뿐 아니라 다른 언어에서도 이런 경우가 종종 있다. 왜냐하면, 새 키워드를 추가하면 추가된 키워드를 다른 이름으로 사용하던 기존의 코드가 돌아가지 못하게 되고, 이는 하위 호환성을 잃는 결과를 가져오기 때문에 보통 언어에서 새 키워드를 추가하는 것을 싫어하기 때문이다.
 사실 C++에는 이미 unnamed namespace를 사용해서 이름을 internal linkage로 만드는 방법이 있다. 그래서 C++11이 제정될 때 namespace scope에 선언된 이름을 internal linkage로 만들기 위해서 static 한정자를 사용하는 것을 deprecate 시키자는 의견도 있었으나 하위 호환성을 깨지 않는 방향으로 가기 위해 받아들여지지 않았다. 결국, 이 3가지 기능이 전부 남게 되었고, 앞으로도 한동안은 이 중 어느 한 기능도 없어지지 않을 것으로 보인다.

2017-08-11

[C++] internal linkage와 external linkage의 차이

 보통 프로그램은 하나의 소스가 아닌 여러 개의 소스로 구성된다. 이 소스를 C++ 표준에서는 translation unit이라고 하고, 소스를 컴파일하는 과정을 translation이라고 한다. translation의 마지막 단계는 link인데, 이 과정을 제외하고는 모든 translation은 독립적으로 진행된다. 이때 translation unit 내에서 이름이 어느 범위까지 사용될 수 있는가를 정하는 것이 지난번 글에서 설명한 스코프이다.
 이번에 설명할 linkage는 같은 이름이 같은 entity를 지칭하는 범위를 정하는 것이다. 각 이름은 어떤 linkage를 가지는가에 따라서 translation의 마지막 단계에서 어떻게 link 될지 달라진다.
 linkage에는 internal linkage와 external linkage 두 가지 종류의 linkage가 있다. 하지만 모든 이름이 linkage를 가지는 것은 아니다. 우선은 linkage를 가지지 않는 이름에 대해 알아보도록 하겠다.

no linkage

 영어로는 has no linkage라고 하여 has internal/external linkage와 자연스럽게 대구가 된다. 하지만 한국어로 말하면 no linkage를 가진다고 하는 것은 어색하므로 그냥 linkage를 가지지 않는다고 하는 것이 적당해 보인다.
 어찌 됐든 모든 이름이 linkage를 가지는 것은 아니다. block scope에 선언된 이름은 함수와 extern으로 선언된 변수를 제외하면 전부 linkage를 가지지 않는다. 따라서 함수 안에서 선언한 로컬 클래스, enumeration, enumerator, 타입 등은 전부 linkage를 가지지 않는다.
 linkage를 가지지 않는 이름은 선언된 스코프에서밖에 접근할 수 없고, 스코프 밖에서 그 이름을 다시 사용해도 그건 다른 entity를 지칭하는 것이 된다. static 한정자를 사용하여 static storage에 저장한 경우도 마찬가지다. 이는 block scope에 선언된 이름은 함수 안에서만 사용하기 위한 것이기 때문이다.

internal linkage

 internal linkage를 가지는 이름은 외부 translation unit으로 공개되지 않는다. internal linkage를 가지는 이름은 다음과 같다.

  1. namespace scope에 선언된 static specifier가 붙은 이름
  2. namespace scope에 선언된 anonymous union의 데이터 멤버
  3. namespace scope에 선언된 const 혹은 constexpr이 붙고 volatile은 아닌 변수
  4. unnamed namespace에 선언된 모든 이름

 internal linkage를 가지는 이름은 두 개 이상의 translation unit에서 공유할 수 없다. 따라서 다른 translation unit에서 같은 스코프에 같은 이름으로 선언된 변수나 함수가 있어도 물리적으로 이는 다른 객체를 가리킨다. 따라서 헤더 파일에 internal linkage를 가지는 mutable 한 객체를 선언한 경우 읽는 사람이 코드를 잘못 읽을 여지를 줄 수 있기에 namespace scope에 선언되는 mutable 한 객체는 헤더 파일에 선언하지 않는 것이 좋다. 만약 반드시 헤더 파일에 선언해야 한다면, external linkage를 가지도록 해야 한다.

external linkage

 위에서 설명한 internal linkage와 다르게 external linkage를 가지는 이름은 다른 translation unit과 공유된다. 다시 말해서 어떤 이름이 external linkage를 가진다면 그 이름은 서로 다른 translation unit에서 사용되더라도 같은 내용을 가져야 한다.

  1. namespace scoep에 선언 된 이름 중 internal linkage가 아닌 이름.
  2. block scope에 선언된 함수
  3. block scope에 선언된 변수 중에서 extern으로 선언된 변수

external linkage를 가지는 이름은 위와 같다. 만약 external linkage로 선언된 이름이 함수일 경우 이 함수는 다른 translation unit에서 같은 코드 영역을 가리키게 되며, 그 함수에서 선언된 static storage나 thread_local storage에 저장되는 지역 변수도 공유하게 된다. 만약 그 이름이 객체를 지칭하는 변수라면, 다른 translation unit에서 사용될 때 같은 메모리를 여역을 공유하며, 따라서 한 translation unit에서 객체의 값을 수정하면 다른 translation unit에서도 수정된 값을 보게 된다. 또한, external linkage로 선언된 클래스가 있다면 그 클래스의 멤버 함수들과 전역 멤버 변수들도 전부 external linkage를 가지게 된다.

2017-08-02

[C++] 이름과 스코프

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

block scope

 block scope는 선언된 문장부터 선언된 block이 끝날 때까지 사용할 수 있다. block 밖에서 선언된 같은 이름이 있으면 이름이 선언되기 전까지는 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, 함수 안에서 선언되는 class와 enum 같은 경우도 함수 안에서 선언됐다면 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)를 통해서 접근할 수 있다.