2025. 5. 3. 17:32ㆍProgramming Language/C++
1. 변환 생성자
먼저 변환이란 키워드는 다른 타입으로 변경해준다는 의미이다.
예를 들어
int main(){
std::string s = "test"; // "test"라는 c-style 문자열이 c++스타일 String으로 변환되어 초기화
if(s){ // string 타입이 bool타입으로 변환되어 true/false를 반환
}
}
이런 과정들에 대한것이다.
그래서 한번 String 클래스를 만들어서 확인해보면
이렇게 생성했을때 보면 문제 없이 String에 c스타일 문자열이 저장되는 것을 볼 수 있다.
근데 이건 사실 저렇게 값을 넣어주는 시점에
이 생성자를 변환 생성자로 인식해서
이렇게 변환된다.
이렇게 암묵적 변환이 일어나는 과정은 사실
1. "asdf"는 const char* 타입 (C-style 문자열) → 2. String 생성자: String(const char* chars) → 3. 타입이 다르니까 → 4. 컴파일러가 자동으로 const char* → String 변환 생성자를 찾아 호출 하는 과정을 가진 것이다.
이렇게 암묵적인 변환이 일어나는 조건은 대입 초기화 방식이면서 ClassName obj = 다른타입값; 의 형태를 가지고 해당 타입을 받을 수 있는 변환 생성자(인자 1개)가 존재할때 암묵적인 변환이 일어난다.
여기서 우리는 변환 생성자를 생성한 적이 없는데 라고 생각할 수 있는데 사실 매개변수가 1개인 생성자는 자동으로 변환 생성자로 간주될 수 도 있다.
근데 여기서 explicit를 해당 생성자의 앞에 붙이게 되면
이렇게 변환을 하기 위한 변환 생성자로써 사용을 하지 못하게 되고 찾지 못했다고 에러를 보여주게 된다.
이 explicit을 왜 쓸까?
class Meter {
public:
Meter(double d) { std::cout << d << " meters\n"; }
};
void printLength(Meter m) {
// ...
}
이런 코드가 있을때 우리는 printLength에 Meter 타입이 오기를 기대한다.
그리고 이 안에 우리는 직접 Meter 타입을 전달해줄때 우리가 생각한대로 결과가 나오게 될 것이다.
그런데 만약 우리가
printLength(3.14);
이렇게 잘못 값을 넣어줬다고 생각해보자.
이러면 사실상 값이 매개변수에 전달하는 과정은 대입연산과 동일하고
void printLength(Meter m = 3.14) {
// ...
}
와 같이 실행하게 되면서 생성자를 불러 3.14를 Meter타입으로 변환해서 printLength 함수를 실행하게 될것이다.
이러면 우리가 잘못 넣었을때 에러가 아니라 이상한 결과를 불러 올 수 있게 된다.
이를 방지 하기 위해서 실제 너가 진짜 타겟하는 타입을 넣을때 명시적으로 전달해줘야만 사용이 가능해 라고 explicit을 선언해주는 것으로
class Meter {
public:
explicit Meter(double d) { std::cout << d << " meters\n"; }
};
printLength(3.14); // ❌ 컴파일 에러
printLength(Meter(3.14)); // ✅ 명시적으로 만들면 허용
이렇게 문제 방지가 가능하게 된다.
이는 결과적으로 명시적으로 호출하고, 자동으로 변환을 금지하고, 의도를 명확히 코드를 짜라는 제한을 둬서 코드에 안정성을 더 높이는 결과를 만들어주게 된다.
이 explicit의 경우는 생성자와 변환 연산자에 선언이 가능하다라는 점까지 알아두고 넘어가자.
여기서 하나 특이점은 복사생성자에 달아 줬을때
이렇게 복사 생성자를 대입연산으로 했을 때도 안된다.
조금 특이하게 복사 초기화일 경우에는 무조건 암묵적 변환을 하기 때문이다.
String s2 = s1;
그래서 위와 같이 대입을 사용해서 초기화를 하면 이를 복사 초기화라고 하고 이때는 무조건 암묵적 변환을 하기에 explicit이 붙어 있는 경우에는 에초에 이런 복사 초기화 방식 자체가 불가능하게 된다.
그래서 복사 생성자에 explicit이 있다면
String s2(s1);
// or
String s2{s1};
의 직접 초기화 방식을 사용해줘야 한다는 것을 알고 있자.
보통은 복사생성자에는 잘 안다는 듯 보인다.
추가로 생성자에서
이렇게 여러개의 char 문자열을 받는다고 할때
클래스에 이렇게 두개의 c스타일의 문자열을 받는 생성자가 있다면
이렇게 문제 없이 변환해서 받아준다.
이 변환은 위에서 말했던것 처럼 직접 String 타입의 생성자를 부른것도 아닌데도 불구하고 String의 생성자를 불러 각각의 값을 생성자에 전달한다는 것으로
이거랑 동일하다고 보면 된다.
C스타일일때는
이런 느낌이라고 생각하면 된다.
또한 이니셜라이져 리스트라고 해서 몇개로 한정되어 있지 않은 매개변수를 받아주는 생성자를 정의할 수 있는 방법으로
이렇게
클래스명(std::initializer_list<전달받을 타입> 매개변수명) {
}
을 사용하면
이렇게 내부에서 받아온 모든 c스타일 문자열을 출력할 수 있도록 만들어 주고
이렇게 만들어서 실행시켜보면
이렇게 출력하는 것을 볼 수 있다.
보면 위에 s1도 출력하는 이유는 std::initializer_list<T>의 경우는 {} 중괄호 초기화일때 가장 우선적으로 선택되기 때문이다라는 점까지 알아두자.
s1이 std::initializer_list 생성자 말고
이 생성자를 선택하게 만들려면
이렇게 중괄호를 사용하는 방식말고 아래와 같이
이런식으로 초기화를 시켜야한다.
이게 변환 생성자로 사실상 explicit이 붙지 않은 대부분의 생성자는 변환 생성자라고 보면 된다.
2. 변환 연산자
변환 연산자는
이런식으로 s1 자체만으로 연산의 결과처럼 변환할 수 있도록 만들어주는 연산자로써
operator 변환하고자하는타입() const{
// 구현코드
}
와 같은 방식으로 구현된다.
만약 우리 코드에서 이걸 구현하고자 한다면 내부 코드에서
이렇게 길이가 0보다 크면 m_chars가 어떤 값이던 들어 있는 것이기 때문에 결과로 true를 반환하고 그게 아니라면 false를 반환해줄것이다.
만약 이걸 빈문자열로 출력하면
false가 출력되고 조건문 내부로는 못들어 간것을 확인할 수 있다.
이렇게 잘 변환이 된건데 위에서 말했다 싶이 변환 연산자에도 explicit을 달아줄 수 있다.
그러면 이렇게 변환함수가 없다고 나오는데 이는 동일하게 대입연산으로 이뤄진 상태이기에 암묵적 변환이 이뤄지기 때문이다.
이때는 직접 bool 타입임을 명시적으로 작성해주면 된다.
근데 여기서 조금 특이한 점은
이렇게 NOT 연산자를 붙이는건 또 explicit가 있어도 허용해준다는 것이다.
여기서 이전에 NOT연산자를 할때는 !가 붙지 않는 연산에 대해서는 NOT 연산자로 연산자 오버로딩을 못한다고 했었는데 이게 사실은 변환 연산자에 !을 붙여서 연산자을 할 수 있기에 차라리 NOT연산자를 오버로딩 할 바에 변환 연산자를 생성해주는게 낫다는 것이다.
그리고 추가로 && 연산자나 ||연산자를 오버로딩 하면 단락연산이 불가했는데
이렇게 해도 이제는 s1 자체로 true나 false를 반환하기에 단락연산이 가능하고 && 연산자 혹은 ||연산자가 시퀀스 포인트로서도 작용을 하게 되기에 더 안전한 방법이 된다.
'Programming Language > C++' 카테고리의 다른 글
Part2::Ch 02. 연산자 오버로딩- 10. 사용자 정의 리터럴 (0) | 2025.05.03 |
---|---|
Part2::Ch 02. 연산자 오버로딩- 09. 호출 연산자 오버로딩, 함수 객체 (0) | 2025.05.03 |
Part2::Ch 02. 연산자 오버로딩- 07. 대입 연산자 오버로딩, 복사 생성자 (0) | 2025.05.03 |
Part2::Ch 02. 연산자 오버로딩- 06. 첨자 연산자 오버로딩 (0) | 2025.05.02 |
Part2::Ch 02. 연산자 오버로딩- 05. 비트 연산자 오버로딩 (0) | 2025.05.02 |