[Effective C++] 3. 낌새만 보이면 const를 들이대 보자!

C++/Effective C++

2020. 7. 17. 00:23

포인터와 const

 

const를 이용하면 Pointer 자체를 상수로 만들거나 Pointer가 가리키는 데이터를 상수로 만들거나 둘 다 적용을 시킬수 있다.

 

char greeting[] = "hello";

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

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

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

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


void f1(const Widget* pw) == void f1(Widget const* pw) // const위치가 무얼 기준하는지 암기!

const 기준

const가 * 앞에 있으면 데이터를 상수화

const가 * 뒤에 있으면 포인터를 상수화

 

STL과 const

 

STL Iterator도 포인터를 본떠서 기본적인 동작 원리가 T* 포인터와 진짜 흡사하다.

std::vector<int> vec;

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

*iter = 10; // OK!
++iter; // ERROR! iter는 상수!

std::vector<int>::const_iterator cIter = vec.begin();
*cIter = 10; // ERROR! *cIter는 상수!
++cIter; // OK!

 

함수 선언과 const

 

const의 강력한 용도는 함수 선언에서 사용할 때이다. 함수 반환 값을 상수로 정해 주면, 안전성이나 효율을 포기하지 않고도 사용자측의 에러 돌발 상황을 줄이는 효과를 꽤 볼 수 있다.

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

여기서 함수 반환 값이 const인 이유는 다음의 경우를 예방하기 위해서이다.

Rational a,b,c;

(a*b) = c;

상수 객체를 반환하지 않는다면 위의 어처구니 없는 연산식이 허용이 된다.

뿐만아니라, 다음과 같은 사용자의 실수도 허용하게 된다.

if(a*b =c) ...

 

상수 멤버 함수와 const

 

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

이런 함수의 장점은 무엇일까?

(1) 클래스의 인터페이스를 이해하기 좋다

그 클래스로 만들어진 객체를 변경할 수 있는 함수는 무엇이고, 또 변경할 수 없는 함수는 무엇인가를 사용자쪽에서 알수 있다.

(2) 코드의 효율을 위한 방법으로 상수 객체를 사용할 수있다

const가 있는 버젼과 없는 버젼으로 함수 오버로딩이 가능하다

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;
};

사용처는 이렇다.

TextBlock tb("Hello");
std::cout<< tb[0]; // 비상수 멤버 함수 호출

const TextBlock ctb("World");
std::cout<< ctb[0]; // 상수 멤버 함수 호출

// 비상수 멤버 함수 호출
void print(const TextBlock& ctb){

std::cout<< ctb[0];
...
}

 

const의 의미, 상수성

 

1. 비트수준 상수성

 - 다른 말로 물리적 상수성이라 불린다. 비트수준 상수성은 어떤 멤버 함수가 그 객체의 어떤 데이터 멤버도 건드리지 않아야 그 멤버 함수가 'const'임을 인정하는 개념이다. C++에서 정의하고 있는 상수성이 비트수준 상수성이다.

 - 어떤 포인터가 가리키는 대상을 수정하는 멤버함수에 대해서도 비트수준 상수성 검사를 통과한다. 즉, 상수 멤버를 호출했더니 값이 변하는 어처구니 없는 상황이 발생할 수 있다.

class CTextBlock{
public:
...

char& operator[](std::size_t position)const
{return pText[position];}

private:
char* pText;
}

// 구현부

const CTextBlock cctb("hello");

char* pc = &cctb[0];

*pc = 'J'; // 값의 변경이 허용됨

 

2. 논리적 상수성

 - 논리적 상수성은 위와 같은 황당한 상황을 보완하는 대체 개념으로 등장하였다. 객체의 한 비트도 수정할 수 없는 것이 아니라 일부 몇 비트 정도는 바꿀 수 있되, 그것을 사용자측에서 알아채지 못하게만 하면 상수 멤버 자격이 있다는 것이다.

class CTextBlock{
public:
...
std::size_t length() const;

private:
char* pText;
std::size_t textLength;
bool lengthIsValid;
};

std::size_t CTextBlock::length() const{ //const 임을 주의하자!
if(!lengthIsValid){
textLength = std::strLen(pText); //Error !
lengthIsValid = true; //Error!
}
return textLength;

Error를 피하면서 비트 수준의 상수성을 지키기 위한 방법

class CTextBlock{
public:
...
std::size_t length() const;

private:
char* pText;
mutable std::size_t textLength; //mutable
mutable bool lengthIsValid; // mutable
};

std::size_t CTextBlock::length() const{ //const 임을 주의하자!
if(!lengthIsValid){
textLength = std::strLen(pText); //Error !
lengthIsValid = true; //Error!
}
return textLength;

const와 코드 중복

 

상수 객체/ 비상수 객체 함수가 그저 상수 객체/ 비상수 객체를 반환한다는 점을 제외하고 로직이 같음에도 불구하고 코드가 중복적으로 작성되어 있다면 비상수 객체 함수를 상수 객체를 이용해서 반환하도록 하여 코드 중복을 피할수 있다.

 

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;
}

코드 중복을 피하는 방법

class TextBlock{
public:
...

const char& operator[](std::size_t position) const{
... // 생각보다 엄청나게 많은 코드들이 들어갈 수 있다.

return text[position];
}

char& operator[](std::size_t position){
	return const_cast<char&>( static_cast<const TextBlock>(*this)[position]);
}

private:
std::string text;
}

const TextBlock 형식으로 형변환하여 [] 연산을 한 후 const_cast를 통해 const를 제거함으로써 비상수 데이터를 반환하게 된다.

 

이것만은 잊지말자!

 

1. const를 붙여 선언하면 컴파일러가 사용상의 에러를 잡아내는 데 도움을 줍니다. const는 어떤 유효범위에 있는 객체에도 붙을 수 있으며, 함수 매개변수 및 반환 타입에도 붙을 수 있으며, 멤버 함수에도 붙을 수 있습니다.

2. 컴파일러 쪽에서 보면 비트수준 상수성을 지켜야 하지만, 여러분은 개념적인 상수성을 사용해서 프로그래밍 해야 합니다.

3. 상수 멤버 및 비상수 멤버 함수가 기능적으로 서로 똑같게 구현되어 있을 경우에는 코드 중복을 피하는 것이 좋은데, 이때 비상수 버전이 상수 버전을 호출하도록 만드세요.