명명된 객체를 상수나 변수로 정의할 수 있다. 즉, 이름은 변경할 수 없거나 변경할 수 있는 보유하는 객체를 참조할 수 있다. 변경할 수 없는 객체를 체계적으로 사용하면 코드를 더 이해하기 쉽게 만들고 더 많은 오류를 조기에 발견하고 때로는 성능을 개선할 수 있다. 특히 불변성은 다중 스레드 프로그램에서 가장 유용한 속성이다. 기본 제공 유형의 단순 상수 정의를 넘어서 유용하려면 다음을 수행할 수 있어야 한다. 사용자 정의 유형의 const 객체에서 작동하는 함수를 정의한다. const T& 인수를 사용하는 함수를 의미하는 독립 함수의 경우. 클래스의 경우 const 객체에서 작동하는 멤버 함수를 정의할 수 있어야 함을 의미한다.
const 멤버 함수
Date 값을 조사하는 방법을 제공하기 위해서 일, 월, 연도를 읽는 기능을 추가해 보자.
class Date {
int d, m, y;
public:
int day() const { return d; }
int month() const { return m; }
int year() const;
void add_year(int n); // add n years
// ...
};
함수 선언에서 (빈) 인수 목록 뒤의 const는 이러한 함수가 Date의 상태를 수정하지 않음을 나타낸다.
당연히 컴파일러는 이 약속을 위반하려는 우발적인 시도를 잡아낸다.
int Date::year() const
{
return ++y; // error : attempt to change member value in const function
}
const 멤버 함수가 클래스 외부에 정의되면 const 접미사가 필요하다.
int Date::year() // error : const missing in member function type
{
return y;
}
즉, const는 Date::day(), Date::month() 및 Date::year() 유형의 일부다.
const 멤버 함수는 const 및 non-const 객체 모두에 대해 호출할 수 있는 반면 non-const 멤버 함수는 non-const 객체에 대해서만 호출할 수 있다.
void f(Date& d, const Date& cd)
{
int i = d.year(); // OK
d.add_year(1); // OK
int j = cd.year(); // OK
cd.add_year(1); // error : cannot change value of a const Date
}
물리적 및 논리적 제약
때때로 멤버 함수는 논리적으로 const이지만 여전히 멤버의 값을 변경해야 한다. 즉, 사용자에게 함수는 객체의 상태를 변경하지 않는 것처럼 보이지만 사용자가 직접 관찰할 수 없는 일부 세부 사항이 업데이트된다. 이것을 종종 논리적 일관성이라고 한다. 예를 들어 Date 클래스에는 문자열 표현을 반환하는 함수가 있을 수 있다. 이 표현을 구성하는 것은 상대적으로 비용이 많이 드는 작업일 수 있다. 따라서 Date값이 변경되지 않는 한 반복된 요청이 단순히 복사본을 반환하도록 복사본을 유지하는 것이 좋다. 이와 같은 값을 캐싱하는 것은 더 복잡한 데이터 구조에 더 일반적이지만 Date에 대해 어떻게 달성할 수 있는지 보자.
class Date {
public:
// ...
string string_rep() const; // string representation
private:
bool cache_valid;
string cache;
void compute_cache_value(); // fill cache
// ...
};
사용자 관점에서 string_rep는 Date의 상태를 변경하지 않으므로 분명히 const 멤버 함수여야 한다. 반면에 캐시 및 cache_valid 멤버는 디자인 의미가 있도록 때대로 변경되어야 한다.
이러한 문제는 캐스트(예:const_cast)를 사용하여 무차별 대입을 통해 해결할 수 있다. 그러나 유형 규칙을 어지럽히지 않는 합리적으로 우아한 솔루션도 있다.
mutable
클래스의 멤버를 변경 가능하도록 정의할 수 있다. 즉, const 객체에서도 수정할 수 있다.
class Date {
public:
// ...
string string_rep() const; // string representation
private:
mutable bool cache_valid;
mutable string cache;
void compute_cache_value() const; // fill (mutable) cache
// ...
};
이제 명백한 방법으로 string_rep()을 정의할 수 있다.
string Date::string_rep() const
{
if (!cache_valid) {
compute_cache_value();
cache_valid = true;
}
return cache;
}
이제 const 및 non-const 객체 모두에 string_rep()를 사용할 수 있다.
void f(Date d, const Date cd)
{
string s1 = d.string_rep();
string s2 = cd.string_rep(); // OK!
// ...
}
간접 참조를 통한 가변성
멤버를 변경 가능으로 선언하는 것은 작은 객체 표현의 작은 부분만 변경할 수 있는 경우에 가장 적합하다. 더 복잡한 경우는 변경 데이터를 별도의 객체에 배치하고 간접적으로 액세스 하여 더 잘 처리되는 경우가 많다. 해당 기술을 사용하는 경우 string-with-cache 예제는 다음과 같다.
struct cache {
bool valid;
string rep;
};
class Date {
public:
// ...
string string_rep() const; // string representation
private:
cache∗ c; // initialize in constr uctor
void compute_cache_value() const; // fill what cache refers to
// ...
};
string Date::string_rep() const
{
if (!c−>valid) {
compute_cache_value();
c−>valid = true;
}
return c−>rep;
}
cache를 지원하는 프로그래밍 기술은 다양한 형태의 지연 평가로 일반화된다.
const는 포인터나 참조를 통해 액세스 되는 객체에 (전이적으로) 적용되지 않는다. 사람은 그러한 객체를 '일조의 하위 객체'로 생각할 수 있지만 컴파일러는 그러한 포인터나 참조가 다른 것과 다르다는 것을 알지 못한다. 즉, 멤버 포인터에는 다른 포인터와 구별되는 특별한 의미가 없다.
'Program Language > C & C++' 카테고리의 다른 글
[C++] initializer-list 생성자 (0) | 2022.01.14 |
---|---|
[C++] 클래스 static 멤버 (0) | 2022.01.14 |
[C++] 클래스 explicit 생성자 (0) | 2022.01.14 |
[C++] namespace를 이용한 version 관리 (0) | 2022.01.14 |
[C++] Catching Exception (0) | 2022.01.13 |