C++은 할건데, C랑 다른 것만 합니다. 5편 explicit , mutable

0

C++

목록 보기
5/10

explicit , mutable

https://modoocode.com/253
본 포스팅은 위 링크의 글을 바탕으로 작성하였습니다.

1. explicit (명시적)

explicit은 c++에서 암시적 변환을 할 수 없도록 컴파일러에게 명시하는 것이다. 즉 implicit conversion(암시적 변환)이 안되도록 하는 것인데, 이는 개발자의 의도대로 코드가 흐르도록 만들기 위함이다.

그럼, implicit conversion은 무엇일까?? 다음의 예제를 살펴보자

#include <iostream>
#include <cstring>
using namespace std;

class People{
    private:
        char* name;
        int nameLen;
    public:
        People(int _nameLen);
        People(const char* _name);
        ~People();
        void print_info();
};

People::People(int _nameLen) : nameLen(_nameLen){}
People::People(const char* _name){
    nameLen = strlen(_name);
    name = new char[nameLen+1];
    strcpy(name, _name);
}
People::~People(){
    if(name != NULL){
        delete[] name;
    }
}
void People::print_info(){
    cout << "People info : " << name << " nameLen : " << nameLen << endl;
}

void printInfoWrapper(People people){
    people.print_info();
}

int main(){
    printInfoWrapper("123");
}
People info : 123 nameLen : 3

위 예제를 보면 한가지 재밌는 점이 나온다.

People::People(int _nameLen) : nameLen(_nameLen){}
People::People(const char* _name){
    nameLen = strlen(_name);
    name = new char[nameLen+1];
    strcpy(name, _name);
}

위 코드의 모든 생성자 부분인데, 이름인 문자열을 할당하던지, 이름의 길이를 할당한다.

void printInfoWrapper(People people){
    people.print_info();
}

다음의 함수를 보면 People객체를 받아서 print를 해준다. 그럼 매개변수로 People 객체가 와야할 것 같은데

printInfoWrapper("123");

main함수 부분을 보면 전혀 그렇지 않다. "123"이라는 문자열을 통째로 넣었을 뿐이지, People이라는 객체를 만들어서 넣어준 것이 아니다.

그런데 문제없이 돌아갔다. 왜 그럴까??

이것이 바로 Implicit Conversion이다. 암시적 변환이 발생했다는 것인데, 컴파일러가 해당 코드를 다음과 같이 만들어줬기 때문이다.

printInfoWrapper(People("123"));

즉, 문자열 "123"People객체의 생성자에 넣어주어, 객체를 생성해주는 것이다.

그리고 생성된 객체가 함수의 매개변수인 people에 할당되는 것이다.

이것이 implicit conversion이다.

그런데, 다음과 같은 경우도 있다.

printInfoWrapper(123);

이런 경우는 에러를 발생시켜야 한다. 왜냐하면, people 객체의 핵심은 name 뿐이므로 문자열이나, people 인스턴스를 매개변수로 받아야만 하기 때문이다.

즉, 다음과 같은 경우는 문자열 123을 쓴다는게 정수 123을 써버린 경우이다.

그런데 코드를 구동시키면 문제가 발생하지 않는다.

결과는 나오지만, 대신 name에 쓰레기 값들이 채워질 것이다.

왜냐하면, 정수를 입력하면 다음의 생성자가 실행되기 때문이다.

People::People(int _nameLen) : nameLen(_nameLen){}

우리 의도와는 다른 생성자가 실행되어 문제가 발생한 것이다.

printInfoWrapper(People(123));

다음과 같이 정수를 받는 생성자로 암시적 변환이 이루어져, 문제가 생긴 것이다.

그래서 우리는 이러한 암시적 변환이 이뤄지지 않도록 컴파일러에게 explicit 키워드를 사용하는 것이다.

explicit 키워드는 클래스의 선언 앞에 써주면 된다. 선언부에만 써주면 된다. 구현부에 써줄 필요는 없다.

explicit 생성자();

위의 People클래스 코드에 explicit을 적용시켜보자

class People{
    private:
        char* name;
        int nameLen;
    public:
        explicit People(int _nameLen);
        People(const char* _name);
        ~People();
        void print_info();
};

이제 explicit 키워드를 사용하면 정수를 받는 생성자에서 implicit conversion이 발생하지않는다. 즉, 위의 People(123) 생성자가 발생하지 않는다.

위의 코드로 바꾸면, 컴파일에러가 발생하는 것을 확인할 수 있다.

"int"에서 "People"(으)로 변환하기 위한 적절한 생성자가 없습니다.

반면,

printInfoWrapper("123");

위 코드는 문제없이 구동된다.

뿐만 아니라, explicit을 선언한 생성자는 복사 생성자의 형태로 호출되는 것을 막는다.

People people = "123";
People people1 = 123;

다음의 경우 explicit으로 선언한 정수 123을 받는 생성자는 에러가 발생한다.

이는 복사 생성자 역시도 암시적 변환으로

People people1(123);

으로 바뀌기 때문이다. 때문에 암시적 변환을 막는 explicit을 사용하므로, 이를 막을 수 있는 것이다.

2. mutable

이전 시간에 const 함수로 되어있다면, 클래스 내부의 맴버 변수들을 바꿀 수 없다고 하였다. 다음의 예를 보자

class Repository{
    private:
        int id;
        int data;
    public:
        Repository(int _id, int _data) : id(_id), data(_data) {}
        void updateData(int _data) const
        {
            data = _data;
        }

};

위 코드는 컴파일 에러가 발생할 것이다. 왜냐하면

void updateData(int _data) const
{
    data = _data;
}

const 함수로 선언된 updateData에서 맴버 변수 data를 _data로 바꾸기 때문이다.

이렇게 const 함수로 선언한 곳에 맴버 변수를 바꿀 수 있게 하는 것이 바로, mutable 키워드이다.

따라서, mutable 키워드는 클래스의 맴버 변수 선언 맨 앞에 붙여주면 된다.

mutable int data;

다음과 같이 바꾸어주면 된다.

class Repository{
    private:
        int id;
        mutable int data;
    public:
        Repository(int _id, int _data) : id(_id), data(_data) {}
        void updateData(int _data) const
        {
            data = _data;
        }
};

이제 에러가 발생하지 않을 것이다.

그럼 왜 const 함수를 쓰며, 왜 mutable을 쓸까??

const 함수는 의미론적으로 "이 함수는 클래스의 맴버 변수를 바꾸지 않습니다." 라는 의미이다. 즉, 클래스의 맴버 변수는 상태를 의미하며, 이 상태에 따라 동작이 달라지는 클래스에게는 굉장히 큰 이점이 작용한다.

프로그래머가 이 함수는 const 함수이니까 상태를 바꾸는 것이 없어, 디버깅에 고려하지 않을 수 있다. 라고 확신할 수 있기 때문이다.

그런데, 프로그램 성능을 위해서 또는 맴버 변수는 바꾸는 데, 상태 변화에 영향이 없어 const 함수에서도 값을 바꾸어 줬으면 하는 것들이 있다.

이때에는 mutable을 써주어 예외를 설정해주는 것이다.

가령, repository를 통해 정보를 불러온다고 하자

class Repository{
    private:
        int id;
        int data;
    public:
        Repository(){}
        Repository(int _id, int _data) : id(_id), data(_data) {}
        void updateData(int _data)
        {
            data = _data;
        }
        int getData() const {
            return data;
        }
};

class UserDbTemplate{
    private:
        Repository repo[10];
    public:
        void setRepo(int id, int data){
            repo[id].updateData(data);
        }
        int getRepodata(int id) const {
            return repo[id].getData();
        }
};

우리는 UserDbTemplate을 통해 정보를 가져오려고 한다. 따라서, UserDbTemplate안에는 Repository를 맴버 변수로 갖고 있고, 이를 getRepodata를 통해 갖고 있는 데이터를 반환시킨다.

getRepodata는 의미상으로 값만 리턴하므로 const 함수이다. 또한, 내부적으로 getData 역시 값만 리턴하므로 const 함수이다.

여기까지는 문제가 없다. 만약 Repository가 현재처럼 간단한게 아니라, 무거운 DB연산을 수행하는 프로그램이라면 어떻게할까??

계속 get을 할 때마다 db랑 connection을 맺고 lock을 걸고 값을 찾는 연산을 너무 비싼 연산이다.

따라서, 캐시를 도입하여 이를 해결해본다고 하자

typedef struct Cashe{
    int id, data;
}Cashe;

cashe 구조체를 만들고,

class UserDbTemplate{
    private:
        Repository repo[10];
        Cashe cashe;
    public:
        void setRepo(int id, int data){
            repo[id].updateData(data);
        }
        int getRepodata(int id) const {
            if(cashe.id == id){
                return cashe.data;
            }
            int data = repo[id].getData();
            cashe.id = id;
            cashe.data = data;
            return data;
        }
};

에 넣어준다.

그런데, cashe 부분에서 에러가 발생할 것이다. 왜냐하면 getRepodata는 const이기 때문이다.

그렇다고 const를 없앨 수 는 없다. getter는 의미론적으로 값을 바꾸는 함수들이 아니기 때문이다.

따라서, cashe를 mutable로 지정하여 예외적으로 값을 갱신할 수 있게 해준다.

cashe가 맴버 변수라고 할 지라도, 상태 변화에 주는 영향은 없다. 즉, cashe는 성능 상의 문제를 해결하기위해 존재하는 것이지, 어떤 함수들의 행동 결과에 영향을 미치는 것이 아니기 때문이다.

class UserDbTemplate{
    private:
        Repository repo[10];
        mutable Cashe cashe;
    public:
        void setRepo(int id, int data){
            repo[id].updateData(data);
        }
        int getRepodata(int id) const {
            if(cashe.id == id){
                return cashe.data;
            }
            int data = repo[id].getData();
            cashe.id = id;
            cashe.data = data;
            return data;
        }
};

컴파일 에러가 멈출 것이다.

0개의 댓글