[Effective C++] #3_const 사용
Scott Meyers의 "Effective C++"를 통해, C++ 구현에 필요한 개념들을 이해하고, 기록하기 위함입니다. 해당 항목은 1장 'C++ 기본', 항목 3 "낌새만 보이면 const를 들이대 보자!"에 해당하는 내용입니다.
#0. const의 정의
char sayHello[] = "Hello!";
const char *p = sayHello; //포인터가 가리키는 데이터를 상수화
char* const p = sayHello; //포인터가 상수화
const char* const p = sayHello; // 둘 다 상수화
"const" 키워드가 붙은 객체는 외부 변경을 불가능하게 합니다. 따라서, 객체의 내용이 불변이어야 한다는 소스코드 제작자의 의도를 컴파일러 및 사용들과 나눌 수 있는 수단입니다. "const" 키워드는 다양한 방법으로 사용할 수 있습니다. 클래스 바깥에서 전역 혹은 네임스페이스 유효 범위의 상수를 선언하는데 쓸 수 있습니다. 더불어, 정적 객체, 클래스 내부의 정적 멤버 및 비정적 멤버 모두 "const"를 사용해서 상수로 선언할 수 있습니다. 위 코드에서 "const char* p"는 포인터가 가리키는 대상을 상수로 선언한 것입니다. 이때, 포인터는 가리키는 대상을 변경할 수 없게 됩니다. 반면, "char* const p"의 경우, 포인터는 가리키는 대상 자체는 변경이 가능
하지만, 자신이 가리키는 대상이 아닌 것을 가리키는 경우가 허용되지 않습니다.
#1. const 반환 타입 메서드
class A {...};
const A operator* (const A& lhs, const A& rhs);
*************오류 발생 예제*************
A classA, classB, classC;
...
(classA * classB) = classC;
or
if(classA * classB = classC) // if ((classA * classB) == classC) 사용 오류
함수에도 const를 사용할 수 있습니다. 이때, 함수의 반환 값, 매개변수, 그리고 함수 전체에 "const"의 성질을 부여할 수 있습니다. 위 코드처럼, 함수 반환 값을 상수로 설정하면, 안전성과 효율성을 갖고 사용자의 에러 발생을 방지하는데 큰 도움을 줄 수 있습니다. 위처럼 어처구니없는 실수가 발생하는 것을, 상수 반환 값 지정으로 미연에 방지할 수 있습니다.
#2. 상수 멤버 함수
class Text {
public:
void Text(std::string textString) { text = textString; } //생성자
...
const char& operator[] (std::size_t position) const //상수 객체를 위한 operator[]
{ return text[position]; }
char& operator[] (std::size_t position) const //비상수 객체를 위한 operator[]
{ return text[position]; }
private:
std::string text;
};
Text a("Hi"); //비상수 객체 생성
std::cout << a[0]; // 비상수 멤버 함수 호출
const Text b("Hi"); //상수 객체 생성
std::cout << b[0]; // 상수 멤버 함수 호출
멤버 함수 뒤에 붙는 "const"의 역할은 해당 멤버 함수가 상수 객체에 대해 호출될 함수이다라고 명시합니다. 멤버 함수를 상수로 정의하는 이유는 무엇일까요? 첫 번째 이유는 객체를 변경할 수 있는 멤버 함수는 무엇이고, 반대로 변경할 수 없는 함수는 무엇인가를 구별하기 위함입니다. 두 번째는 "const"를 사용해서 상수 객체를 사용하기 위함입니다. C++ 소스코드 작성 시 자주 사용하는 상수 객체에 대한 참조자 전달(const std::string& a)을 진행할 때, 전달된 상수 객체를 조작하기 위해 우리는 상수 멤버 함수가 준비되어 있어야 하겠죠.
#3. 비트 수준의 상수성, 혹은 물리적 상수성
class AText {
public:
...
// 데이터 멤버의 참조값을 반환하는 상수 멤버 함수 선언
char& operator[](std::size_t position) const
{ return ptr[position]; }
private:
char* ptr;
};
const AText a("Hello");
char* p = &a[0];
*p = 'J'; // ptr = "Hello" -> "Jello"로 변경 된다.
상수 멤버 함수는 두 가지 주요 개념을 기반으로 합니다. 그중 하나는 "물리적 상수성"으로, 어떤 멤버 함수가 어떤 데이터 멤버도 건드리지 않아야 결국 상수 멤버 함수로서 인정하는 개념입니다. 이때, 객체를 구성하는 "비트"들 중 어떠한 비트도 변경할 수 없어야 합니다. 우리는 물리적 상수 성을 간단하게 확인할 수 있습니다. 어떤 멤버 함수가 해당 객체의 데이터 멤버에 대해 대입 연산을 수행했는가를 확인하면 됩니다. 하지만, 물리적 상수 성만으로 상수 멤버 함수를 온전히 뒷받침할 수 없습니다. 어떤 포인터가 가리키는 대상을 수정하는 멤버 함수는 "물리적 상수성"을 만족하지만, 온전히 상수 성의 원칙을 지키지 못합니다. 이해가 쉽도록 위 코드를 살펴보겠습니다. "operator []" 함수 자체는 "ptr", 즉 데이터 멤버를 건드리지 않습니다, 다만 이 멤버에 대한 참조값을 반환할 뿐이죠. 여기까지 "operator []" 함수는 물리적 상수 성을 만족합니다. 하지만, "a" 객체는 상수 객체임에도 불구하고, 참조값을 반환받은 포인터 "p"를 통해 데이터 멤버를 조작할 수 있는 오류를 범하게 되죠. 이러한 현상을 보완하기 위해 두 번째 개념 "논리적 상수성"이 존재합니다.
#4. 논리적 상수성
class AText {
public:
...
std::size_t length() const;
private:
char* ptr;
mutable std::size_t textLength;
mutable bool lengthIsValid;
};
std::size_t AText::length () const
{
if(!lengthIsValid){
textLength = std::strlen(ptr);
lengthIsValid = true;
}
return textLength;
}
논리적 상수성이란, 상수 멤버 함수는 객체의 몇 비트 정도는 바꿀 수 있으며, 사용자 측에서 이러한 조작을 알아채지 못한다면, 상수 멤버 함수의 자격을 부여한다는 의미입니다. 상수 멤버 함수 중 "length" 멤버 함수에 대한 예제를 위 코드를 통해 살펴보겠습니다. 먼저 "legnth" 멤버 함수는 직전에 계산한 "textLength"가 현재 ptr이 가리키는 문자열의 길이와 다르다면 "lengthIsValid"를 true로 변경하고 , 현재 문자열의 길이로 "textLength"를 업데이트 후 반환합니다. 이때, 상수 멤버 함수로 정의된 "length"는 절대 데이터 멤버들을 조작할 수 없습니다!! 하지만, 내용을 살펴보면 앞으로 사용자가 정의할 "AText"의 상수 객체에 대해서는 아무 문제가 발생하지 않는 함수로 보입니다. 현재 객체가 갖고 있는 문자열의 길이와 이 길이가 현재 문자열의 길이가 맞는지 여부에 대한 내용이기 때문입니다. 따라서, 우리는 "물리적 상수성"을 만족시키며, "논리적 상수성" 또한 만족시키기 위해 데이터 멤버들을 "mutable" 키워드와 함께 정의합니다. "mutable" 키워드 사용은 객체의 어떠한 데이터 멤버도 조작할 수 없는 "const"키워드가 붙어있는 상수 멤버 함수가 해당 데이터 멤버들을 변경할 수 있다고 알려줍니다.
#5. 상수 멤버 함수, 그리고 동일한 비 상수 멤버 함수 간의 코드 중복
class ABlock {
public:
...
const char& operator[] (std::size_t position) const // 상수 멤버 함수
{
...
...
... //동일한 내용
...
}
char& operator[] (std::size_t position) // 비상수 멤버 함수
{
...
...
... //동일한 내용
...
}
}
똑같은 일을 수행하는 "operator[]" 멤버 함수를 "const"의 성격을 갖는 것과, 그렇지 않은 것을 모두 정의하는 경우, 심각한 코드 중복 현상이 발생합니다. 우리는 이러한 코드 중복 현상을 방지하기 위해 다른 방법을 생각해 보겠습니다. 비 상수 멤버 함수가 상수 멤버 함수를 호출도록 수정해 보겠습니다.
#6. 상수 멤버 함수를 호출하는 비 상수 멤버 함수
class AText {
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 AText&>(*this)[position]);
}
...
};
코드 중복을 피하기 위해 우리는 "상수 멤버 함수"를 호출하는 "비 상수 멤버 함수"를 코드로 작성해 보겠습니다. 이때, 눈여겨보아야 할 점이 있습니다. 바로 "캐스팅"입니다. 쉽게 얘기해서, 상수 멤버 함수를 호출하기 위해 비 상수 멤버 함수의 반환 값을 "상수"로 만들고, 반면 상수 멤버 함수는 비 상수 멤버 함수 안에서 호출되기 위해 반환 값을 "비 상수"로 만들어 주어야겠죠. 조금 복잡한 부분이기 때문에 "캐스팅"을 위해 사용된 "const_cast"와 "static_cast"의 차이를 먼저 살펴보겠습니다.
#7. const_cast 개념
const_cast <새로운 타입> expression
const_cast는 포인터 또는 참조형의 상수 성을 제거합니다. 우리가 잘 아는 일반 캐스트 연산자와 달리, const_cast는 오직 상수 성만 제거하고, 형 변환은 불가능합니다. 아래 예제를 살펴보죠.
char a[] = "HELLO"; // 문자열을 가리키는 "a"
const char* p = a; // 상수 포인터 "p"도 문자열 "HELLO"를 가리킨다.
char* c = const_cast<char*>(p); // c는 p에서 const를 떼고,
c[0] = 'J'; // 값을 수정하는것이 가능합니다.
cout << c << endl;
"HELLO"라는 문자열을 정의합니다. 이때, a는 해당 문자열의 첫 번째 주소를 가리킵니다. 상수 포인터 "p"는 a를 물려받고, const_cast 되어 "c"가 됩니다. 결국, c를 통해 문자열 조작이 허용됩니다.
#8. static_cast 개념
static_cast <새로운 타입> expression;
char str[] = "HELLO"
char* p = str;
const char* c = static_cast<const char* > (p);
"static_cast" 논리적으로 변환 가능한 타입을 변환할 수 있습니다. 일반적인 "char*"타입의 p를 "const char*"타입의 a로 만들 수 있습니다.
#9. 상수 멤버 함수를 호출하기 위한 "캐스팅" 연산
const char& operator[] (std::size_t position) const // 상수 멤버 함수
{
...
...
return text[position];
}
char& operator[] (std::size_t position) //상수 멤버함수를 호출하는 비상수 멤버 함수
{
return const_cast<char&>(static_cast<const AText&>(*this)[position]);
}
다시 위에서 보았던 코드를 함수 부분만 따로 떼어서 보겠습니다. 먼저, 비 상수 멤버 함수의 반환 값은 당연하게도 "char&", 비 상수 참조자입니다. 반면, 상수 멤버 함수는 "const AText&"를 반환하겠죠. 따라서, 안쪽에 "static_const"를 사용하여 "const AText&"로 반환 값을 캐스팅해 줍니다. 이렇게 하면, 상수 멤버 함수의 형식을 따라주어, 상수 멤버 함수를 정상적으로 호출할 수 있게 되겠죠. 다음으로 바깥쪽에 다시 "const_cast"를 사용하여 상수 성을 제거하고 "char&" 형식의 결과 값을 반환할 수 있습니다. 정리하면, (*this)[position] 연산은 자기 자신, 즉 비 상수 타입의 결과 값을 반환하기 위함입니다. 따라서, 이를 static_cast <const AText&>로 캐스팅해 주어 상수 멤버 함수의 형식을 갖추게 됩니다. 이후, 비 상수 멤버 함수 내부에서 호출한 상수 멤버 함수는 정상적으로 결과 값을 끝까지 반환하기 위해 다시 "char&"로 캐스팅되는 겁니다.
const의 사용은 컴파일 에러를 잡는데 유용합니다. 우리는 const를 논리적 상수 성을 지켜 사용하고, 코드 중복을 피하기 위해 동일한 내용의 상수와 비 상수 함수들은 비 상수 함수가 상수 함수를 호출하는 것으로 합니다.
'언어 > Effective C++' 카테고리의 다른 글
[Effective C++] #5 생성자, 소멸자, 복사 생성자, 복사 대입 연산자 (0) | 2022.01.06 |
---|---|
[Effective C++] #4 객체의 초기화 (0) | 2022.01.06 |
[Effective C++] #2_#define, 매크로 사용의 대안 (0) | 2022.01.03 |
[Effective C++] #33_오버라이딩 문제 (0) | 2022.01.03 |
[Effective C++] #32_public 상속 (0) | 2022.01.03 |