며칠 전 연산자 오버로딩을 공부하다가 const에 대해 의문이 들고, 또 그에서 파생된 레퍼런스와 관련된 문제때문에 의문점이 계속되어, 고심끝에 도달한 결론을 글로써 남긴다.
보통 const를 단순하게 상수 변수를 위한 키워드 정도로 배우며, 결론부터 말하면 그게 정답이다. 그런데 직접 코드를 짜다보면, 단지 변수를 상수화하는 쓰임이라고 직관적으로 받아들이기 힘든 부분에까지 const는 매우 치밀하게 사용된다. 가장 대표적인 부분이 클래스를 만들 때 method를 설계하는 대목에서이다.
이 글에서 다룰 method, 즉 함수 설계에 있어 const와 엮었을 때 가장 중요한 대전제는 바로,
일반 객체는 const 매개변수, 일반 매개변수 둘 다 매개될 수 있다.
그러나 상수 객체(숫자든, 상수화 변수든 객체든)는 const 매개변수에만 매개될 수 있다.
자, 가장 중요한 핵심 개념을 명시했다. 위의 대전제가 이해가 되었다면 바로 아래 문장으로 넘어가자.
const[1] data-type operator operator-name (const[2] operand) const[3] { function activity ... }
(ex) const Time operator + (const Time&) const { 임시 Time 객체를 이용하여 처리한 뒤 임시 객체를 반환 }
보다시피, 익숙한 연산자 오버로딩 문장이다. 여기서 const는 총 3번 사용되었으며, 각각의 const는 다 쓰임이 다르다. 쉬운 이해를 위해 순서대로 설명하지 않겠다. 여기서는 시간 데이터를 저장하는 자료형 Time을 가정하여, Time 객체끼리의 더하기 연산을 오버로딩하는 상황을 가정해보겠다. 즉, Time 객체 A,B,C가 있다고 가정할 때, A=B+C; 같은 문장이 유효하게끔 하는 처리이다.
[3] 위의 연산자는 클래스 메소드로서 연산자를 정의한 것이다. 따라서 이 const가 영향을 미치는 것은 {} 내에서 활동하는 호출 객체에 영향을 미치는 것이다. 즉 B+C라는 수식이 있다면, B에 영향을 미치는 것이 [3]의 const이다. 호출 객체의 값이 method 실행 과정에서 수정되는 일이 없도록 제한을 걸어준다.
[2] 피연산자에 영향을 미치는 const이다. 즉 B+C라는 수식이 있다면, C에 영향을 미치는 것이 [2]의 const이다. 일반적으로 피연산자는 매개변수로서 넘어올 때 레퍼런스(&)로 넘어오기 때문에 값이 수정될 가능성이 있다. 따라서 const를 걸어줌으로써 값이 수정될 여지를 없애주는 것이다.
[1] 연산이 완료된 뒤 반환(return)되는 값이 수정되지 않도록 해준다. 즉 B+C라는 수식이 있다면, (B+C) = 5; 와 같은 수식이 성립되지 않도록 하는 것이다. 위에 예시에 적었듯이 연산이 완료된 뒤에 반환되는 것은 임시 객체, 즉 변수이다. 지역 변수이기에 오류인 것은 두말할 것도 없고, 설사 전역 변수가 반환된다고 할 지라도 쓰기가 안되게 만들어야 연산 결과를 보장할 수 있다.
오버로딩을 다룰 때에도 const를 제대로 이해해야 실수를 해놓고도 왜 틀렸는지 이해가 안 되는 불쌍사를 미연에 방지할 수 있다. 글의 제목이 연산자 오버로딩과 const가 아닌 데에는 다 이유가 있다. 아래의 경우를 보자.
1 class Time
2 {
3 private:
4 int hour, min, sec;
5
6 public:
7 Time(){ hour = 0; min = 0; sec = 0; }
8 Time(int h, int m, int s){ hour = h; min = m; sec = s; }
9 const Time operator+(const Time &T) const // A=B+C
10 {
11 Time R;
12 R.sec = sec + T.sec;
13 R.min = sec + T.min;
14 R.hour = hour + T.hour;
15 R.min += R.sec / 60;
16 R.sec %= 60;
17 R.hour += R.min / 60;
18 R.min %= 60;
19 return R;
20 }
21 const Time operator+(int s) const // A+5
22 {
23 Time R = *this;
24 R.sec += s;
25 R.min += R.sec / 60;
26 R.sec %= 60;
27 R.hour += R.min / 60;
28 R.min %= 60;
29 return R;
30 }
31 void OutTime()
32 {
33 printf("%d:%d:%d\n", hour, min, sec);
34 }
35};
36
37const Time operator+(int s, const Time &T) //5+A
38{
39 return T + s;
40}
Time 객체에 대해 정의를 하고, Time에서 쓰일 + 연산자에 대한 오버로딩한 코드이다. 모든 경우의 수는 오른쪽 각주에 써놓은 경우들이다. 37번째 줄에 보면 5+A 를 연산하는 경우, 정수와 Time객체 순으로 받은 것은 Time객체와 정수 순서로 다시 연산한 뒤 return하여 코드를 절약하고 있다. 사실상 중개자 역할을 한 것이다. 그런데 여기서 만약에, 21번째 줄의 코드를 다음과 같이 바꿨다고 해보자.
21 const Time operator+(const int s) // A+5
맨 뒤의 const를 삭제한 것 이외에는 일체 동일한 코드이다. 그런데 이 처리를 한 순간 37번째 코드는 먹통이 되어버린다. 왜 에러가 났는지 살펴본다. 컴파일러가 말하길 "이러한 피연산자와 일치하는 "+" 연산자가 없습니다. 피연산자 형식이 const Time + int 입니다"라고 한다. 아하, 39번째 줄에서 보듯이 T는 const가 지정된 상수 객체인데, 방금 고친 코드에서는 이를 일반 객체로 받아버린 것이다. 위에 대전제에서 적어 놓았듯이, 상수 객체는 상수 변수로만 받아야 한다.
그런데 여기서 또다시 의문이 든다. 아니, 위에 보니까, 정수를 매개변수로 받을 때에는 const를 쓰지 않네. 정수라는 건 어쨌든 상수잖아? 그런데 왜 여기서는 const를 쓰지 않는 거야? 그러네, 그러면 거기다가도 const를 써보자. const를 써도 아무 문제 없고, const를 적지 않아도 아무 문제가 없다. 왜 그럴까? 위에서 하나 언급하지 않은 것이 있는데, 매개변수로 Time 객체를 받을 때 모두 레퍼런스 변수(Time&)로 객체를 전달받고 있었다. 즉, 레퍼런스이기때문에 const를 집어넣어야 한다는 것이다. 왜냐고? 레퍼런스로 전달하면 객체 자체를 전달하는 거나 마찬가지이다. 지금 다루고 있는 + 연산에서는 인자들을 쓰기 용도로는 일절 사용하고 있지 않다. 오로지 읽기 전용으로만 사용하고 있으며, 컴파일러에게 이러한 사실을 주지시키는 것이 const의 역할이다. 일반화할 수 있는 제약조건을 성립시키고 공고화하는 것이 컴파일러의 또다른 역할이기에, const 객체는 const 매개변수만이 받을 수 있게끔 견고한 룰을 적용함으로서 프로그래머의 실수를 줄이는 것이다.
그럼 다시 돌아가서, 왜 정수는 const를 안 써도 문제가 안 되는 걸까? 그냥 값(value)만 전달하니까 그렇지. 아마 객체를 레퍼런스로 전달하지 않고 그냥 객체 데이터들을 한꺼번에 전달한다면 그 때는 const를 쓰지 않더라도 문제가 없을 것이다. 값은 상수임은 불변의 진리이고, 컴파일러 또한 알고 있다. 굳이 알려 줄 필요가 없는 것이다.
덧글
시원하게 이해했습니다..