C, C++/C++ 언어 / / 2024. 7. 24.

[C++] explicit 키워드와 변환 생성자( converting constructor )

변환 생성자( Converting Constructor )가 무엇인가

변환 생성자는 암시적인 타입 변환을 통해 호출되는 생성자를 말합니다.

그리고, 이 생성자를 통해서 임시 객체가 생성됩니다.

 

먼저, 암시적인 타입 변환의 예를 보겠습니다.

void printVal(double val){	
    cout << val;
}

int main(){
    printVal(5);
}

main 함수에서 printVal 함수를 int 인자로 호출하게 되면, 컴파일러는 먼저 int 매개 변수를 가진 함수를 찾습니다.

만약, 그러한 함수가 없다면 "숫자 변환 규칙"을 따라 int를 대신할 수 있는 타입을 매개 변수로 가진 함수를 찾아서 호출합니다.

이것이 암시적인 타입 변환입니다.

 

그리고, 이러한 암시적인 타입 변환은 사용자 정의 타입에 대해서도 수행됩니다.

그렇지만, 사용자 정의 타입에 대한 "숫자 변환 규칙" 같은 변환 규칙은 없습니다.

따라서, 컴파일러는 사용자 정의 타입의 생성자 중에서, int를 매개 변수로 하는 생성자를 찾습니다.

만약, 그러한 생성자가 있다면, 그 생성자를 호출해서 임시 객체를 만듭니다.

즉, int를 사용자 정의 타입으로 변환한 것입니다.

이것을 사용자 정의 변환( user-defined conversion )이라고 합니다.

class IntegerObj{	// 사용자 타입
    int m_val;
public:
    IntegerObj(int val) : m_val(val){}

    int getVal() const{
        return m_val;
    }
};

void printVal(const IntegerObj& obj){	// 사용자 타입의 매개 변수
    cout << obj.getVal();
}

int main(){
    printVal(5);	// integer 타입 인자로 함수 호출
}

위의 예제에서, int 5를 인자로 printVal 함수 호출 시, 컴파일러는 IntegerObj 생성자를 호출해서, int 5를 IntegerObj로 변환합니다.

이때 호출된 생성자를 변환 생성자( converting constructor )라고 합니다.

기본적으로, 사용자 정의 타입의 모든 생성자는 변환 생성자입니다.

 

하지만, 이러한 사용자 변환은 오직 한 번만 적용됩니다.

class StringObj{
    string m_str;
public:
    StringObj(string str) : m_str(str){}

    string getVal() const{
        return m_str;
    }
};

void printVal(const StringObj& str){
    cout << str.getVal();
}

int main(){

    printVal("c-str");  // c-str 인자를 StringObj로 변환할 수 없음. error
	
    printVal(std::string("c-str"));  // ok
}

위의 "c-str" ( const char* )은 std::string 타입으로 암시적인 변환을 할 수 있습니다.

그러나, 한 번 변환한 string 타입을 다시 StringObj로 변환할 수는 없습니다.

이것이 가능하다면, 변환 체인이 생기게 되고, 이것이 타입의 모호성을 가져올 수 있기 때문입니다.

 

그래도, 이러한 암시적인 타입 변환이 있기 때문에, 다음과 같은 코드가 가능합니다.

void printVal(const IntegerObj& obj){
    cout << obj.getVal();
}

int sum( const IntegerObj& a, const IntegerObj& b){
    return a.getVal() + b.getVal();
}

int main(){

    IntegerObj a = 5;	// 암시적인 타입 변환
    IntegerObj b = 6;

    IntegerObj c = sum(a, b);	// 암시적인 타입 변환
    printVal(c);    
}

 

그러나, 단점도 있습니다.

void printVal(const IntegerObj& obj){
    cout << obj.getVal();
}

void printVal(char* pStr){	// 호출하고자 하는 함수
    cout << pStr;
}

int main(){

    printVal(5);    // "5"를 출력하려고 했는데, 실수로 5을 인자로 사용
}

이 예제에서는 문자열 "5"를 출력하려고 했지만, 의도치 않게 5를 인자로 printVal 함수를 호출했습니다.

그 결과, overload 된 다른 함수 printVal( const IntegerObj& obj )을 호출하게 됩니다.

 

하지만, 컴파일러는 절대로 경고하지 않습니다.

만약, 프로그램이 복잡하고 거대하며, 심지어 당장 코드가 제대로 동작한다면 이런 버그는 한참 뒤에나 찾을 수 있을 겁니다.

 

explicit 키워드

explicit는 사용자 변환( user-defined conversion )을 금지시키는 키워드입니다.

그러므로, explicit 키워드가 붙은 생성자는 변환 생성자( converting constructor )가 될 수 없습니다.

 

이렇게 함으로써, 이전 항목과 같은 오류나 컴파일러의 암시적인 타입 변환 방식을 인식하지 못해서 발생하는 문제점을 완화시킬 수 있습니다.

class IntegerObj{
    int m_val;
public:

    IntegerObj(int val) : m_val(val){}

    int getVal() const{
        return m_val;
    }
};

int main(){

    IntegerObj a = 5;	// 복사 초기화 

}

위의 코드에서는, IntegerObj를 복사 초기화( copy initialization ) 통해 초기화하는 것을 볼 수 있습니다.

하지만, explicit 키워드를 사용하면 복사 초기화가 금지됩니다.

 

그리고, 복사 초기화를 이용해서 사용자 변환( user-defined conversion )을 하기 때문에, IntegerObj( int )는 더 이상 변환 생성자( converting constructor )가 될 수 없습니다.

더 이상 int에서 IntegerObj로의 암시적인 변환이 일어나지 않는다는 뜻입니다.

class IntegerObj{
    int m_val;
public:

    // explicit 키워드 사용
    explicit IntegerObj(int val) : m_val(val){}

    int getVal() const{
        return m_val;
    }
};

void printVal(const IntegerObj& obj){
    cout << obj.getVal();
}

int main(){

    //printVal(5);    // error !

    //IntegerObj a = 5;   // error !

    IntegerObj b( 5 );  // direct initialization. ok
    
    IntegerObj c{ 5 };  // uniform initialization. ok
}

explicit 키워드를 사용하면 복사 초기화를 사용할 순 없지만, 직접 초기화( direct initialization )이나 유니폼 초기화( uniform initialization )를 통해서 IntegerObj를 초기화할 수 있습니다.

 

그리고, printVal 함수를 호출할 때는 당연히 아래와 같이 명시적인 인자를 사용해야 합니다.

printVal( IntegerObj(5) );

 

 

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유