모두의 코드
씹어먹는 C ++ - <4 - 6. 클래스의 explicit 과 mutable 키워드>

작성일 : 2018-12-26 이 글은 2899 번 읽혔습니다.

이번 강좌에서는

에 대해 다룹니다.

안녕하세요 여러분! 이번 강좌는 클래스에서 비교적 자주 쓰이지는 않지만 그래도 나름 중요한 두 개의 키워드인 explicitmutable 에 대해 다루어 보도록 하겠습니다.

explicit

지난 번에 만들었던 MyString 클래스를 기억 하시나요? 여기에 아래와 같이 미리 크기를 할당 받는 새로운 생성자를 추가하도록 합시다.

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

성공적으로 컴파일 하면

실행 결과

Capacity : 3

와 같이 나옵니다.

우리가 추가해준

// capacity 만큼 미리 할당함.
MyString(int capacity);

위 생성자가 capacity 를 받아서 그 만큼의 공간을 미리 할당하게 됩니다. 그렇다면 아래와 같이 MyString 을 인자로 받는 함수를 생각해봅시다.

void DoSomethingWithString(MyString s) {
  // Do something...
}

그렇다면 일단 아래와 같은 코드는 컴파일 될까요?

DoSomethingWithString(MyString("abc"))

당연히 되겠지요. MyString 객체를 생성해서 이를 인자로 전달합니다. 그렇다면 MyString 을 명시적으로 생성하지 않을 경우는 어떨까요?

DoSomethingWithString("abc")

일단 DoSomethingWithString 함수를 살펴보면 인자로 MyString 을 받고 있습니다. 하지만 "abc" 는 MyString 타입이 아니지요. 그런데 C++ 컴파일러는 꽤나 똑똑해서 "abc" 를 어떻게 하면 MyString 으로 바꿀 수 있는지 생각해봅니다. 그리고 다행이도 MyString 의 생성자들 중에서는

// 문자열로 부터 생성
MyString(const char* str);

위와 같이 const char* 로 부터 생성하는 것이 있었습니다. 따라서, DoSomethingWithString("abc") 는 알아서

DoSomethingWithString(MyString("abc"))

로 변환되서 컴파일 됩니다. 위와 같은 변환을 암시적 변환(implicit conversion) 이라고 부릅니다. 하지만 암시적 변환이 언제나 사용자에게 편리한 것은 아닙니다. 때로는 예상치 못한 경우에 암시적 변환이 일어날 수 도 있습니다.

예를 들어서 아래와 같은 문장은 어떨까요?

DoSomethingWithString(3)

이는 아마도 높은 확률로 위 함수를 사용자가 잘못 사용했을 가능성이 높습니다. 왜냐하면 문자열을 받는 함수에 문자열을 전달해야지 정수 데이터를 전달하려는 일은 없기 때문이죠. 하지만 컴파일러는 위 문장을 오류로 판단하지 않습니다. 왜냐하면;

// capacity 만큼 미리 할당함.
MyString(int capacity);

위와 같이 int 인자를 받는 MyString 생성자가 있기 때문에 위 함수는

DoSomethingWithString(MyString(3))

으로 변환되어서 컴파일 됩니다. 즉, 사용자가 의도하지 않은 암시적 변환이 일어나게 됩니다.

하지만 다행이도 C++ 에서는 원하지 않는 암시적 변환을 할 수 없도록 컴파일러에게 명시할 수 있습니다. 바로 explicit 키워드를 통해 말이지요.

#include <iostream>

class MyString {
  char* string_content;  // 문자열 데이터를 가리키는 포인터
  int string_length;     // 문자열 길이

  int memory_capacity;

 public:
  // capacity 만큼 미리 할당함. (explicit 키워드에 주목)
  explicit MyString(int capacity);

  // 문자열로 부터 생성
  MyString(const char* str);

  // 복사 생성자
  MyString(const MyString& str);

  ~MyString();

  int length() const;
  int capacity() const;
};

// .. (생략) ..

void DoSomethingWithString(MyString s) {
  // Do something...
}

int main() {
  DoSomethingWithString(3);  // ????
}

컴파일 하였다면

컴파일 오류

test5.cc:56:3: error: no matching function for call to 'DoSomethingWithString'
  DoSomethingWithString(3); // ????
  ^~~~~~~~~~~~~~~~~~~~~
test5.cc:51:6: note: candidate function not viable: no known conversion from 'int' to 'MyString' for 1st argument
void DoSomethingWithString(MyString s) {
     ^
1 error generated.

위와 같이 DoSomethingWithString(3) 부분에서 컴파일 오류가 발생함을 알 수 있습니다. 그 이유는 int capacity 를 인자로 받는 생성자가

// capacity 만큼 미리 할당함. (explicit 키워드에 주목)
explicit MyString(int capacity);

위와 같이 explicit 으로 되어 있기 때문이지요. explicitimplicit 의 반대말로, 명시적 이라는 뜻을 가지고 있습니다.

컴파일러에서 이 MyString 생성자를 explicit 으로 선언한다면 이 생성자를 이용한 암시적 변환을 수행하지 못하게 막을 수 있습니다. 실제 컴파일 오류 메세지를 보아도, int 에서 MyString 으로 변환할 수 없다고 나옵니다.

explicit 은 또한 해당 생성자가 복사 생성자의 형태로도 호출되는 것을 막게 됩니다. 예를 들어서;

MyString s = "abc";  // MyString s("abc")
MyString s = 5;      // MyString s(5)

MyString(int capacity);explicit 이 없을 경우, 위 코드는 잘 작동합니다. 왜냐하면 컴파일러가 알아서 적당한 생성자를 골라서 호출되기 때문이지요. 하지만 생각해보면

MyString s = 5;  // MyString s(5)

는 마치 s 에 5 를 대입하고 있다는 의미를 전달하게 됩니다. 실제로는 capacity 를 5 로 해주는 것인대도 말이지요. 따라서, explicit 으로 MyString(int capacity) 를 설정하면

MyString s(5);   // 허용
MyString s = 5;  // 컴파일 오류!

위와 같이 명시적으로 생성자를 부를 때 에만 허용할 수 있게 됩니다.

mutable

다음으로 살펴볼 키워드로 mutable 이 있습니다. 혹시 이전에 배우신 const 멤버 함수 기억 하시나요? const 함수 내부에서는 멤버 변수들의 값을 바꾸는 것이 불가능 합니다. 하지만, 만약에 멤버 변수를 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();
}

컴파일 하였다면

컴파일 오류

test6.cc:9:11: error: cannot assign to non-static data member within const member function 'DoSomething'
    data_ = x;
    ~~~~~ ^
test6.cc:8:8: note: member function 'A::DoSomething' is declared const here
  void DoSomething(int x) const {
  ~~~~~^~~~~~~~~~~~~~~~~~~~~~~~
1 error generated.

위와 같이 const 함수 안에서 멤버 변수에 값을 대입한다는 오류를 볼 수 있습니다. 하지만 data_mutable 로 선언하면 어떨까요.

#include <iostream>

class A {
  mutable 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();
}

성공적으로 컴파일 하였다면

실행 결과

data: 3

위 처럼 data_ 의 값이 const 함수 안에서 바뀐 것을 알 수 있습니다.

그런데 생각해보면 mutable 을 쓸 바에는 차라리 그냥 DoSomething() 에서 const 를 떼어버리는게 낫지 않을까요? 왜 mutable 키워드를 만들었을까요?

그래서 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);
  }
};

이 서버에는 GetUserInfo 라는 함수가 있는데 입력 받은 user_id 로 데이터베이스에서 해댱 유저를 조회해서 그 유저의 정보를 리턴하는 함수 입니다. 당연히도 데이터베이스를 업데이트 하지도 않고, 무언가 수정하는 작업도 당연히 없기 때문에 const 함수로 선언되어 있습니다.

그런데 대개 데이터베이스에 요청한 후 받아오는 작업은 꽤나 오래 걸립니다. 그래서 보통 서버들의 경우 메모리에 캐쉬(cache)를 만들어서 자주 요청되는 데이터를 굳이 데이터베이스까지 가서 찾지 않아도 메모리에서 빠르게 조회할 수 있도록 합니다.

물론 캐쉬는 데이터베이스만큼 크지 않기 때문에 일부 유저들 정보 밖에 포함하지 않습니다. 따라서 캐쉬에 해당 유저가 없다면 (이를 캐쉬 미스-cache miss 라고 합니다), 데이터베이스에 직접 요청해야겠지요. 대신 데이터베이스에서 유저 정보를 받으면 캐쉬에 저장해놓아서 다음에 요청할 때는 빠르게 받을 수 있게 됩니다.

이를 구현한다면 아래와 같겠지요.

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

그런데 문제는 GetUserInfoconst 함수라는 점입니다. 따라서

cache.update(user_id, user_data);  // <-- 불가능

위 처럼 캐쉬를 업데이트 하는 작업을 수행할 수 없습니다. 왜냐하면 캐쉬를 업데이트 한다는 것은 케쉬 내부의 정보를 바꿔야 된다는 뜻이기 때문이죠. 따라서 저 update 함수는 const 함수가 아닐 것입니다.

그렇다고 해서 GetUserInfo 에서 const 를 떼기도 좀 뭐한것이, 이 함수를 사용하는 사용자의 입장에선 당연히 const 로 정의되어야 하는 함수 이기 때문이지요. 따라서 이 경우, Cachemutable 로 선언해버리면 됩니다.

mutable Cache cache;  // 캐쉬!

위 처럼 말이지요. 이렇듯, mutable 키워드는 const 함수 안에서 해당 멤버 변수에 const 가 아닌 작업을 할 수 있게 만들어줍니다.

그렇다면 이것으로 클래스의 explicitmutable 키워드들에 대해 알아보았습니다. 다음 시간에는 내가 만든 클래스에 연산자를 사용할 수 있게 해주는 연산자 오버로딩에 대해 배울 것입니다.

강좌를 보다가 조금이라도 궁금한 것이나 이상한 점이 있다면 꼭 댓글을 남겨주시기 바랍니다. 그 외에도 강좌에 관련된 것이라면 어떠한 것도 질문해 주셔도 상관 없습니다. 생각해 볼 문제도 정 모르겠다면 댓글을 달아주세요.

현재 여러분이 보신 강좌는 <4 - 6. 클래스의 explicit 과 mutable 키워드> 입니다. 이번 강좌의 모든 예제들의 코드를 보지 않고 짤 수준까지 강좌를 읽어 보시기 전까지 다음 강좌로 넘어가지 말아주세요
댓글이 2 개 있습니다!
프로필 사진 없음
강좌에 관련 없이 궁금한 내용은 여기를 사용해주세요