[C++ Primer Plus] Chapter 18. (1) Move Semantics와 rvalue참조, 새로운 클래스 형태
Table of Contents
C++ 기초 플러스 책을 읽고 공부한 노트입니다.
Move Semantics의 필요성 #
- Move Semantics는 객체의 리소스(동적 할당된 메모리 같은 것)를 또 다른 객체로 이동하는 것을 의미한다.
- 만약 1,000자의 문자
string
을 20,000개 가지고 있는vector
가 있다고 하자. - 이것을 복사하면?
vector
복사 생성자가new
를 이용해서 20,000개의string
객체를 넣을 메모리를 만들어서 대입하고,string
복사 생성자가 또new
를 이용해서 1,000개의 문자를 넣을 메모리를 만들어서 대입한다.- 그러면 총 20,000,000개의 문자가 복사된다.
- 하지만 굳이 이런 복사를 하지 않아도 되는 경우가 있다.
- 아래 예시 (2)의 경우가 그렇다.
AllCaps()
에서 임시 객체temp
를 생성하고 그리고 함수 종료 시 임시 객체를 삭제한다.- 20,000,000개의 문자가 새로운 곳(
copy2
)에 저장되고 이전 저장소(temp
)가 삭제되는 대신 문자가 남아 있는 자리에 이름만copy2
로 바꿔주면 어떨까? - 이런 접근법을 Move Semantics라고 한다.
- Move Semantics을 위해서는…
- rvalue 참조를 사용하는 이동 생성자를 만들어야 한다.
- 만약 1,000자의 문자
vector<string> AllCaps(const vector<string>& vs)
{
vector<string> temp;
// 매개변수로 전달된 vs를 모두 대문자로 만들어서 temp에 저장한다.
return temp;
}
int main()
{
vector<string> strs;
// strs가 1,000자의 문자 string을 20,000개 가지고 있다고 가정하자.
vector<string> copy1(strs); // (1)
vector<string> copy2(AllCaps(strs)); // (2)
}
이동 생성자 및 이동 대입 연산자 예시 #
- 이동 생성자
Useless::Useless(Useless && f)
- 매개변수로 전달된
f
의 주소를 가로채고,nullptr
로 설정해준다. 이것은 소멸자가 같은 주소에 대해 두 번delete []
하는 것을 막는다. 이것을 pilfering(필퍼링, 좀도둑질)이라고 한다.
- 매개변수로 전달된
#include <iostream>
using namespace std;
class Useless
{
private:
int n; // 매개변수 수
char* pc; // 데이터를 가리키는 포인터
static int ct; // 전체 객체 수 카운트
void ShowObject() const;
public:
Useless();
explicit Useless(int k);
Useless(int k, char ch);
Useless(const Useless& f); // 일반적인 복사 생성자
Useless(Useless&& f); // 이동 생성자
~Useless();
Useless operator+(const Useless& f)const;
Useless& operator=(const Useless& f); // 일반적인 복사 대입 연산자
Useless& operator=(Useless&& f); // 이동 대입 연산자
void ShowData() const;
};
int Useless::ct = 0;
Useless::Useless()
{
++ct;
n = 0;
pc = nullptr;
cout << "기본 생성자 호출됨; 총 객체 수: " << ct << endl;
ShowObject();
}
Useless::Useless(int k) : n(k)
{
++ct;
cout << "int 생성자 호출됨; 총 객체 수: " << ct << endl;
pc = new char[n];
ShowObject();
}
Useless::Useless(int k, char ch) : n(k)
{
++ct;
cout << "int, char 생성자 호출됨; 총 객체 수: " << ct << endl;
pc = new char[n];
for (int i = 0; i < n; i++)
pc[i] = ch;
ShowObject();
}
Useless::Useless(const Useless& f) : n(f.n) // 일반적인 복사 생성자
{
++ct;
cout << "복사 생성자 호출됨; 총 객체 수: " << ct << endl;
pc = new char[n];
for (int i = 0; i < n; i++)
pc[i] = f.pc[i];
ShowObject();
}
Useless::Useless(Useless&& f) : n(f.n) // 이동 생성자
{
++ct;
cout << "이동 생성자 호출됨; 총 객체 수: " << ct << endl;
pc = f.pc; // 주소 가로채기
f.pc = nullptr; // 이전 객체가 아무것도 반환하지 않도록 함
f.n = 0;
ShowObject();
}
Useless::~Useless()
{
cout << "소멸자 호출됨; 남은 객체 수: " << --ct << endl;
cout << "삭제된 객체 정보: ";
ShowObject();
delete[] pc;
}
Useless Useless::operator+(const Useless& f)const
{
cout << "연산자 +() 진입\n";
Useless temp = Useless(n + f.n);
for (int i = 0; i < n; i++)
temp.pc[i] = pc[i];
for (int i = n; i < temp.n; i++)
temp.pc[i] = f.pc[i - n];
cout << "연산자 +() 나옴\n";
return temp;
}
Useless& Useless::operator=(const Useless& f) // 일반적인 복사 대입 연산자
{
if (this == &f) return *this;
delete[] pc;
n = f.n;
pc = new char[n];
for (int i = 0; i < n; i++)
pc[i] = f.pc[i];
return *this;
}
Useless& Useless::operator=(Useless&& f) // 이동 대입 연산자
{
if (this == &f) return *this;
delete[] pc;
n = f.n;
pc = f.pc; // 주소 가로채기
f.pc = nullptr; // 이전 객체가 아무것도 반환하지 않도록 함
f.n = 0;
return *this;
}
void Useless::ShowObject() const
{
cout << "매개변수 수: " << n;
cout << " 데이터 주소: " << (void*)pc << endl;
}
void Useless::ShowData() const
{
if (n == 0)
cout << "(객체 없음)";
else
for (int i = 0; i < n; i++)
cout << pc[i];
cout << endl;
}
int main()
{
{
Useless one(10, 'x');
cout << endl;
Useless two = one; // 복사 생성자 호출
cout << endl;
Useless three(20, 'o');
cout << endl;
Useless four(one + three); // +()연산자와 이동 생성자 호출
cout << endl;
cout << "모든 객체 정보 표시해보기 \n";
cout << "one: ";
one.ShowData();
cout << "two: ";
two.ShowData();
cout << "three: ";
three.ShowData();
cout << "four: ";
four.ShowData();
cout << endl;
}
}
int, char 생성자 호출됨; 총 객체 수: 1
매개변수 수: 10 데이터 주소: 01526730
복사 생성자 호출됨; 총 객체 수: 2
매개변수 수: 10 데이터 주소: 01526180
int, char 생성자 호출됨; 총 객체 수: 3
매개변수 수: 20 데이터 주소: 01529F50
연산자 +() 진입
int 생성자 호출됨; 총 객체 수: 4
매개변수 수: 30 데이터 주소: 01529F90
연산자 +() 나옴
이동 생성자 호출됨; 총 객체 수: 5
매개변수 수: 30 데이터 주소: 01529F90
소멸자 호출됨; 남은 객체 수: 4
삭제된 객체 정보: 매개변수 수: 0 데이터 주소: 00000000
모든 객체 정보 표시해보기
one: xxxxxxxxxx
two: xxxxxxxxxx
three: oooooooooooooooooooo
four: xxxxxxxxxxoooooooooooooooooooo
소멸자 호출됨; 남은 객체 수: 3
삭제된 객체 정보: 매개변수 수: 30 데이터 주소: 01529F90
소멸자 호출됨; 남은 객체 수: 2
삭제된 객체 정보: 매개변수 수: 20 데이터 주소: 01529F50
소멸자 호출됨; 남은 객체 수: 1
삭제된 객체 정보: 매개변수 수: 10 데이터 주소: 01526180
소멸자 호출됨; 남은 객체 수: 0
삭제된 객체 정보: 매개변수 수: 10 데이터 주소: 01526730
강제 이동 #
- 이동 생성자와 이동 대입 연산자는 rvalue와 함께 동작한다.
- 하지만 lvalue와 함께 사용하고 싶을 때가 있다.
Useless choices[10];
Useless best;
int pick;
// 한 객체를 선택하고 pick 번째를 색인 하기 위한 설정...
best = choices[pick]; // 이렇게 하나를 선택하고 이전 배열을 버린다면...
// choices[pick]은 lvalue이지만 rvalue로 사용해서 이동 대입 연산자를 사용할 수 있다면 편리할 것이다.
static_cast<>
연산자를 이용해서 객체를Useless&&
타입으로 변환하면 가능하다.- C++11에서는 이와 비슷한 방법을 제공한다.
utility
헤더파일에 선언된move()
함수를 이용할 수 있다.
Useless one(10, 'x');
cout << endl;
Useless two;
cout << endl;
two = one; // 복사 대입
cout << endl;
cout << "모든 객체 정보 표시해보기 \n";
cout << "one: ";
one.ShowData();
cout << "two: ";
two.ShowData();
cout << endl;
two = move(one); // 강제 이동 대입
cout << endl;
cout << "모든 객체 정보 표시해보기 \n";
cout << "one: ";
one.ShowData();
cout << "two: ";
two.ShowData();
cout << endl;
//...
복사 대입 연산자 호출
모든 객체 정보 표시해보기
one: xxxxxxxxxx
two: xxxxxxxxxx
이동 대입 연산자 호출
모든 객체 정보 표시해보기
one: (객체 없음)
two: xxxxxxxxxx
//...
move()
는 이동 대입 연산자를 호출한다. 만약 이동 대입 연산자를 정의하지 않았다면,- 복사 대입 연산자를 사용한다. 그것도 정의하지 않았다면, 대입은 절대 허용되지 않는다.
새로운 클래스 형태 #
-
복사 생성자, 복사 대입 연산자를 제공한다면,
- 컴파일러는 이동 연산자나 이동 대입 연산자를 자동으로 제공하지 않는다.
-
반대로, 이동 연산자나 이동 대입 연산자를 제공한다면,
- 컴파일러는 복사 생성자나 복사 대입 연산자를 제공하지 않는다.
- 기본 함수, 삭제 함수
default
키워드- 컴파일러가 생성하는 기본 함수를 사용하고자하는 경우
delete
키워드- 컴파일러가 특정 함수를 사용하는 것을 방지하고자 하는 경우
class Someclass
{
public:
// 컴파일러가 생성한 기본 생성자를 사용한다.
Someclass() = default;
// 복사 생성자와 복사 대입 연산자를 사용할 수 없다.
Someclass(const Someclass&) = delete;
Someclass& operator=(const Someclass&) = delete;
// 컴파일러가 생성한 이동 생성자와 이동 대입 연산자를 사용한다.
Someclass(Someclass&&) = default;
Someclass& operator=(Someclass&&) = default;
Someclass& operator+(const Someclass&) const;
};
int main()
{
Someclass one;
Someclass two;
Someclass three(one); // (X) lvalue는 허용하지 않는다.
Someclass four(one + two); // (O) rvalue는 허용한다.
}
class Someclass
{
public:
// delete키워드로 특정 변환을 막을 수 있다.
void DoSomething(int) = delete;
void DoSomething(double);
};
int main()
{
Someclass one;
one.DoSomething(1); // (X) 컴파일 에러
one.DoSomething(1.1); // (O)
}
- 위임 생성자
- 다른 생성자의 정의의 일부를 생성자로 사용할 수 있도록 한 것이다.
- 한 생성자가 임시로 다른 생성자가 생성한 객체에 책임을 전가하기 때문에 위임(delegation)이라고 한다.
class Notes
{
private:
int i;
double d;
string st;
public:
Notes();
Notes(int);
Notes(int, double);
Notes(int, double, string);
};
Notes::Notes(int ii, double dd, string stt) : i(ii), d(dd), st(stt) { }
Notes::Notes() : Notes(0, 0.0, "") { }
Notes::Notes(int ii) : Notes(ii, 0.0, "") { }
Notes::Notes(int ii, double dd) : Notes(ii, dd, "") { }
- 상속 생성자
- 파생 클래스에서 기본 클래스의 모든 생성자를 사용할 수 있다.
- 상속된 기본 클래스 생성자는 오직 기본 클래스 멤버만을 초기화한다는 것을 명심하라.
class Base
{
private:
int i;
double d;
public:
Base() : i(0), d(0.0) {}
Base(int ii) : i(ii), d(0.0) {}
Base(double dd) : i(0), d(dd) {}
Base(int ii, double dd) : i(ii), d(dd) {}
};
class Derived : public Base
{
private:
short s;
public:
using Base::Base; // Base 클래스의 생성자를 사용할 수 있다.
Derived() : s(0) {}
Derived(double dd) : Base(dd), s(0) {}
Derived(int ii) : Base(ii), s(0) {}
};
int main()
{
Derived derived(10, 1.8);
// Derived에 (int, double) 생성자가 없으므로
// Base(int, double)을 사용한다.
// 당연히 Derived의 멤버는 초기화되지 않는다.
}
- 가상 함수 관리
- 가상 지정자
override
를 사용함으로써 가상 함수를 오버라이드 하겠다는 것을 명시할 수 있다.- 선언이 기본 함수와 일치하지 않았을 때,
override
를 사용하지 않으면 단순히 기본 함수를 숨기기만 한다.- 하지만
override
를 사용하면 컴파일 에러가 난다.
- 지정자
final
은 파생 클래스에서 재정의할 수 없는 가상 함수를 지정한다.
- 가상 지정자
class Base
{
public:
virtual void Function(int ch) const // int형 매개변수
{
cout << "Base" << endl;
}
};
class Derived : public Base
{
public:
virtual void Function(int* ch) const // int*형 매개변수 (다르다)
{
cout << "Derived" << endl;
}
};
int main()
{
int num = 1;
Derived d;
d.Function(&num); // (O) Derived의 Function이 Base의 Function를 숨긴다.
}
class Base
{
public:
virtual void Function(int ch) const // int형 매개변수
{
cout << "Base" << endl;
}
};
class Derived : public Base
{
public:
// (X) override 지정자를 사용했다. 매개변수가 기본 함수와 달라서 컴파일 에러가 난다.
virtual void Function(int* ch) const override
{
cout << "Derived" << endl;
}
};
class Base
{
public:
virtual void Function(int ch) const final // final 지정자
{
cout << "Base" << endl;
}
};
class Derived : public Base
{
public:
virtual void Function(int ch) const // (X) final인 함수를 오버라이드할 수 없다.
{
cout << "Derived" << endl;
}
};