Effective C++ 3rd Edition. Scott Meyers.
C++ 프로그래머의 필독서라고 불리는 Effective C++을 읽고 중요한 내용을 정리한 글 입니다.
Item3. 낌새만 보이면 const를 들이대 보자!
🔻const
const에 대해 생각해 볼 때 가장 멋지다고 말할 수 있는 부분이 있다면, '의미적인 제약'을 소스 코드 수준에서 붙인다는 점과 컴파일러가 이 제약을 단단히 지켜준다는 점이다. 어떤 값이 불변이어야 한다는 제작자의 의도를 컴파일러와 다른 프로그래머와 나눌 수 있는 아름다운 수단이다.
const는 팔방미인! const의 사용처
- 전역 혹은 네임스페이스 유효범위의 상수를 선언(정의)하는데 사용(항목 2)
- 파일, 함수, 블록 유효범위에서 static으로 선언한 객체에도 붙일 수 있음.
- 클래스 내부에서, 정적 멤버 및 비정적 데이터 멤버를 모두 상수로 선언 가능.
- 포인터의 상수 지정.
🔶포인터의 상수 지정
먼저 포인터의 경우를 보자. const의 위치에 따라서 포인터 자체를 상수로, 혹은 포인터가 가리키는 데이터를 상수로 지정할 수 있다.
char greeting[] = "Hello";
char *p = greeting; // 비상수 포인터,
// 비상수 데이터
const char *p = greeting; // 비상수 포인터,
// 상수 데이터
char *const p = greeting; // 상수 포인터,
// 비상수 데이터
const char *const p = greeting; // 상수 포인터,
// 비상수 데이터
const가 *의 왼쪽에 있으면 포인터가 가리키는 대상이 상수인 반면, const가 *의 오른쪽에 있으면 포인터 자체가 상수이다. 양쪽에 다있으면 둘다 상수이다.
포인터가 가리키는 대상을 상수로 지정할 때는 두가지 스타일로 사용할 수 있는데, 의미의 차이는 없다.
void f1(const Widget *pw); //상수 Widget객체에 대한 포인터를 매개변수.
void f2(Widget const *pw); //위와 동일하다. 표현 스타일의 차이일 뿐이다.
그냥 *을 중심으로 왼쪽이면 가리키는 대상, 오른쪽이면 포인터 자체로 생각하면 편할 것 같다.
STL의 반복자(iterator)는 포인터를 본뜬 것이다. 따라서 어떤 반복자를 const로 선언하는 일은 포인터를 상수로 선언하는 것(T* const)과 같다. 즉 가리키는 대상을 바꾸는 건 안되지만, 가리키는 대상 자체를 변경할 수는 있다. 만약 이거도 변경이 불가능한 반복자를 원하면 const_iterator를 쓰도록 하자.
const vector<int>::iterator iter = v.begin();
//iter는 T *const 처럼 동작
*iter = 10; // OK. 가리키는 대상의 내용물 변경가능.
++iter; // 에러. iter는 상수다.
vector<int>::const_iterator cIter = v.begin();
//cIter는 const T*처럼 동작
*cIter = 10; // 에러. *cIter가 상수이다.
++cIter; // OK. cIter는 const가 아니니까 가능.
🔸함수 선언에서 const
함수 선언문에 있어서 const는 함수 반환 값, 각각의 매개변수, 멤버 함수 앞에 붙을 수 있고, 함수 전체에 대해 const의 성질을 붙일 수 있다.
함수 반환 값을 상수로 정해 주면, 안정성이나 효율을 포기하지 않고도 사용자측의 에러 돌발 상황을 줄이는 효과를 많이 볼 수 있다.
class Rational { ... };
const Rational operator*(const Rational& lhs, const Rational& rhs);
이렇게 반환 값을 상수 객체로 설정 해주지 않으면 어떤 일이 일어나는 지 보자.
Rational a, b, c;
...
if((a * b) = c) ...; // a * B에 결과에다가 operator=을 호출한다.
이런 상황이 자주 안날 것 같지만, 나만해도 코딩할 때, 이런 실수를 자주한다. ==을 입력하려했는데 오타가나서 =으로 되는 상황이다. a와 b가 기본 타입이었으면 명백히 문법 위반으로 걸리는 코드이지만, 사용자 정의 타입에서 이런 쓸데없는 경우를 막아놓지 않았으므로, 에러도 안나고 자기가 원하는 결과도 안나와서 어디가 원인인지 또 찾아야되는 상황이 온다. 이러한 상황을 미연에 방지하는 해결책이 바로 반환 값을 const로 해주는 것이다. 상수로 해주면 대입이 에러가 날 것이다.
함수의 매개변수도 수정할 수 없게 하는게 목적이라면 무조건 사용하자.
눈 딱 감고 여섯 글자만 더 늘려보자. c.o.n.s.t
🔸상수 멤버 함수
멤버 함수에 붙는 const 키워드의 역할은 "해당 멤버 함수가 상수 객체에 대해 호출될 함수이다"라는 사실을 알려주는 것이다. 이러한 함수가 중요한 이유는 두가지이다.
- 클래스의 인터페이스를 이해하기 좋게 하기위해서.
- 이 키워드를 총해 상수 객체를 사용할 수 있게하자는 것. 코드의 효율을 위해 굉장히 중요하다. 객체의 전달을 'reference to const'로 하는 것이 프로그램의 실행 성능을 높이는 핵심 기법중 하나기 때문이다. 이 기법을 제대로 활용하려면 당연히 상수 멤버 함수가 준비되어 있어야 한다는 것!
const 키워드가 있고 없고의 차이만 있는 멤버 함수들은 오버로딩이 가능하다. (아주 중요한 성질) 이 개념을 가지고 예제코드를 한번 봐보자.
class TextBlock
{
public:
...
const char &operator[](std::size_t position) const // 상수 객체에 대한 operator[]
{
return text[position];
}
char &operator[](std::size_t position) // 비상수 객체에 대한 operator[]
{
return text[position];
}
private:
std::string text;
};
실제 프로그램에서 상수 객체가 생기는 경우는 1. 상수 객체에 대한 포인터 혹은 2. 상수 객체에 대한 참조자로 객체가 전달될 때이다. 아래의 예제가 그 경우이다.
void print(const TextBlock &ctb) // 이 함수에서 ctb는 상수객체.
{
std::cout << ctb[0]; // TextBlock::operator[]의 상수멤버 호출
ctd[0] = 'x' // 에러 발생. cbt는 상수객체
}
이 코드에서 눈여겨 볼점이 두가지 있는데 첫째, ctb의 값을 수정하려 했을 때, 발생한 에러는 순전히 operator[]의 반환타입이 const char&이기 때문에 생긴 것이고, 둘째로 operator[]의 비상수 멤버는 char이 아닌 char&를 반환한다는 것인데, 참조자를 반환하지 않으면 '값에 의한 반환'을 수행하므로 tb.text[0]의 사본을 수정하므로 의도한 동작이 아닐 것이다.
🔸비트수준의 상수성과 논리적 상수성
어떤 멤버 함수가 상수 멤버라는 것이 어떤의미 일까? 비트수준의 상수성과 논리적 상수성에 대해 알아보자.
비트수준의 상수성 이란 어떤 멤버 함수가 그 객체에 어떠한 데이터 멤버(정적 멤버는 제외)도 건드리지 않아야한다. 즉 그 객제를 1비트라도 바꾸어선 안된다는 것이다. C++에서 정의하고 있는 상수성이 비트수준 상수성이다.
class CTextBlock
{
public:
char &operator[](std::size_t position) const
{
return pText[position];
}
private:
char *pText;
};
하지만 제대로 const로 동작하지 않는데도, 비트수준의 상수성 검사를 통과하는 경우가 있는데, 위의 코드를 보면 char*을 멤버함수로 가지고 있다. 그 객체 내부의 값을 수정해도 포인터값은 변하지 않으므로 비트수준의 상수성을 지키고있고, 컴파일러 선에서는 이것만 점검하면 끝이다. 하지만 이로인해 상수 멤버 함수로도 값을 변경 시킬수 있는 문제가 발생한다.
따라서 이러한 황당한 개념을 보완하기 위해 논리적 함수성이라는 개념이 등장하게 되었다. 상수 멤버 함수라고 해서 객체의 한 비트도 수정할 수 없는 것이 아니라 일부 몇비트 정도는 바꿀 수 있되, 사용자특에 알아채지 못하게만 하면 상수 멤버 자격이 있다는 것이다.
class CTextBlock
{
public:
std::size_t length() const;
private:
char *pText;
std::size_t textLength; // 바로 직전에 계산한 텍스트 길이
bool lengthIsValid; // 길이가 현재 유효한가?
};
std::size_t CTextBlock::length() const
{
if (!lengthIsValid)
{
textLength = std::strlen(pText); // 에러. 상수 멤버 함수안에서는
lengthIsValid = true; // 멤버 변수를 수정할 수 없다.
}
return textLength;
}
위의 코드는 비트수준 상수성에 명백히 어긋나는 코드이다 하지만 상수 객체에 대해서 아무 문제가 없어야하는 코드이다. 하지만 컴파일러는 에러를 쏟아낼 것이고, 컴파일러의 검열을 통과하기 위해 mutable키워드를 사용해주면 된다.
mutable을 비정적 데이터 멤버를 비트수준의 상수성의 족쇄에서 풀어주는 역할을 하는 키워드이다.
class CTextBlock
{
public:
std::size_t length() const;
private:
char *pText;
mutable std::size_t textLength; // mutable로 인해 상수 멤버에서도 수정가능해짐
mutable bool lengthIsValid;
};
std::size_t CTextBlock::length() const
{
if (!lengthIsValid)
{
textLength = std::strlen(pText); // 문제없다.
lengthIsValid = true;
}
return textLength;
}
🔸상수 멤버 및 비상수 멤버 함수에서 코드중복을 피하는 방법
상수 멤버 함수와 비상수 멤버 함수가 하는 일이 똑같을 경우 비상수 멤버함수를 캐스팅해서 상수 버전을 호출하게 하여 코드의 중복을 피할 수 있다.
class TextBlock
{
public:
const char &operator[](std::size_t position) const // same as before
{
...
return text[position];
}
char &operator[](std::size_t position) // now just calls const op[]
{
return const_cast<char &>(
static_cast<const TextBlock &>(*this)[position]
);
}
};
이 챕터를 읽을 때, 이렇게 두번 나누어서 코드를 짜면 귀찮을 것같은데라고 생각했었는데, 정확히 그 가려움을 긁어주엇다!. 굉장히 신박한 방법으로 코드 중복을 없앴다. 비상수 멤버를 const TextBlock으로 casting해주고(같은 타입의 const를 붙혀주는 static_cast는 위험요소가 없음), 그 결과값을 다시 const_cast로 const를 떼주었다. 이렇게 되면 비상수를 상수로 바꾸어 상수 멤버 함수를 수행하고 그 결과를 다시 비상수로 바꾼느 방식으로 코드중복문제를 해결하였다.
주의할 점은 위와 같은 방법은 비상수 -> 상수만 가능하지 상수를 비상수로 바꾸어 위와같이 하는건 문제가 발생한다.
🔻마치며
개인적으로 const에 대해 그냥 안붙히든 붙히는 알아서 잘판단 하면되겠지 하는 생각이 컸는데 해당 내용을 읽고 const에 중요성에 대해 다시 한번 생각해보게 되었다. Scott Meyers는 const는 할 수 있으면 아낌없이 남발하라고 말햇다. 앞으로 const 키워드를 의식하면서 자주 사용해야겠다.
💡 핵심!
🔸 const를 붙여 선언하면 컴파일러가 사용상의 에러를 잡아내는데 도움을 준다,
🔸 컴파일러 쪽에서 보면 비트수준 상수성을 지켜야 하지만, 우리는 논리적 상수정을 이용하여 프로그래밍 해야한다.
🔸 상수 멤버 및 비상수 멤버 함수가 기능적으로 서로 똑같게 구현되어 있을 경우. 코드 중복을 피하는 것이 좋은데, 이때 비상수 버전이 상수 버전을 호출하도록 만든다.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 6. 컴파일러가 만들어낸 함수가 필요없으면 확실히 이들의 사용을 금해버리자 (0) | 2022.11.10 |
---|---|
[Effective C++] 5. C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자 (0) | 2022.11.10 |
[Effective C++] 4. 객체를 사용하기 전에 반드시 그 객체를 초기화하자 (0) | 2022.11.09 |
[Effective C++] 2. #define을 쓰려거든 const, enum, inline을 떠올리자. (0) | 2022.11.08 |
[Effective C++] 1. C++을 언어들의 연합체로 바라보자. (2) | 2022.11.08 |
게임개발자를 꿈꾸는 대학생의 개발 공부 블로그
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!