728x90
반응형

const의 매력

'의미적인 제약'을 소스코드 수준에서 붙인다는 점

컴파일러가 이 제약을 단단히 지켜준다는 점

즉, 어떤 값이 불변이어야 한다는 제작자의 의도를 컴파일러 및 다른 프로그래머와 나눌 수 있는 수단이 된다는 점이 매력적이라 볼 수 있다.

 

const는 팔방미인

클래스 바깥에서 전역 혹은 네임스페이스 유효 범위의 상수를 선언(정의)하는 데 사용할 수 있으며 파일, 함수, 블록 유효 범위에서 static으로 선언 한 객체에도 const를 붙일 수 있다.

클래스 내부에서는 정적 멤버 및 비정적 데이터 멤버 모두를 상수로 선언할 수 있다.

 

Q. 포인터는 어떨까?

char greeting[] = "Hello";

char* p = greeting; // 비상수 포인터, 비상수 데이터
const char* p = greeting; // 비상수 포인터, 상수 데이터
char* const p = greeting; // 상수 포인터, 비상수 데이터
const char* const p = greeting; // 상수 포인터, 상수 데이터

여기서의 핵심은 const가 *(포인터) 왼쪽에 있느냐 오른쪽에 있느냐에 대한 이야기이다.

왼쪽은 포인터가 가리키는 대상이 const이고, 오른쪽은 포인터 자체(주소)가 const이다.

포인터가 가리키는 대상을 상수로 만들 때 const를 사용하는 스타일은 조금씩 다른데, 위치가 달라도 의미적인 차이는 없다.

즉, 아래의 함수들이 받아들이는 매개변수 타입은 모두 똑같다.

void f1(const Widget *pw);
void f2(Widget const *pw);

 

STL 반복자(iterator)

STL 반복자는 포인터를 본 뜬것이기 때문에 기본적인 동작 원리가 T* 포인터와 흡사하다. 즉, 어떤 반복자를 const로 선언하는 것은 포인터를 상수로 선언하는 것(T* const)과 같다.

std::vector<int> vec;

const std::vector<int>::iterator iter = vec.begin(); // T* const 처럼 동작(주소가 상수)
*iter = 10; // 문제없음. iter가 가리키는 대상을 변경
++iter; // iter는 상수이기 때문에 Error

std::vector<int>::const_iterator cIter = vec.begin(); // const T* 처럼 동작(값이 상수)
*cIter = 10; // *cIter가 상수이기 때문에 Error
++cIter; // 문제없음. cIter 변경

 

함수 선언에 쓰는 const의 강력한 용도

함수 선언문에 있어서 const는 함수 반환 값, 각각의 매개변수, 멤버 함수 앞에 붙을 수 있고 함수 전체에 대해 const의 성질을 붙일 수 있다.

const를 붙여서 아래와 같은 실수를 막을 수 있는데

class Rational {...};

const Rational operator*(const Rational& lhs, const Rational& rhs);

--- 사용자 정의타입 특징으로 인해 발생하는 실수 ---
Rational a, b, c;
(a * b) = c; // a*b의 결과에 operator= 호출

if(a * b = c) // 비교하려고 했지만..

기본 제공 타입이었다면 문법 위반에 걸리지만 사용자 정의 타입들의 특징 중 하나인 기본 제공 타입과의 비호환성을 피한다는 것으로 인해 두 수의 곱에 대한 대입 연산이 되도록 놓아진다. 이러한 경우를 막기 위해 반환 값을 const로 정해놓으면 된다.

 

상수 멤버 함수

멤버 함수에 붙는 const 키워드의 역할은 "해당 멤버 함수가 상수 객체에 대해 호출될 함수이다"라는 사실을 알려주는 것이다.

const 함수의 중요함

1. 클래스의 인터페이스를 이해하기 좋게 하기 위해서 인데, 그 클래스로 만들어진 객체를 변경할 수 있는 함수는 무엇이고 변경할 수 없는 함수는 무엇인가를 사용자 쪽에서 알 수 있기 하기 위해 사용

2. 이 키워드를 통해 상수 객체를 사용할 수 있게 한다.

추가로 const 키워드가 있고 없고의 차이만 있는 멤버 함수들은 오버 로딩이 가능하다.

class TextBlock
{
public:
	const char& operator[](std::size_t position) const {return text[position];} // 상수 객체에대한 operator
	char& operator[](std::size_t position) {return text[position];} // 비상수 객체에 대한 operator
    
private:
	std::string text;
}

--- 사용 ---
TextBlock tb("Hello");
std::cout<< tb[0]; // TextBlock::operator[]의 비상수 멤버 호출

TextBlock ctb("Hello");
std::cout<< ctb[0]; // TextBlock::operator[]의 상수 멤버 호출

 

비트 수준 상수성(bitwise constness) = 물리적 상수성(physical constness)

어떤 멤버함수가 정적 멤버를 제외한 그 객체의 어떤 데이터 멤버도 건드리지 않아야 그 멤버 함수가 'const'임을 인정하는 개념

즉, 그 객체를 구성하는 비트들 중 어떤 것도 바꾸면 안 된다는 것

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

class CTextBlock
{
public:
	char& operator[](std::size_t position) const { return pText[position]; } // 부적절하지만 비트수준 상수성이 있어서 허용되는 operator[]의 선언
    
private:
	char *pText;
}

----
const CTextBlock cctb("Hello");
char *pc = &cctb[0];
*pc = 'J'; // cctb = "Jello"

 

논리적 상수성(logical constness)

상수 멤버 함수라고 해서 객체의 한 비트도 수정할 수 없는 것이 아니라 일부 몇 비트 정도는 바꿀 수 있되, 그것을 사용자 측에서 알아채지 못하게만(은닉)하면 상수 멤버 자격이 있다는 것

class CTextBlock
{
public:
	std::size_t length() const;
    
private:
	char *pText;
    
	multable std::size_t textlength;
	multable bool lengthIsValid;
};

std::size_t CTextBlock::length() const
{
	if(!lengthIsValid)
    {
    	textlength = std::strlen(pText);
        lengthIsValid = true;
    }
    
    return textlength;
}

여기서 mutable은 비정적 데이터 멤버를 비트 수준 상수성에 관계없이 수정이 가능하도록 하게 해주는 키워드임.

 

상수 멤버 및 비상수 멤버 함수에서 코드 중복 현상을 피하는 방법

class TextBlock
{
public:
	const char& operator[](std::size_t position) const {return text[position];}
	char& operator[](std::size_t position) {return text[position];}
    
private:
	std::string text;
}

위의 operator [] 코드를 보면 동일한 동작이지만 const 유무로 인해 중복된 코드를 작성하게 된다. 이는 컴파일 시간, 유지보수, 코드 크기 증가 등을 부르기 때문에 핵심 기능을 하나 구현하고 아래와 같이 이를 호출하는 식으로 만든다면 더 좋은 방향을 가지게 된다.

class TextBlock
{
public:
	const char& operator[](std::size_t position) const // 이전과 동일
    {
    	return text[position];
    }
    
	char& operator[](std::size_t position) // 상수버전 op[]를 호출하고 끝
    {
    return const_cast<char&> // op[]의 반환 타입에 캐스팅을 적용, const를 떼어냅니다.
    (
    static_cast<const TextBlock&> // *this의 타입에 const를 붙입니다.
    (*this)[position] // op[]의 상수버전을 호출합니다.
    ); 
    }
}

이와 반대로 비상수 멤버 함수를 호출하면 안 되냐는 생각을 할 수 있는데, 상수 멤버 함수는 해당 객체의 논리적인 상태를 바꾸지 않겠다고 약속했기 때문에 상수 함수에서 비상수 함수를 호출하면 틀리다.

 

요약

  • const를 붙여 선언하면 컴파일러가 사용상의 에러를 잡아내는 데 도움을 준다. const는 어떤 유효 범위에 있는 객체에도 붙을 수 있으며, 함수 매개변수 및 반환 타입에도 붙을 수 있고, 멤버 함수에도 붙을 수 있다.
  • 컴파일러 쪽에서 보면 비트 수준 상수성을 지켜야 하지만, 프로그래머는 논리적인(개념적인) 상수성을 사용해서 프로그래밍해야 한다.
  • 상수 멤버 및 비상수 멤버 함수가 기능적으로 서로 똑같게 구현되어 있을 경우에는 코드 중복을 피하는 것이 좋은데, 이때 비상수 버전이 상수 버전을 호출하도록 만들어라.
728x90
반응형