c++ const 객체 참조자와 const static 멤버변수
글쓴이: wans038 / 작성시간: 일, 2016/02/21 - 11:00오후
안녕하세요, const에 관해 질문 2가지를 할려고 합니다.
const이놈 때문에 미치겠네요;;
class One { private: int x; public: One(int x) : x(x) { // empty } One(char* name) { // empty } }; int main(int argc, char** argv) { // 성공: 임시객체를 참조 const One& one1(One(1)); // 실패: 저가 생각하는 이유는 당연히 참조자인데 어떻게 // int형 상수 2를 가르킬 수 있겠음! One객체를 가르켜야지 One& one2(2); // 성공: 어라... 어떻게 에러가 안나지? // const참조자 객체 one3가 int형 상수 3을 가르킬 수 있는가...ㅜㅜ const One& one3(3); // 어라... 생성자랑 연관이 있는것 같네... const One& one4("const 미치겠다"); }
주석을 통해 제생각을 써놓았는데
너무 미치도록 궁금합니다...
참조자는 그냥 가르키는 변수나 객체 등 에 새로운 이름을 부여하는거 아닌가요?
그런데 어떻게
const One& one3(5); 가 가능한 걸까요...? 그리고 왜! const가 있어야지 에러가 안날까요?
--------------------------------------------------
class Two { public: const static int INTS = 50; // 선언과 동시 초기화 가능 // const static Two TOWS1(40); // 에러 const static Two TOWS2; static int TEST; public: Two(int x) { // empty } }; const Two Two::TOWS2(30); // 성공... 왜지 int Two::TEST = 0; int main(int argc, char** argv) { // empty }
이유는 알것 같으면서 모르겠습니다.
int Two::TEST = 0; // TEST가 메모리 공간에 저장될때 0으로 초기화 하라!
const static int INTS = 50; // 선언과 동시 초기화 가능
const static Two TOWS1(40); 왜 객체를 생성, 생성자 호출 왜 오류가 날까요?
왜! const static 객체는 왜! 외부에서 초기화를 해야 할까요...
미치겠습니다... const 와 생성자...
저의 한을 풀어주시옵서서
제가 아직 초보도 아닌 입문자라 용어가 머릿속에서 정확히 구분이 안됩니다. 이해 부탁드리면 감사하겠습니다.
죄송하지만 용어 지적도 해주셨으면 감사하겠습니다.
긴 글 읽어주셔서 감사합니다.
Forums:
와! 재밌는 C++ 표준 다이빙이다
첫 번째 질문은 참조자를 rvalue로 초기화하는 경우에 대한 질문이네요.
C++를 쓰면서 자연스럽게 자주 써먹는 기법인데 엄밀하게 따지고 들어가면 참 특이하죠. 누구라도 한 번쯤 의문을 가질 만한 문제가 아닐까 싶습니다.
두 번째 질문은 클래스의 const static 멤버 초기치와 관련한 질문이고요.
표준 안 뒤져봐도 이 질문은 상식선에서 답을 드릴 수는 있는데, 기왕 하는 김에 본격적으로 살펴봐도 재밌을 것 같습니다.
이렇게 딥 다크한 C++ 상세사항에 다이빙하는 거 개인적으로 무척 좋아하는데, 지금은 부득이 시간이 없네요.
대략 이틀정도 안에 다른 답변이 없으면 제가 한번 해보겠습니다. 보장은 못 드리겠습니다만..
제 질문에 관심을 가져주셔서 정말 감사드립니다ㅜㅜ 꼭
제 질문에 관심을 가져주셔서 정말 감사드립니다ㅜㅜ
꼭 제 한을 풀어주시옵서서!
C++03 기준으로 설명드리겠습니다. 1.
C++03 기준으로 설명드리겠습니다.
1. lvalue와 rvalue
뜬금없지만, 아래 예제부터 시작하죠.
C++를 조금이라도 아는 사람이라면, 최소한 전자는 "안 될 것"이고 후자는 "될 것"이라는 느낌을 받았을 겁니다.
하지만 syntax의 측면에서 보면
a + b
와*c
는 둘 다 expression입니다. 그리고 둘 다 int 타입이죠. 근데 왜 전자는 assignment operator의 왼쪽에 올 수 없고, 후자는 올 수 있을까요?혹자는
*c
의 타입이 int가 아니라 int&이고, int에 대한 대입은 불가능하지만 int&에 대한 대입은 가능하기 때문이라고 말할 수도 있겠습니다. 일단 틀린 설명이고, 게다가 너무 앞서기까지 했네요. C++03 표준은 Unary operator *에 대해서 분명히 이렇게 진술합니다. "If the type of the expression is “pointer to T,” the type of the result is “T.”"더 정확한 설명은 이렇습니다.
a + b
의 결과는 rvalue이고,*c
의 결과는 lvalue입니다. 따라서 전자는 assignment operator의 왼쪽에 올 수 없는 반면, 후자는 올 수 있는 겁니다.어떻게 보면 순환 참조같은 느낌이 듭니다. 사실 lvalue의 어원 자체가 "대입 연산자의 왼쪽에 올 수 있는 값", rvalue는 "대입 연산자의 오른쪽에만 있을 수 있는 값"이거든요.
C++에서는 lvalue와 rvalue를 더 근본적인 개념으로 보고, assignment operator의 요구조건을 이에 따라 정의합니다. "All require a modifiable lvalue as their left operand".
"modifiable"에도 주목하세요. 약간 짜증스럽게도, 모든 lvalue가 assignment operator의 왼쪽에 올 수도 없다는 얘깁니다. 뭐 별달리 신기한 얘기를 하려는 건 아니고요...
자, 그럼 C++에서는 무엇이 lvalue인가? 가 선행되어야겠는데, 기대하셨다면 죄송하지만 이걸 간단히 정의하는 건 불가능합니다. 제가 아는 걸 감추는 것도 아니고, 표준에 구멍이 있는 것도 아닙니다만 이렇게밖에 말씀드릴 수 없는 이유가 있어요. 일단...
(1) 모든 expression은 lvalue이거나 rvalue입니다.
(2) rvalue가 필요한 맥락에 lvalue가 나타나면, 그 lvalue는 rvalue로 변환됩니다.
표준은 lvalue가 무엇이고 rvalue가 무엇인지 딱 잘라 정의하는 대신, Expression의 정의 자체에 lvalue와 rvalue의 개념을 마구 뿌려 놓습니다. "이건 lvalue이고 저건 rvalue이고, 이건 lvalue를 요구하고 저건 rvalue를 요구하고" 하는 식으로 말이죠. 그러니 표준을 전부 읽고 이해하지 않는 이상은 짧게 간추릴 수 있을 리가 없죠. 부끄럽지만 제 능력으로는 필요한 부분을 발췌해 오는 정도밖에 할 수가 없네요.
(3) literal은 rvalue입니다. 단 string literal은 lvalue입니다.
(4) nonreference type으로 캐스팅함으로서 얻어진 임시 객체(temporary object)는 rvalue입니다. functional notation을 이용해 명시적으로 생성한 객체도 포함됩니다.
그 외에도 수많은 규칙들이 있는데 오늘은 안 필요하네요.
사실 lvalue니 rvalue니 하는 건 C++ semantics을 정의하기 위한 다분히 인공적인 문법 요소입니다. 이런 걸 자세히 모르고도 단순히 느낌(위에서
a + b
와*c
를 구분할 수 있었던 그 느낌)만으로도 C++를 어느 정도 써먹을 수 있어요. 하지만 C++11에 들어서 C++에 rvalue reference가 등장함에 따라 드디어 이 개념이 전면에 드러났고, 이제는 사실 누구라도 알아두면 꽤 좋을 개념이 되고 말았습니다... 아마도?뭐, C++의 철학이죠. 프로그래머에게 많은 지식을 요구하고, 또 올바르게 적절히 써 줄 거라고 전적으로 신뢰하는 것. 그런 매력에 빠져서 C++ 쓰는 거 아니겠습니까.
2. reference의 initializer
C++의 선언 규칙은 복잡하죠. 애초에 제법 복잡한 편이었던 C의 선언 규칙을 가져온 것부터가 문제의 근원인데, 거기에 reference가 추가되서 더 복잡해졌습니다. C++11부터는 rvalue reference가 붙어서 더 깊어졌죠.
복잡해진 건 그것뿐만이 아닙니다. declarator에 뒤따르는 initializer도 무시할 수 없죠. 대충 절반 이상은 생성자 탓이고, reference 탓도 조금은 있습니다.
reference의 초기화는 엄밀히 말하면 객체를 새로 만드는 것은 아니기 때문에, 표준에서도 별도의 단락으로 뽑아서 다루고 있는데요, 일단 syntax부터 다루죠.
kukyakya님이 말씀하신 대로, reference initializer에서 괄호를 쓴 표기법과 등호를 쓴 표기법 사이에는 차이가 없습니다. (더 엄밀히는, 일반적인 initializer에 대해서 그 둘은 "거의" 차이가 없습니다. "거의"라 함은 예외가 있단 얘기죠. 클래스 타입을 초기화할 때가 예외입니다.)
아; 결국 설명을 다 드려야 되네요. 이게 제일 귀찮은데.
ⅰ) cv-qualifier라는 게 있습니다. const와 volatile의 조합이죠. 총 4개 있습니다. (none), const, volatile, const volatile.
우열관계는 뭐 이렇습니다. (none) < const, (none) < volatile, (none) < const volatile, const < const volatile, volatile < const volatile
모르시면 넘어가세요. 완전성을 기하기 위해 다 설명드리긴 했는데(const는 이번 질문 주제이기도 하고) 제가 이후에 필요한 만큼만 언급할겁니다.
ⅱ) 두 개의 타입, "cv1 T1"와 "cv2 T2"가 있다고 가정하죠. cv1과 cv2는 앞서 설명한 cv-qualifier입니다.
- T1이 T2와 같은 타입이거나 T1이 T2의 base class이면 "cv1 T1"은 "cv2 T2"에 reference-related 되었다고 합니다.
- "cv1 T1"이 "cv2 T2"에 reference-related 되었고, cv1이 cv2와 같거나 혹은 cv2 < cv1이면 "cv1 T1"은 "cv2 T2"에 reference-compatible하다고 합니다.
ⅲ) "cv1 T1"의 레퍼런스가 "cv2 T2" 타입의 expression에 의해 초기화될 경우...
ⅲ-1)
- expression이 비트 필드가 아닌 lvalue이고 "cv1 T1"이 "cv2 T2"에 reference-compatible하거나,
- expression이 클래스 타입이고(즉 T2가 클래스) "cv3 T3" 타입의 lvalue로 암시적 변환될 수 있으며 "cv1 T1"이 "cv3 T3"에 reference-compatible하면
reference는 initializer expression의 lvalue에 directly bound 되거나(첫째 조건) 변환된 결과의 lvalue에 bound 됩니다(둘째 조건)
앞선 조건 중 어디에도 맞지 않을 경우 reference는 non-volatile const여야 합니다. 즉 cv1은 "const"여야 합니다. 그렇지 않으면 곧바로 실패합니다.
ⅲ-2) initializer expression이 rvalue이고, T2가 클래스 타입이며 "cv1 T1"이 "cv2 T2"에 reference-compatible하면, reference는 아래 둘 중 한가지 방법에 따라 bound됩니다.
- reference는 해당 rvalue initializer expression이 나타내는 object 혹은 그 object의 sub-object에 bound됩니다.
- "cv1 T2" 타입의 temporary가 rvalue initializer expression으로부터 복사 생성되며 reference는 이 temporary에 bound됩니다.
특기할 점은, 복사 생성자 역시 rvalue expression을 reference에 bound하려 할 것이기 때문에, 결국엔 첫째 조건을 한 번은 택할 수밖에 없습니다.
ⅲ-3) 여전히 조건에 맞지 않을 경우, "cv1 T1" 타입의 temporary가 initializer expression으로부터 복사 생성됩니다. (non-reference copy initialization) 그리고 reference는 그 temporary에 bound됩니다.
만약 T1이 T2에 reference-related될 경우, cv1은 반드시 cv2와 같거나 혹은 cv2 < cv1이어야 합니다.
3. temporary
한가지 더 짚고 넘어갈 점이 있다면, 앞서 자주 언급된 temporary입니다. C++에서는 다양한 문맥에서 temporary가 발생하게 되는데, 대체로 프로그래머가 뭘 어떻게 해보기도 전에 사라져 버리는 경우가 많습니다.
하지만 temporary의 수명이 (프로그래머가 의식할 수 있을 정도로) 길어지는 경우가 몇몇 있는데요. 그 중 하나를 지금 다루려고 합니다.
"The temporary to which the reference is bound or the temporary that is the complete object to a subobject of which the temporary is bound persists for the lifetime of the reference except as specified below."
temporary가 reference에 bound되었거나, temporary가 complete object이면서 그 sub-object가 reference에 bound되었을 경우, 그 temporary는 reference의 lifetime만큼은 살아있게 됩니다. 예외가 있는데...
1) 생성자의 ctor-initializer를 통해 reference member에 bound된 temporary는 생성자 종료시까지
2) function call 과정에서 reference parameter에 bound된 temporary는 함수 호출을 포함하는 full expression의 completion까지
3) function return statement에서 returned value에 bound된 temporary는 함수가 종료될때까지 살아있습니다.
예외조건에 대해서도 사실 설명을 더 해야 할 것들이 있는데(full expression의 의미라던지), 표준문서에 기반한 설명을 하면서 계속 가지를 치다간 끝이 없으니 관두겠습니다. 이번 질문이랑 관련있는 내용도 아니고요. 알고 싶으시면 표준을 직접 읽으세요.
이론은 여기까지 하고, 이제 질문자님의 코드를 봅시다.
cv1 = const, T1 = One, initializer expression은 One(1)이므로 cv2 = (none), T2 = One이군요. expression은 rvalue이며 뭔 짓을 해도 lvalue로 암시적 변환이 안되므로 ⅲ-1은 물건너갑니다.
근데 reference가 non-volatile const이고 "cv1 T1"이 "cv2 T2"에 reference-compatible이므로 ⅲ-2은 만족하네요.
복사 생성자를 호출하래봤자 One타입 rvalue를 const One &에 bound하려는 시도가 똑같이 이어지므로 많은 구현체에서 temporary를 reference에 직접 bound해버릴 겁니다. 뭐 안 그럴 수도 있고요. temporary의 수명 연장은 덤.
cv1 = (none), T1 = One, initializer expression은 2이고, string literal이 아닌 literal이므로 rvalue입니다. 즉 cv2 = (none), T2 = int. 마찬가지로 int를 가지고 뭔 짓을 해도 lvalue를 만들 수가 없어서(이러려면 int를 받고 One &를 반환하는 암시적 변환 함수가 있어야 합니다) ⅲ-1는 또 물건너갑니다.
근데 이번엔 reference가 non-volatile const가 아니네요? 남은 조건도 다 실패하고 컴파일 에러가 됩니다.
앞서와 같은데 이번엔 reference가 non-volatile const이군요. ⅲ-1이 물건너가는 과정은 생략합니다. T1 = One, T2 = int이므로 여전히 "cv1 T1"은 "cv2 T2"에 reference-compatible하지 않고, 따라서 ⅲ-2도 물건너갑니다.
하지만 int로부터 One을 생성할 방법이 있으므로 ⅲ-3은 만족합니다. 즉 "cv1 T1" 타입의 객체, 그러니까 const One타입의 temporary가 3을 받아 생성되고 reference는 여기에 묶입니다. temporary의 수명 연장은 역시 덤.
아... 이건 또 다른 설명을 필요로 하는 문젭니다. 제가 아까 string literal은 lvalue라고 그랬죠? 더 정확히는 const char [n] 타입의 lvalue인데, 이게 적당한 조건 하에서 const char * 타입의 rvalue로 decay되고, 또 적당한 조건 하에서 char * 타입의 rvalue로 decay됩니다(!) 전자는 별로 신기할 거 없는 array-to-pointer conversion인데 후자는 좀 많이 신기하고(constness가 갑자기 떨어져나가다니!) 사실 deprecated된 C언어의 유산입니다. 오직 string literal에 대해서만 일어날 수 있는 일이죠. 설명을 더 드려야 할 것 같긴 한데 이미 글이 너무 길어지고 있고, 솔직히 저도 귀찮아요. 넘어갑시다.
어쨌거나 ⅲ-1, ⅲ-2 모두 물건너가고 ⅲ-3 조건에 따라 one3의 경우와 마찬가지로 temporary가 생성되어 bound되는 건 직접 확인해보세요. 연습문젭니다. 참고로 직접 컴파일해 보면 괜찮은 컴파일러가 "deprecated된 기능을 사용했느니" 하고 불평해줍니다.
표준은 합의의 결과물이지 합의 과정을 설명해주는 문서가 아닙니다. 다시 말하면, reference의 initialize가 저런 식으로 이루어져야 한다고 말할 뿐, 왜 그렇게 결정했는지에 대해서는 별달리 언급이 없습니다.
그러니 "왜 저렇게 정의되었느냐"고 묻는다면, 제가 적당히 어림짐작해서 알려드릴 수밖에 없을 것 같네요.
말씀하신 대로 reference는 lvalue에 bound되어, 해당 lvalue가 가리키는 object에 대한 별칭처럼 동작하는 게 기본적인 동작입니다. 뭐 예컨대 아래의 call-by-reference처럼.
하지만, 경우에 따라 call-by-value처럼 다양한 형태의 파라미터를 넘겨서 쓰고 싶으면서도, call-by-reference처럼 복사 비용을 안 물고 싶을 때도 있습니다.
이런 경우에 편리하게 쓸 수 있도록, 표준은 non-volatile const reference가 rvalue에 bound되도록 할 수 있게 한 겁니다.
단, 한 가지 주의할 점이 있습니다. non-volatile const reference에 rvalue를 직접 담으려 할 경우에만 표준이 reference가 bound될 대상의 수명을 보장해준다는 거죠.
가끔 non-volatile const reference를 남용하다가 생기는 이런 문제에 대해서는 해당사항 없습니다.
두 번째 질문에 대해서는 나중에 시간 나면 달아드리죠. 글이 너무 길어지니까 힘들어서 여기서 자르겠습니다. 그럼 안녕히.
덧. 혹시 틀린 내용 있으면 지적 및 정정 환영합니다. 저는 정확한 답변을 달기 위해 최대한 노력했지만, 아무것도 보장해 드리거나 책임질 수는 없습니다. 자유 소프트웨어 배포와 비슷한 입장이라고 보면 되겠네요.
const One& one3(3); 위 문장은
위 문장은 다음과 같은 표현입니다.
One의 생성자 One::One(int)가 explicit이 아니기 때문에 위의 코드는 다음과 같습니다.
조금 이해가 됬는니다만....ㅠ제가 국어를 진짜 못하나 봅니다
explicit 키워드는 대입연산자로 생성, 복사를 막는것이 잖아요.
const One& one3(3); == const One& one3 = 3; // 같은 표현
const One& one3 = 3; == const One& one3 = One(3); 같다는 말인가요?
그럼 임시객체를 생성해서 참조하는 것이군요.(따질게 아니지만 c++ 왜이렇게 헷갈리게 만들어 놨을까요...)
즉
const One& one3(3); OR const One& one3 = 3; 는 const One& one3 = One(3) 이므로 (const One& one3(one3) 형태가 아니라)
복상 생성자에 explicit 키워드를 추가하면 에러가 났던것이군요.
근데 한가지 궁금한게 있는데요.
const 참조자로 참조해야지 왜 에러가 안날까요? 임시객체라서 그런가요?
One& one3(One(5)); // 성공 // 임시객체는 아닌것 같고
이유가 뭘까요...누가 저의 한을 제발 풀어주세요!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
explicit은 매개변수가 1개인 생성자를 이용한
explicit은 매개변수가 1개인 생성자를 이용한 implicit 형변환을 막아주는 키워드입니다.매개변수가 1개인 경우는 c++11 표준 이전 내용이었네요. 자세한 내용은 http://en.cppreference.com/w/cpp/language/explicit 참고하세요.
이 경우 not_a_number n = 3; 과 같은 부자연스러운 표현을 막을 수 있습니다.
이와 같은 경우에는 big_integer bi = 3; 과 같은 표현이 가능해야하므로 explicit을 쓰지 말아야겠죠.
임시 변수는 non-const lvalue reference로 바인딩할 수 없고 const lvalue reference나 rvalue reference를 사용해야합니다.
댓글 달기