본문 바로가기

Effective C++/1. C++에 왔으면 C++ 법을 따릅시다

항목 3: 낌새만 보이면 const를 들이대 보자!

cosnt = 외부 변경을 불가능 하게 하는 keyword

 

=> 값이 변하면 안되는게 맞다면, 우리도 반드시 지켜야 한다. 이렇게 해야 컴파일러가 const 지키는데 거들 있기 때문이다.

 

 

char text[] = "hello";

 

char *p = text;

비상수 포인터, 비상수 데이터

 

const char *p = text;

비상수 포인터, 상수 데이터

 

char * const p = text;

상수 포인터, 비상수 데이터

 

const char * const p = text;

상수 포인터, 상수 데이터

 

 

  1. iterator =: pointer

 

std::vector<int> vec;

int arr[] = {1,2,3};

const std::vector<int>::iterator iter = vec.begin();     <->   const int* parr = arr;

 

*iter = 10; // ok

++iter;     // error          

 

 

cstd::vector<int>::const_iterator citer = vec.begin();     <->   int* const cparr = arr;

 

*citer = 10; // error

++citer;     // ok          

 

 

  1. 함수 선언에서의 const

 

 

이럴일은 없겠지만, 만약  if(a * b = c) 이런 식으로 == 써야 하는데 오타로 = 썼다?  built-in type 이였으면 바로 에러를 날렸겠지만, operator* 리턴 객체가 const 아니였으면 에러가 나지 않았을 것이다. 이런 경우를 미연에 방지

 

  1. 상수 맴버 함수

 

상수 맴버  함수 = const 멤버함수와 const 객체 (tistory.com)

 

const 키워드가 있고 없고의 차이만 있는 맴버 함수들은 오버로딩이 가능

 

 

 

어떤 맴버 함수가 상수 맴버(const) 라는 것이 대체 어떤 의미일까?

 

  1. 비트수준 상수성(bitwise constness == physical constness)

 

어떤 맴버 함수가 객체의 어떤 데이터 맴버도 건드리지 않아야 한다는 의미(static 맴버는 예외)이다. 객체를 구성하는 어떤 bit 건들면 안된다는 뜻이다.

 

그런데, 제대로 const 동작하지 않는데도 비트수준 상수성 검사를 통과하는 맴버 함수들이 적지 않다. 대표적인 예가 어떤 포인터 맴버가 가르키는 대상을 수정하는 맴버 함수들이다.

 

TextBlock 비슷한데, 맴버가 string 아닌 char* 관리되는 클래스가 있다고 가정해보자. 이땐 보통 string 객체를 사용할 없는 C API에도 이것을 있게 하기 위해서이다.

 

 

먼저 operator[] 보면 분명 부적절하다. CTextBlock 내부의 데이터에 대한 참조자를 그대로 리턴하기 때문이다.(const 안붙이고), 하지만 이게 pText 값을 바꾸는 것이 아니기 때문에 bitwise constness 지켜지고 있다. 그래서 컴파일 과정에서 문제가 되지 않는다.

 

이때의 문제점이 main() 드러나는데, 상수 객체를 선언하고, 상수 맴버함수 [] 호출했다. (이렇게 되면 상수 객체 cctb 어떤 맴버도 바꾸지 않겠다는 뜻이다.) 하지만 값이 변하는걸 있다!!

 

 

  1. 논리적 상수성(logical constness)

 

이런 황당한 상황을 보완하는 대체 개념으로 나온게 논리적 상수성인데, 상수 맴버함수라고 해서 객체의 비트도 수정할 없는건 에바고, 일부 비트 정도는 필요에 따라 바꿀 있되, 그것을 사용자측에서 알아차리지 못하게만 하면 상수 맴버함수의 자격이 있다는 것이다.

 

 

다음 예제에서 length 너무 자명하게 bitwise constness 어긴다.

 

 

그래서 컴파일러도 에러를 내는 모습이다. 상수 맴버함수인데 맴버 textLength lengthIsValid 바꿀려 하고 있기 때문이다.

 

그렇지만 CTextBlock2 라는 상수 객체에 대해서는 문제가 없어야 같은 코드이다. 왜냐하면 length() 목적은 그냥 private 맴버인 textLength 값을 리턴하는 것이기 때문이다. (examiner)

 

이럴  어떻게 해야 할까??

 

=> mutable이라는 keyword 사용하면 된다. const 반대임

 

mutable = static 아닌 맴버를 bitwise constness 영향을 안받게 , 어떤 상황속에서도 수정이 가능하게

 

 

이제 textLength lengthIsValid 맴버는 어떤 상황 속에서도 수정 가능, 컴파일 에러가 나지 않는다.

 

 

  1. 상수 맴버  비상수 맴버 함수에서 코드 중복 현상을 피하는 방법

 

mutable 함수 구현단계에서 생각지도 않은 bitwise constness 해결해주지만, 이걸로 const 관련 골칫거리를 완벽하게 해결할 수는 없다. 위쪽 예제(TextBlock1)에서 지금은 특정 문자의 참조자만 반환하지만, 이것 말고도 여러가지를 있다. 이런 저런 코드를 전부 operator[] 상수/비상수 버전에 넣어버리면 코드가 너무 커진다.(코드가 중복됨)

 

 

 

이런 경우 어떻게 중복을 막을 있을까? private 맴버 함수를 별도로 만들어 operator[] 버전에 각각 호출하게 하면 어떨까?

 

 

?? => 이렇게 하면 코드 중복은 해결되지만, 원래 operator[] 호출하던게 operator[] -> duplicatedFunc() 두번 호출 하게 된다.

 

우리가 진짜로 원하건 opreator[] 핵심 기능을 번만 구현해놓고, 이걸 두번 사용하고 싶은 것이다. operator[] 버전만 제대로 만들고 다른 버전은 이것을 호출하는 식으로 만들고 싶다.

 

=> 리턴값만 다른데 const_cast 씌워 리턴하면 안되나?

 

기본적으로 casting 하는 방법은 좋은 아이디어가 아니다. 하지만 코드 중복을 해결해야하는게 크다면, 다음과 같은 방법이 있겠다.

 

 

비상수 operator[] 상수 버전을 호출하는 방법이다. operator[] 상수 버전은 비상수 버전과 비교해서 반환 type const 붙어 있다는 것만 다르다. 여기서 casting 써서 반환 type으로부터 const 껍데기를 없애는 것만이라면 안전하다.  비상수 operator[] 상수 버전을 호출하도록 구현하는 것이다.

 

=> 비상수 operator[] 그냥 (*this)[position] 리턴하면(원래 의도는 상수 버전으로 redirection) 당연히 자기 자신을 호출할태니까 *this type const 바꿔준다. 비상수 -> 상수는 안전한 cast 이므로 static_cast 충분하다. const char& 리턴 객체가 나오면 여기에 const 떼어준다. 여기선 상수->비상수 이므로 const_cast 밖에 없다. TextBlock 같이 객체가 아닌, 단순히 맴버의 부분만 리턴하므로 비교전 안전하다.

 

 

방법을 뒤집어 하는 (, 상수버전이 비상수 버전을 초대하는 방법) 생각할 있는데, 상수 맴버함수는 해당 객체의 논리적인 상태를 바꾸지 않겠다고 컴파일러와 굳게 약속한 함수(logical constness), 비상수 맴버함수는 이런 약속 자체가 없다. 어쩌다가 상수 맴버함수에서 비상수 맴버함수를 호출하게 되면, 수정하지 않겠다고 약속한 객체를 배신(비상수 함수호출)하는 셈이 된다. 상수 맴버함수에서 객체가 변경될 위험이 있는 것이다.

 

그리고 상수 맴버함수에서 비상수 맴버함수를 호출하는 코드를 어떻게든 컴파일할려면 *this 앞에 붙은 const const_cast 통해 떼어내야하는데, 이는 무조건 재앙이다. 그러나 비상수 맴버 함수 안에서는 맴버를 바꾸든 지지든 볶든 아무 상관이 없기 때문에, 거기서 상수 맴버함수를 호출한다고 해서 문제될게 없다.

 

  • const 선언하면 컴파일러가 에러를 잡아내는데 도움을 준다. const 어떠한 유효범위에 있는 객체에도 붙일 있고, 함수 parameter, return type 에도 붙일 있고, 맴버 함수에도 붙일 수 있으니 언제든 낌새만 보이면 붙이자!!
  • 컴파일러 쪽에서는 상수 맴버함수는 반드시 bitwise constness 지켜야 하지만, 우리는 logical constness 사용해 프로그래밍 해야 한다. (mutable)
  • 상수 맴버함수와 비상수 맴버 함수가 기능적으로 서로 똑같이 구현되있을 경우, 코드 중복을 피하기 위해 하나로 합칠려고 하면, 비상수 버전이 상수 버전을 호출하도록 만들자.