[C++] explicit과 mutable
위 사이트에서 공부한 내용 정리입니다
예제 코드
#include <iostream>
class MyString {
char* string_content; // 문자열 데이터를 가리키는 포인터
int string_length; // 문자열 길이
int memory_capacity;
public:
// capacity 만큼 미리 할당함.
MyString(int capacity);
// 문자열로 부터 생성
MyString(const char* str);
// 복사 생성자
MyString(const MyString& str);
~MyString();
int length() const;
};
MyString::MyString(int capacity) {
string_content = new char[capacity];
string_length = 0;
memory_capacity = capacity;
std::cout << "Capacity : " << capacity << std::endl;
}
MyString::MyString(const char* str) {
string_length = 0;
while (str[string_length++]) {
}
string_content = new char[string_length];
memory_capacity = string_length;
for (int i = 0; i != string_length; i++) string_content[i] = str[i];
}
MyString::MyString(const MyString& str) {
string_length = str.string_length;
string_content = new char[string_length];
for (int i = 0; i != string_length; i++)
string_content[i] = str.string_content[i];
}
MyString::~MyString() { delete[] string_content; }
int MyString::length() const { return string_length; }
int main() { MyString s(3); }
String 클래스를 새로 만든 예제인데, 여기에 MyString(int capacity) 생성자를 추가한 것이다
void DoSomethingWithString(MyString s) {
// Do something...
}
만약 이런 함수가 있을 때
DoSomethingWithString(MyString("abc"))
DoSomethingWithString("abc")
이 코드들은 둘 다 컴파일이 된다
처음 MyString("abc")는 MyString형이어서 당연히 컴파일 되는데,
아래 그냥 "abc"는 자동으로 컴파일러가 아래 생성자로 MyString형을 생성해서 컴파일이 가능하다
MyString(const char* str);
이런 경우를 암시적 변환이라고 한다
DoSomethingWithString(3)
그러면 MyString도 아니고 문자열도 아닌 이 문장은 컴파일이 될까?
DoSomethingWithString(MyString(3))
정답은 MyString이 int인자로 받는 생성자가 있기 때문에 가능하다(암시적 변환)
그치만 이 코드는 누가 봐도 의도하지 않은 동작이고, 이런 의도하지 않은 암시적 변환을 막는 것이
바로 explicit이다.
explicit MyString(int capacity);
MyString에서 int인자를 받는 생성자에 explicit 키워드를 붙인다면
DoSomethingWithString(3) 해당 부분에서 컴파일 오류가 발생한다
MyString s = "abc"; // MyString s("abc")
MyString s = 5; // MyString s(5)
explicit은 위처럼 복사 생성자의 형태로 호출되는 것도 막는다
만약 int인자를 받는 생성자에 explicit을 붙인다면
MyString s(5); // 허용
MyString s = 5; // 컴파일 오류!
위처럼 동작할 것이다
mutable
mutable로 선언한 변수는 const함수에서도 이들 값을 바꿀 수 있다
예제
#include <iostream>
class A {
int data_;
public:
A(int data) : data_(data) {}
void DoSomething(int x) const {
data_ = x; // 불가능!
}
void PrintData() const { std::cout << "data: " << data_ << std::endl; }
};
int main() {
A a(10);
a.DoSomething(3);
a.PrintData();
}
여기서 DoSomething에 data_ = x 부분에서 컴파일 오류가 발생한다
const 함수 내에서 변수의 값을 바꾸려고 했기 때문이다
mutable int data_;
하지만 data_를 mutable로 선언한다면 정상적으로 실행된다
mutable이 왜 필요할까?
그냥 const 함수 안쓰고 일반 함수로 쓴 다음 mutable을 안쓰면 되지 않나? 라고 생각할 수 있다
여기서 const 함수를 왜 쓰는지 의미를 살펴봐야한다
const 함수 의미: 이 함수는 객체의 내부 상태에 영향을 주지 않습니다
대표 예) 읽기 작업만 하는 함수
위 사이트에서는 데이터베이 서버를 예시로 들었다
class Server {
// .... (생략) ....
// 이 함수는 데이터베이스에서 user_id 에 해당하는 유저 정보를 읽어서 반환한다.
User GetUserInfo(const int user_id) const {
// 1. 데이터베이스에 user_id 를 검색
Data user_data = Database.find(user_id);
// 2. 리턴된 정보로 User 객체 생성
return User(user_data);
}
};
대충 이런 구조인데, 데이터 베이스는 요청한 데이터를 받아오는 작업이 오래 걸리기 때문에 캐시를 사용한다
그러면 아래와 같은 구조가 나오게 되는데
class Server {
// .... (생략) ....
Cache cache; // 캐쉬!
// 이 함수는 데이터베이스에서 user_id 에 해당하는 유저 정보를 읽어서 반환한다.
User GetUserInfo(const int user_id) const {
// 1. 캐쉬에서 user_id 를 검색
Data user_data = cache.find(user_id);
// 2. 하지만 캐쉬에 데이터가 없다면 데이터베이스에 요청
if (!user_data) {
user_data = Database.find(user_id);
// 그 후 캐쉬에 user_data 등록
cache.update(user_id, user_data); // <-- 불가능
}
// 3. 리턴된 정보로 User 객체 생성
return User(user_data);
}
};
여기서 cache는 GetUserInfo가 const함수이므로 값을 바꾸면 컴파일 에러가 발생한다
cache.update(user_id, user_data);
이렇게 캐시 값을 바꿀 때 컴파일 에러가 발생한다
하지만 데이터 베이스에서 값을 읽어오는데 const를 빼는 것도 의미상 맞지 않다
데이터 베이스에서 값을 읽어오는건 내부 값이 변경 안되니까 당연히 const 함수로 선언해야한다
이럴 때 cache 변수를 mutable로 선언해서 컴파일 에러를 막는 것이다

