http://sweeper.egloos.com/3148392
수까락 님의 블로그 글을 퍼와서 공부한 내용 입니다.
- User가 선언한 복사 생성자가 없어야 한다.
- User가 선언한 복사 대입 연산자가 없어야 한다.
- 이동 생성자의 경우 User가 선언한 이동 생성자가 없어야 한다.
- 이동 할당 연산자의 경우 User가 선언한 이동 할당 연산자가 없어야 한다.
- User가 선언한 소멸자가 없어야 한다. ( 중요 )
# 여기서 중요한거는 '이녀석' 이라는게 이동 생성자와 이동 할당 연산자 라는 것이다. 즉, 다시 말해 이동생성자와 이동 할당 연산자에만 해당하고 rule of three ( 소멸자, 복사 생성자, 복사 할당 연산자 ) 에는 적용되지 않는다는 소리이다.
rule of three 는 직접 생성해주지 않는 한 기본으로 암시적으로 생성된다?
rule of three 중 소멸자를 만들어 줬는데도, 위 코드가 오류 없이 실행 되는거 보니 암시적으로 복사 생성자 가 만들어져서 그 복사생성자를 호출하는것 같다.
* * 복사생성자를 정의 해주면 기본 defualt 생성자가 안생기고, 기본 defalut 생성자를 정의해줘도, rule of three 는 생성된다. **
#include <string>
#include <iostream>
struct A
{
std::string s;
// 기본 생성자
A() : s("hello") {std::cout << "A의 기본 생성자\n";}
// copy constructor
A(const A& o) : s(o.s) { std::cout << "A 의 copy constructed\n"; }
// copy assignment
A& operator=(const A& other)
{
s = other.s;
std::cout << "A 의 copy assigned\n";
return *this;
}
// move constructor
A(A&& other) noexcept : s(std::move(other.s)) { std::cout << "move constructed\n"; }
// move assignment
A& operator=(A&& other)
{
s = std::move(other.s);
std::cout << "move assigned\n";
return *this;
}
};
* 이동생성자가 noexcept 인 이유는 다른 글에서 설명!
struct B : A
{
std::string s2;
// B 는 아무런 user 코드가 없기 때문에 자동으로 rule of five 생성
// 암시적으로 이동 생성자와 이동 할당 연산자가 생성되었음.
// B(B&&)
// B& B::operator=(B&&)
// B(B&&) 이동 생성자 호출시, A의 이동생성자 호출(s가 move 됨)
// -> s2에 대해서도 B의 이동생성자 호출
// B& operator=(B&&) 이동 할당 연산자 호출시에도 마찬가지...
};
struct C : B
{
// User가 직접 소멸자를 선언하였기에,
// 이동 생성자 및 이동 할당 연산자가 암시적으로 생성되지 않았음
~C() {}
};
struct D : B
{
D() {}
~D() {}
// 소멸자가 있어 암시적으로 생성X, 명시적으로 이동 생성자를 선언
D(D&&) = default;
// 명시적으로 이동 할당 연산자를 선언
D& operator=(D&&) = default;
};
아래 의 f 함수가 있다고 할 때
A f(A a) { return a; }
main 함수에서 각각을 알아보면
// 아래 함수에는 이동 생성자에 대한 코드만 포함되어 있지만,
// 아래 코드들을 이동 할당 연산으로 대입시켜도 문제 없음.
int main()
{
std::cout << "A 이동 생성 시도\n";
// 임시 객체에 의한 이동 생성자 호출
A a1 = f(A());
// move 연산자에 의한 이동 생성자 호출
A a2 = std::move(a1);
std::cout << "B 이동 생성 시도\n";
B b1;
// B 가 암시적 기본 생성자에 의해 생성될 때 상속해준 Base 클래스인 A도 기본 생성자에 의해 생성.
std::cout << "이동전 b1.s = " << b1.s << "\n";
B b2 = std::move(b1);
// B의 이동 생성자 호출시, A의 이동생성자 호출 -> B의 멤버변수 s2에 대해서도 이동생성자 호출
std::cout << "이동후 b1.s = " << b1.s << "\n";
std::cout << "C 이동 생성 시도\n";
C c1;
// C 가 디폴트 기본 생성자에 의해 생성될 때 상속해준 Base 클래스인 B 클래스도 디폴트 기본 생성자 호출
// -> B 클래스 생성자 에서 A의 디폴트 생성자 호출 ( A 객체 생성. -> B 객체 생성 -> C 객체 생성 )
// 선처리 영역에서 부모 클래스의 생성자를 호출하기 때문에 스택처럼 파고드는 개념이다.
// 소멸자 선언으로 인해 이동 생성자가 암시적으로 생성되지 않았음.
// 그러나, rule of three에 의해 암시적으로 생성된 복사 생성자는 인자로 const 를 받기 때문에
// c1 의 우측값도 인자로 받을 수 있다. 그래서 C 클래스의 암시적 복사생성자를 호출하고
// 복사 생성자 안의 전처리 단계에서 B의 암시적 복사생성자, A의 명시적 복사 생성자를 호출한다.
C c2 = std::move(c1);
// 그래서 결과가 아래 그림과 같이 나온다.
std::cout << "Trying to move D\n";
D d1;
d1.s2 = "world";
// d1 { s="hello", s2="world" }
// 명시적으로 이동 생성자를 선언하였기에, 이동 생성자 호출
D d2 = std::move(d1);
// d1 { s = "", s2 = "" }
// d2 { s="hello", s2="world" }
}
C의 결과
그렇다면 아래와 같은 코드가 있다고 그러면 결과는 어떻게 될까??
struct A
{
std::string s;
// 기본 생성자
A() : s("hello") { std::cout << "A 생성자 호출했다" << std::endl; }
// 복사 생성자
A(const A& o) : s(o.s) { std::cout << "A의 copy constructed\n"; }
// 대입 연산자
A& operator=(const A& other)
{
s = other.s;
std::cout << "A의 copy assigned\n";
return *this;
}
// 이동 생성자
A(A&& other) noexcept : s(std::move(other.s)) { std::cout << "move constructed\n"; }
// 이동 할당 연산자
A& operator=(A&& other)
{
s = std::move(other.s);
std::cout << "move assigned\n";
return *this;
}
};
A f(A a) { return a; }
struct B : A
{
std::string s2;
B() { std::cout << "B 생성자 호출 됐다. " << std::endl; };
B(const B& b) { std::cout << "B의 복사 생성자 호출 " << std::endl; }
};
struct C : B
{
C() { std::cout << "C 생성자 호출 됐다." << std::endl; };
C(const C& c) { std::cout << " C의 복사생성자 호출 " << std::endl; }
~C() {}
};
int main()
{
std::cout << "C 이동 생성 시도\n";
C c1;
// 소멸자 선언으로 인해 이동 생성자가 암시적으로 생성 되지 않았음.
std::cout << std::endl;
C c2 = std::move(c1);
}
참고로 위 코드는 C의 복사생성자를 명시적으로 생성해줬는데 그렇지 않아도 암시적으로 생성되기 때문에 명시적으로 생성해주지 않았어도 C의 암시적 복사생성자를 호출 할 거다.
4번째 줄인 "C 생성자 호출 됐다." 까지는 Okay
그 다음에는
C c2 = std::move(c1);
에서 C 클래스의 이동 생성자가 암시적으로 생성되지 않았으니, const 때문에 우측값도 인자로 받을 수 있는 복사 생성자를 호출하고 ( 여기 까지 맞음 ) -> 전처리 단계에서 B의 복사 생성자를 호출하는게 아닌가?
* 정확한 이유는 모르는데 C (const C& c) 에서 우측값을 받을 수 있다고 해도 소문자 c는 어쨋든 좌측값이고 그래서
B(const B& b) 가 아니라 B() 를 호출해주지 않나 싶다..
혹시 B의 s2 값이 없어서 복사 생성자를 호출 안하나 싶어 위 처럼 해보았는데,
c1.s2 값을 진짜 복사를 안하고 그냥 B의 생성자만 생성자만 호출한다
실제로 이렇게 해주면 B의 복사 생성자를 호출해주긴 한다.
그런데 또 희한하게 C의 복사 생성자를 주석 처리해주면
B의 복사생성자를 잘 호출하는것을 알 수 있다.
그리고 B의 복사생성자가 명시적으로 있으니 A의 복사생성자가 아니라 A의 생성자를 호출한것같다.
03.07 추가)
Rule of Five에 의해 C의 이동생성자가 호출 됐고, B도 이동생성자를 호출할라고 하니깐 B는 이미 복사생성자가 정의되어 있어서 Rule of Five의 암시적 이동생성자가 만들어지지 않아서 복사 생성자를 호출됐다.
그 다음 A는.. B가 복사 생성자가 호출 되었으니 복사 생성자가 호출 되어야 될 것 같은데
위에서 봤다 싶이 복사 생성자 일때는 그냥 일반 생성자가 호출된다.
실제로 B의 복사생성자도 주석처리하면 A의 복사생성자를 호출한다.
'Language & etc > C++' 카테고리의 다른 글
const_cast에 대해서 (0) | 2023.02.07 |
---|---|
std::Vector push_back() vs emplace_back() (0) | 2022.07.31 |
비트 필드 (0) | 2022.07.27 |
[컴파일/링킹]에서 본 Static Member Variables, function... (0) | 2022.07.02 |
std::Move, perfect forwarding - ① (0) | 2022.06.19 |