
복사 생성자
다음과 같은 코드는 잘 동작한다
class Knight
{
public:
Knight(int Inhp = 0, int Inmp = 0)
: hp(Inhp), mp(Inmp)
{
std::cout << "Knight(int, int)" << std::endl;
}
private:
int hp{};
int mp{};
};
int main()
{
Knight K1{ 100, 200 };
Knight K2{ K1 };
}
Knight K1{ 100, 200 };은 Knight(int, int) 생성자를 호출하여 초기화가 되는데 Knight K2{ K1 };은 어떤 생성자를 호출할까?
복사 생성자가 호출되면서 해당 코드가 문제 없이 동작한다
복사 생성자란 동일한 타입의 기존 객체로 새로운 객체를 초기화하는데 사용하는 생성자이다
(기존 객체의 복사본을 만드는 생성자)
프로그래머가 명시적으로 복사 생성자를 정의하지 않으면 컴파일러에 의해 암시적 복사 생성자가 public:에 정의된다, 따라서 Knight K2{ K1 };이라는 구문은 암시적 복사 생성자를 호출한 것이다
기본적으로 암시적 복사 생성자는 인자로 전달받은 객체의 멤버 데이터를 복사하여 새로운 객체의 멤버 데이터를 초기화 한다 (K1의 hp, mp값인 100, 200으로 K2를 초기화 한다)
그렇다면 명시적으로 프로그래머가 직접 복사 생성자를 정의하려면 어떻게 해야할까?
Knight(const Knight& InKnight)
: hp(InKnight.hp), mp(InKnight.mp)
{
std::cout << "Knight(const Knight&)" << std::endl;
}
생성자의 매개변수를 자기 자신과 같은 타입의 const&로 넣어주고 초기화하면 된다
만약 인자로 값 전달을 사용한다면 인자와 매개변수가 같은 타입일 경우 복사 생성자를 호출하게된다 (사본 생성 과정), 참조 타입으로 전달하면 사본이 생성되지 않기 때문에 복사 생성자 호출이 되지 않는다
void Foo(Knight InKnight)
{
}
Knight K1{ 100, 200 };
Foo(K1); //Knight의 복사 생성자 호출
접근 제어는 객체별 기준이 아닌 클래스 별 기준으로 작동한다, 따라서 같은 타입의 클래스 타입 객체라면 private: 멤버 데이터에 접근이 가능하다 (Foo()말고 위의 복사 생성자에서 InKnight의 private: 멤버 데이터에 접근이 가능하다)
복사 생성자는 기존 객체를 복사하는 동작 이외에 다른 동작을 수행해서는 안된다, 컴파일러가 특정 경우에 복사 생성자를 최적화하여 제거할 수 있기 때문이다, 만약 복사 생성자에 다른 동작을 추가한다면 해당 동작은 제대로 동작하지 않을것이다
반드시 명시적으로 복사 생성자를 정의해서 사용하는것이 아니라면 암시적 복사 생성자를 이용하는걸 권장한다
(ex) 동적 메모리 할당)
값 타입 반환 함수의 객체를 이용하여 초기화를 하게 되면 임시 객체가 생성되고 해당 임시 객체는 복사 생성자에 의해 복사되어 초기화 된다 (하지만 현대 컴파일러는 복사 생략 (Copy Elision) 최적화를 통해 복사 생성자가 호출되지 않을 수 있다)
Knight GetKnight(int InHp, int InMp)
{
Knight TempKnight{ InHp, InMp };
return TempKnight;
}
int main()
{
Knight K1{ GetKnight(100, 200) };
}
(최신 컴파일러의 경우 복사 생략으로 GetKnight()로 생성된 임시객체가 바로 K1의 메모리에 생성될 수 있다)
이전에 정리한 것과 마찬과지로 =default를 통해 명시적으로 컴파일러에게 기본 복사 생성자를 생성하도록 요청할 수 있다
Knight(const Knight& InKnight) = default;
= delete
특정 클래스의 객체가 복사되지 않기를 원하는 경우에 사용한다, 이는 복사 생성자 함수가 삭제되었음을 표현한다
class Knight
{
public:
Knight(int Inhp = 0, int Inmp = 0)
: hp(Inhp), mp(Inmp)
{
std::cout << "Knight(int, int)" << std::endl;
}
Knight(const Knight& InKnight) = delete;
private:
int hp{};
int mp{};
};
int main()
{
Knight K1{ 100, 200 };
Knight K2{ K1 }; // 복사 생성자 사용 불가 (=delete)
}
클래스 초기화, 복사 생략
여러 생성자가 존재할 때 어떤 생성자가 호출되는지 확인해보자
class Knight
{
public:
Knight() //기본 생성자
{
std::cout << "Knight()" << std::endl;
}
Knight(int a) //일반 생성자
{
std::cout << "Knight(int a)" << std::endl;
}
Knight(const Knight& InKnight) //복사 생성자
{
std::cout << "Knight(const Knight& InKnight)" << std::endl;
}
};
int main()
{
//기본 생성자 호출
Knight K1; //기본 초기화
Knight K2{}; //값 초기화 (권장)
//일반 생성자 호출
Knight K3 = 1; //복사 초기화
Knight K4(1); //직접 초기화
Knight K5{ 1 }; //직접 리스트 초기화 (권장)
Knight K6 = { 1 }; //복사 리스트 초기화
//복사 생성자 호출
Knight K7 = K3; //복사 초기화
Knight K8(K3); //직접 초기화
Knight K9{ K3 }; //직접 리스트 초기화 (권장)
Knight K10 = { K3 }; //복사 리스트 초기화
}
초기화가 수행될 때 클래스의 생성자 집합을 확인하고 가장 일치하는 생성자를 결정하기 위한 오버로드 확인(overload resolution)을 한다, 여기에서 인수의 암시적 변환이 발생할 수 있다
기본 타입의 초기화의 경우 생성자가 따로 없기 때문에 암시적 변환을 사용하여 타입을 맞춰준다
리스트 초기화는 축소 변환 (narrowing conversion)을 허용하지 않는다
복사 초기화는 non-explicit 생성자/변환 함수만 고려한다
리스트 초기화는 다른 일치하는 생성자보다 리스트 생성자를 우선시 한다
불필요한 복사
class Knight
{
public:
Knight() // 기본 생성자
{
std::cout << "Knight()" << std::endl;
}
Knight(const Knight& InKnight) //복사 생성자
{
std::cout << "Knight(const Knight& InKnight)" << std::endl;
}
};
Knight GetKnight()
{
Knight knight;
return knight; // 복사 생성자 호출
}
int main()
{
Knight K1{ GetKnight() };
}
아무런 최적화가 없다는 가정하에 GetKnight()에서 복사 생성자 한번, GetKnight()에서 생성된 임시객체를 이용하여 K1을 초기화 하는 과정에서 복사 생성자가가 또 한번 호출되어 두번의 복사 생성자 호출이 발생하게 된다
이러한 불필요한 복사를 방지하기 위해 현대 컴파일러는 복사 생략 (copy elision)을 하여 컴파일러가 객체의 불필요한 복사를 하지 않도록 최적화한다
(컴파일러가 복사 생성자 호출을 피하도록 코드를 재작성하는 방식)
이러한 방식은 복사 생성자에 다른 로직이 있더라도 복사 생략이 허용된다 (따라서 복사 생성자에 복사 외 다른 기능을 추가하는게 좋지 않다는 의미)
따라서 위 코드에서는 기본 생성자만 한번 호출되고 끝난다
일반적으로 매개변수와 동일한 타입의 인자가 값으로 전달되거나 반환되면 복사 생성자가 호출된다, 마찬가지로 현대 컴파일러에서 이러한 복사 생성자 호출은 생략될 수 있다
class Knight
{
public:
Knight() = default;
Knight(const Something&) {
std::cout << "Copy constructor called\n";
}
};
Knight rvo() {
return Knight{}; // Knight() 및 복사 생성자 호출
}
Knight nrvo() {
Knight s{}; // Knight() 호출
return s; // 복사 생성자 호출
}
int main() {
std::cout << "Initializing s1\n";
Knight s1{ rvo() }; // 복사 생성자 호출
std::cout << "Initializing s2\n";
Knight s2{ nrvo() }; // 복사 생성자 호출
return 0;
}
원래는 4번의 복사 생성자가 호출 되어야 하지만 컴파일러의 copy elision에 의해 생략되어 복사 생성자가 호출되지 않는 모습을 볼 수 있다
C++17에서 복사 생략은 필수가 되었다
복사 생략에는 크게 두가지 개념이 존재한다
1. Optional Elision (선택적 복사 생략)
2. Mandatory Elision (필수적 복사 생략) (C++17이후)