대메뉴 바로가기 본문 바로가기

데이터 기술 자료

데이터 기술 자료 상세보기
제목 C++ 프로그래밍 - 상수 멤버 함수(const Member Function)
등록일 조회수 6071
첨부파일  

C++ 프로그래밍

상수 멤버 함수(const Member Function)



C/C++을 비롯해 주요 프로그래밍 언어는 상수(const) 객체를 가지고 있다. 상수의 의미는 불변, 고정의 개념을 가지고 있기에, 상수 객체의 값은 변할 수 없다. 따라서 흔히 자주 언급되는 ‘상수 변수’라는 말 자체가 모순의 의미를 내포한 것이라고 볼 수 있다.



1. 상수 클래스 객체

<리스트 1> 상수 클래스 객체 class CTest { public: CTest() { m_Val = 0; } void Set(int arg) // ③ { m_Val = arg; } void Func() {} // ⑤ int m_Val; }; void main() { const CTest t; // ① t.Set(1); // ② Error t.Func(); // ④ Error }



<리스트 1>의 ①에서 볼 수 있듯이, CTest t는 const가 지정돼 있다. 즉, 객체 t는 더 이상 변경될 수 없다. 클래스 객체가 변할 수 없다는 의미는 객체 멤버들 모두가 변할 수 없음을 의미한다. 따라서 CTest t의 경우 멤버인 m_Val의 값이 변할 수 없음을 의미한다. ②를 살펴보자. t에 대해 멤버 함수 Set을 호출한다. ③의 Set 함수 정의를 확인해보면 멤버 m_Val을 변경하는 것을 확인할 수 있다. 즉, ②가 실행된다면 상수의 의미가 사라지므로 컴파일러는 ②에 대해서 컴파일 에러를 발생시키게 된다. 여기까지는 쉽게 이해할 수 있다. 이번에는 조금 다른 경우인 ④, ⑤를 살펴보자. Func의 함수 정의는 빈 본체이기 때문에 t의 멤버인 m_Val을 전혀 변경하지 않는다. 그럼에도 불구하고 컴파일러는 컴파일 에러를 발생시킨다. 결국 기준이 무엇일까? 기준은 무척 간단하다. 상수 객체는 일반 멤버 함수를 호출할 수 없다는 것이다.

여기서 몇 가지 의문이 들 수 있다. 첫 번째 의문은, 상수 객체는 어떤 멤버 함수도 호출할 수 없으므로 무용지물이 아닌가 하는 점이고, 두 번째는 ⑤의 Func처럼 컴파일러가 충분히 멤버 값의 변경이 없는 함수를 충분히 인지할 수 있음에도 왜 컴파일 금지를 하는가 여부다. 그 외에도 여러 가지 의문이 있을 수 있는데, 그런 모든 의문을 해결하기 위해 등장한 것이 바로 상수 멤버 함수라고 생각하면 된다. 상수 멤버 함수는 상수 객체가 호출할 수 있는 특별한 멤버 함수를 의미한다. 상수 멤버 함수가 어떻게 여러 의문들을 해결하게 되는지는 이 글을 끝까지 읽게 되면 자연스럽게 알게 될 것이다.



2. 상수 멤버 함수

상수 멤버 함수를 만드는 것은 간단하다. 멤버 함수를 선언 및 정의할 경우 함수 인자 정의가 끝나는 곳에 상수를 붙여주면 된다. <리스트 2>를 보면 쉽게 확인할 수 있다.



<리스트 2> 상수 멤버 함수 class CTest { public: CTest() { m_Val = 0; } void Set(int arg) const // ④ { m_Val = arg; // ⑤ Error } void Func() const // ⑥ { } int m_Val; }; void main() { const CTest ct; // ① ct.Func(); // ② OK CTest t; t.Func(); // ③ OK }



<리스트 2>에서 상수 멤버 함수를 확인해보자! ④, ⑥에서 볼 수 있듯이 함수명 끝에 상수를 붙여서 Set과 Func는 상수 멤버 함수가 되었다. ②를 살펴보자! 더 이상 컴파일 에러가 발생하지 않는다. 한가지 더! ct가 상수 객체이기 때문에 상수 함수 Func를 호출할 수 있듯이, 상수 객체가 아닌 t도 ③과 같이 상수 함수 Func를 호출할 수 있다.

간단히 정리하면 상수 멤버 함수는 상수 객체나 일반 객체 모두 자유롭게 호출할 수 있는 것이고, 비 상수 멤버 함수는 오직 비 상수 객체만이 호출할 수 있는 것이다. 상수 멤버 함수를 도입해서 문제가 잘 해결되는 것처럼 보였지만 아직 약간의 문제가 남아있다. 바로 ⑤에서 컴파일 에러가 발생하는 것이다. 이유는 명확하다. Set 함수가 상수로 지정되었지만 막상 함수 본체 안에서는 멤버 m_Val을 변경하고 있기 때문이다. 힘들게 상수 객체가 상수 함수를 호출했는데, 함수 안에서 멤버를 변경시킨다면 상수의 의미가 사라진다. 따라서 컴파일러는 객체의 상태가 변경될 소지가 있는 코드에 대해서는 컴파일 에러를 발생시킨다.



<리스트 3> 상수 멤버 함수 - 허용되지 않는 구문 class CTest { public: CTest() { m_Val = 0; } void Func() {} void Func1() const { Func(); // ① Error } CTest& Func2() const { return *this; // ② Error } CTest* Func3() const { return this; // ③ Error } const CTest& Func4() const { return *this; // ④ OK } const CTest* Func5() const { return this; // ⑤ OK } int m_Val; };



이미 살펴본 것처럼 상수 멤버 함수는 객체를 변경시킬 소지가 있는 일체의 코드도 허용하지 않는다. 조금이라도 낌새가 보이면 여지없이 컴파일 에러가 발생한다. 어떤 경우가 있는지 <리스트 3>을 통해서 살펴보자! ①은 비 상수 함수 Func를 호출한다. 앞에서 살펴보았듯이 상수 객체는 비 상수 멤버 함수를 호출할 수 없다. 따라서 상수 멤버 함수는 함수 본체에서 비 상수 멤버 함수를 호출할 수 없다. 간단히 정리하면 const 멤버 함수는 const 멤버 함수만을 호출할 수 있다. ②, ③은 무척 중요하다. 바로 객체 자신 혹은 객체의 포인터를 반환하는 경우이다. 함수 자체에서 객체를 변경시키지는 않지만, 해당 함수를 호출한 곳에서 객체를 변경할 소지가 있으므로 이와 같은 코드도 허용되지 않는다. 따라서 객체 자신 혹은 포인터를 반환하려면 ④, ⑤와 같이 상수 타입으로 변환해 반환해야 한다. Func4, Func5의 반환 타입에 const가 지정돼 있으므로 아무 문제도 발생하지 않는다.

필자가 처음 상수 멤버 함수에 대해서 알게 되었을 때 인상 깊었던 점이 있는데, 첫 번째는 멤버가 변경될 수 있는 코드를 탐지해 컴파일 에러를 발생시키는 것이 무척 신기했으며, 두 번째는 이렇게 컴파일러가 멤버 변경 코드를 탐지할 수 있다면 굳이 const 지정자를 함수에 쓸 필요가 있을까라는 의문이 들었다는 것이다. 즉, 상수 객체가 어떤 멤버 함수를 호출할 때, 해당 멤버 함수가 멤버를 변경하는지 아닌지를 알 수 있으므로, 함수의 내용에 따라서 컴파일을 허용하거나 막을 수 있지 않을까라는 생각이다.

가령 <리스트 1>을? 각각 멤버를 변경하는 지 여부를 충분히 알 수 있다. 따라서 상수 지정자가 없어도 충분히 Set 호출은 금지하고, Func 호출은 허용할 수 있는 것이다. 결국 const 지정자가 불필요한 것이라고 판단할 수도 있지 않을까? 독자들도 한번 곰곰이 생각해보길 바란다.

필자는 꽤 오랫동안 멤버 함수에 const가 왜 필요한지 전혀 알 수 없었다. 그러나 C++의 멤버 함수의 다양한 호출 방식을 알고 난 이후에 제대로 깨달을 수 있었다. 즉, 상수 객체를 위해 멤버 함수에 반드시 const 지정자가 필요할 수밖에 없다는 것을 알게 되었다. 바로 가상 함수를 사용하는 경우다.



3. 상수 가상 함수

<리스트 4> 상수 멤버 함수 - 상수 필요 이유 class CParent { public: CParent() { m_Val = 0; } virtual void VFunc() // ⓐ { m_Val = 1; } int m_Val; }; class CChild : public CParent { virtual void VFunc() {} // ⓑ }; void main() { const CChild c; const CParent* pP = &c; pP->VFunc(); // ① }



<리스트 4>의 ①에서 컴파일 에러가 발생한다. 왜냐하면 ⓐ, ⓑ에 const가 지정돼 있지 않기 때문이다. 그러나 <리스트 4>는 const가 필요한지를 이론적으로 따지는 경우이므로 컴파일 에러는 일단 무시하자! 즉, 아무 문제없이 잘 돌아간다고 가정하자! 또한 const가 없어도 되는지를 따지는 것이므로 ⓐ, ⓑ에는 const가 지정되지 않았다.

만일 컴파일러가 const 지정이 없어도 함수의 내용을 분석해서 상수 객체에 대해서 적절하게 컴파일 허용 및 금지를 할 수 있다고 가정해보자! ①에서 컴파일러는 어떤 결정을 해야만 하는 것일까? ⓐ는 분명히 멤버 m_Val을 변경하고 있고, ⓑ는 아무 일도 하지 않는다. 결국 컴파일러가 ①이 수행될 때, 객체가 변경될 소지가 있는지를 확인하려면 ⓐ와 ⓑ중에 어떤 함수가 호출되는지를 알아야만 한다. 그러나 VFunc는 가상함수이기 때문에 컴파일러 입장에서는 컴파일 시점에 어떤 함수가 호출되는지 전혀 알 수가 없다. 오직 실행 시점에서야 비로소 ⓑ 함수가 실행되도록 결정될 뿐이다. 즉, const 지정이 없을 경우 컴파일러는 가상 함수에 대해서 멤버의 변경 여부를 분석할 수 없으므로 어쩔 수 없이 const 지정자를 쓸 수밖에 없는 것이다. ①에서 왜 VFunc가 실행 시점에 결정되는지 지금 잘 이해되지 않을 수도 있으나, 마이크로소프트웨어의 필자의 글을 꾸준히 보게 된다면 언젠가 ‘가상 함수’를 상세하게 다루게 될 때 충분히 쉽게 이해할 수 있게 될 것이다.



4. 상수 멤버 함수 우선 순위

상수 멤버 함수는 비 상수 멤버 함수와 완전히 다른 함수로 취급된다. 즉, 인자 타입과 반환 타입, 함수 이름이 똑같아도 const의 유무로 서로 다른 함수로 구분된다. 결국 const가 함수의 시그니처에 포함되며, 함수의 타입을 결정하는 중요한 요소가 된다.



<리스트 5> 상수 멤버 함수 - 우선 순위 class CTest { public: void Func() // ⓐ { cout < < "Non const Function" < < endl; } void Func() const // ⓑ { cout < < "const Function" < < endl; } }; void main() { CTest t; t.Func(); // ① const CTest ct; ct.Func(); // ② }



<리스트 5>에는 같은 이름의 Func가 일반 함수와 상수 함수로 각각 정의돼 있다. 즉, const 여부로 중복 정의가 된다. ①과 같이 Func를 호출할 경우는 우선 순위가 존재한다. 비 상수 객체의 경우 비 상수 함수인 ⓐ를 우선 선택한다. 만약 ⓐ 함수가 정의되지 않았다면 그때는 ①에서 상수 함수인 ⓑ를 선택한다. 그러나 ②의 경우 상수 객체이기 때문에 오직 상수 함수인 ⓑ를 호출하게 된다.



5. 상수 멤버 함수 활용

보통 프로젝트에서 상수 멤버 함수를 구경하기가 쉬운 것은 아니다. 그만큼 많이 사용되지 않는다는 증거다. 그래서 내용을 잘 모르는 경우도 많은 편인데, 그래도 상수 멤버 함수를 알아야 할 필요가 있다. STL에서 의외로 많이 사용되기 때문이다. 간단한 예제를 통해서 상수 멤버 함수가 어떻게 사용되는지 확인해보자!

STL의 컨테이너 중에서 map이 꽤 많이 사용된다. map은 < Key, Value>를 모아놓은 자료구조다. 보통 Key는 숫자나 문자열이 되는 경우가 일반적이다. 그러나 Key는 꼭 숫자나 문자열만 되는 것은 아니다. 필요하면 원하는 타입을 지정할 수도 있다. 단, Key가 되는 클래스 작성시 반드시 추가되어야 할 연산자가 있다.



<리스트 6> STL map - const Member Function 1 class CTest { public: BOOL operator < (const CTest& rhs) const // ⓐ { return m_Val < rhs.m_Val; } int m_Val; }; void main() { std::map< CTest, CTest*> test_map; // ① CTest t; test_map[t] = &t; // ② Error }



<리스트6>에서 볼 수 있듯이, 클래스 CTest가 STL map의 Key 타입으로 사용되려면 반드시 ⓐ와 같은 연산자 ‘< ’의 정의가 추가되어야 한다. 왜냐하면 map은 내부에서 트리 구조를 사용하는데, 트리를 구성할 때 Key들의 대소 비교를 위하여 연산자 ‘< ’를 요구하기 때문이다. 이때 연산자 ‘< ’는 반드시 const 지정이 되어야만 한다. 바로 map이 내부에서 Key의 대소 비교를 할 때 Key를 상수 객체로 취급하기 때문이다. 즉, map의 Key는 상수 객체이기 때문에 오직 상수 멤버 함수만을 호출할 수 있다.



정리

간단히 const 멤버 함수를 정리해보자! const 클래스 객체는 절대로 멤버가 변경되어서 안 된다. 따라서 멤버를 변경하지 않는 const 멤버 함수만을 호출할 수 있으며, const 멤버 함수를 정의할 경우에는 멤버가 변경될 소지가 있는 코드가 사용될 경우 컴파일 에러가 발생한다. const 키워드는 가상 함수로 인해서 반드시 필요하며, STL 컨테이너에서 사용할 클래스를 작성할 경우 const 멤버 함수를 필수적으로 구현해야 하는 경우도 있다.



출처 : 마이크로소프트웨어 8월호

제공 : 데이터 전문가 지식포털 DBguide.net