I'm FanJae.

[C++ Intermediate] 객체의 변환(Conversion) 본문

C++/Intermediate

[C++ Intermediate] 객체의 변환(Conversion)

FanJae 2024. 9. 13. 22:21

1. 변환 연산자와 변환 생성자

 

1-1. 변환 연산자

#include <iostream>

class Int32;
{
   int value;
public:
   Int32() : value(0) { }
   
   operator int() const { return value; }
};

int main()
{
    int pn;   // primitive type
    Int32 un; // user type
    
    pn = un; // un.operator int()
    un = pn; // pn.operator Int32() 이는 불가능
    	     // 1. un.operator=(pn)
             // 2. Int32(pn)
}

- 객체가 다른 타입으로 변환 될 때 호출되는 함수이다.

 

operator TYPE()
{
   return value;
}

- 반환 타입을 표기하지 않는다. 함수 이름에 반환 타입이 포함되어 있다.

 

※ 변환 연산자와 변환 생성자의 차이

- int pn, Int32 un 일 때

pn = un; un.operator int();
변환 연산자
객체(Int32) -> int로 변환될 때
un = pn; pn.operator Int32() 는 만들 수 없다.
① 대입연산자, un.operator=(int)
② 변환생성자, Int32(pn)

 

1-2. 변환 생성자

- 변환 생성자(인자가 한 개인 생성자)가 있다면 총 4개의 형태가 나온다.

#include <iostream>

class Int32
{
    int value;
public:
    Int32(int n) : value(n) { }

};

int main()
{
    Int32 n1(3);
    Int32 n2 = 3;
    Int32 n3{3};
    Int32 n4 = {3};

    n1 = 3;
}

 

Int32 n1(3); direct copy initialization
Int32 n2 = 3; copy initialization
Int32 n3{3}; direct copy initialization C++ 11
Int32 n4 = {3}; copy initialization C++ 11
n1 = 3; cconversion (int->Int32)

 

Int32 n2 = 3;

- 이것이 C++ 14와 C++ 17을 기점으로 약간 다르다.

 

① C++14 까지 (Int32 n2 = 3; -> Int32 n2 = Int32(3);

- 인자가 한 개인 생성자를 사용해서 Int32 임시객체 생성

- 생성된 임시객체를 복사 생성자(C++98) 또는 move 생성자(C++11 이후)를 사용해서 n2에 복사(이동)

- 대부분 컴파일러가 최적화를 통해서 임시 객체 생성이 제거됨.

 

② C++17 이후

- 임시객체를 생성하지 않고, 인자 한 개인 생성자 호출

n1 = 3; -> n1 = Int32(3);

- 인자가 한 개인 생성자를 사용해서 Int32 임시객체를 생성한다.

- 생성된 임시객체를 디폴트 대입 연산자를 사용해서 n1에 대입한다.

- 대부분 컴파일러 최적화를 통해서 임시객체 생성이 제거된다.

- 디폴트 대입연산자가 삭제된 경우는 컴파일 에러가 발생한다.

#include <iostream>

class Int32
{
    int value;
public:
    Int32(int n) : value(n) { }
    Int32& operator=(const Int32&) = delete;
};

int main()
{
    Int32 n1(3);
    Int32 n2 = 3;
    Int32 n3{3};
    Int32 n4 = {3};

    n1 = 3;
}

- 이와 같은 경우 디폴트 대입 연산자의 삭제로 컴파일 에러가 발생한다.

 

2. Explicit 생성자

class Vector
{
public:
	explicit Vector(int size) {}
};

void foo(Vector v) {} // Vector v = 3;

int main()
{
	Vector v1(3);
	Vector v2 = 3; // error
	Vector v3{ 3 };
	Vector v4 = { 3 }; // error

	v1 = 3; // error

	foo(3); // error
}

- 생성자가 암시적 변환의 용도로 사용될 수 없게 한다.

- 직접(Direct) 초기화 만 가능하고 복사(Copy) 초기화도 사용할 수 없다.

- 클래스에 따라서 Explicit를 사용할지 판단을 잘 해야 한다.

void f1(Int32 n)
{

}
f1(3);

 

void f2(Vector v)
{

}
f2(3);

- explicit를 붙일지 말지에 대한 여부를 잘 판단하여 붙여야 한다.

 

2-1. Explicit 변환 연산자

- 객체의 유효성을 if 문으로 조사하고 싶다.

#include <iostream>
class Machine
{
	int data = 10;
	bool state = true;
public:
};

int main()
{
	Machine m;

	if (m)
	{

	}
}

- 객체의 유효성을 if 문으로 조사하고 싶으면, Machine을 bool로 변환할 수 있으면 된다는 의미이다.

#include <iostream>
class Machine
{
	int data = 10;
	bool state = true;
public:
};

int main()
{
	Machine m;

	if (m)
	{

	}
}

- 이와 같이 operator bool()을 제공하면 된다.

- 하지만, 이는 side effect가 많다.

 

2-1-1. 예제의 문제점

#include <iostream>
class Machine
{
	int data = 10;
	bool state = true;
public:
    operator bool() { return state; }
};

int main()
{
	Machine m;

    bool b1 = m; // ok
    bool b2 = static_cast<bool>(m); // ok
    
    m << 10;
    
	if (m)
	{

	}
}
m << 10;

- 여기서 이 부분은 시프트 연산이다. m이 bool로 변환되기 때문에 이는 곧 정수로 인식한다.

 

2-1-2. 문제 해결법

① Explicit operator bool()

- C++11 부터 생성자 뿐 아니라 변환 연산자도 explicit를 붙일 수 있다.

- bool로의 암시적 변환은 허용되지 않는다.

- if 문 안에서는 사용될 수 있다.

- safe bool이라고도 한다.

#include <iostream>
class Machine
{
	int data = 10;
	bool state = true;
public:
    explicit operator bool() { return state; }
};

int main()
{
	Machine m;

    bool b1 = m; // ok
    bool b2 = static_cast<bool>(m); // ok
    
    m << 10;
    
    if (m)
    {

    }
}

 

2-1-3. C++ 버전과 Explicit

explicit 변환 생성자 C++98 부터 제공되는 문법
explicit 변환 연산자 C++11 부터 제공되는 문법
explicit(bool) C++20 부터 제공되는 문법

 

2-2. explicit(bool) // C++20

#include <iostream>
#include <type_traits>

template<class T>
class Number
{
    T value;
public:
    explicit(!std::is_integral_v<T> ) Number(T v) : value(v) {}
};

int main()
{
    Number n1 = 10;
    Number n2 = 3.4;
}
explicit(!std::is_integral_v<T>)

- 이렇게 처리할 경우, 정수일때만 explicit를 허용하지 않겠다는 의미가 된다.

- 즉 explicit(조사식) 과 같은 형태로 사용할 수 있는 방식이다.

 

3. 변환 예제

 

3-1. nullptr

- 널 포인터를 의미한다.

- 포인터 초기화 시 0을 사용하지 말고 nullptr를 사용하라.

- nullptr의 타입은 std::nullptr_t이다.

void foo(int* p) { }
void goo(char* p) {}

struct nullptr_t
{
	template <class T>
	constexpr operator T* () const {
		return 0;
	}
};

nullptr_t xnullptr;

int main()
{
	foo(xnullptr); // xnullptr.operator int*()
	goo(xnullptr); // xnullptr.operator char*()

}

- 과거에는 이런 방식으로 만들었다.

 

3-2. return type resolver

#include <iostream>

/*
template<class T>
T* Alloc(std::size_t sz)
{
   return new T[sz];
}
*/
struct Alloc
{
      std::size_t size;
      
      Alloc(std::size_t sz) : size(sz) { }
      
      template<class T>
      operator T*() { return new T[size]; } 
};
int main(void)
{
//    int* p1 = Alloc<int>(10);
//    double* p2 = Alloc<double>(10);
    
      int* p1 = Alloc(10);     // 임시객체.operator int*()
      double* p2 = Alloc(10);  // 임시객체.operator int*()
}

- <int>, <double> 등을 매번 명시해줘야 해서 다소 번거로운데 이를 생략하고 사용할 수 없을까?

- 좌변을 보고 우변의 반환 타입을 자동으로 결정하는 기술이다.

 

3-3. 람다 표현식과 변환

int main()
{
    auto f1 = [](int a, int b){ return a + b; };
    
    int (*f2)(int, int) = [](int a, int b){return a+ b;};
                         // 임시객체
                         // 임시객체.operator 함수포인터()

- 람다 표현식 자체가 임시객체를 만든다.

- 따라서, 이는 임시 객체가 함수객체로 들어가는 과정이다. 이에 따라서 임시객체.operator 함수포인터() 형태이다.

Comments