[C/C++] R-value 참고자료

그림자왕국·2020년 9월 19일
1

C++

목록 보기
16/24

R-value로 인수를 받을 경우 copy가 일어나지 않는다.
L-value는 1회, 일반 Value(복사)는 3회 발생

1번 2번 메모리를 보면 참조자(&) 자체도 스택에 할당된다는 걸 확인할 수 있다.
string의 문자열 "abc"는 힙에 저장된다.

std::move()를 사용하지 않고 s 참조자를 그냥 대입했을 때 1copy가 일어난다.

만약 string b가 일반 변수가 아닌 참조자(&)였으면 0회 copy가 일어난다.
하지만 단점으론 참조자의 원본인 s의 값이 사라졌을 경우 같이 null이 된다. (참조자 특성)
그래서 std::move()로 소유권을 확실히 이전해주며, copy도 일어나지 않는 게 좋다.


이동생성자

node(int && a) :data(std::move(a)) {
		cout << "이동생성자 호출" << endl;
	};

이동대입연산자

	node & operator=(node && other) noexcept {
		this->pys = move(other.pys);
		cout << "이동대입연산자 호출" << endl;
		return *this;
	}
    
    *n = move(*p); // 포인터가 대입연산자 오버로딩을 호출하려면 간접연산(*)을 해야 한다.

연산자 오버로딩 케이스

node& operator+(node& other); // 일반 객체용 연산자 오버로딩
node& operator+(node* other); // 포인터용 연산자 오버로딩

반환형 * , & 비교

Node other;
Node* test1() {
	return &other;
}

Node& test2() {
	return other;
}
main() {
	Node* a = test1();
	Node& b = test2();

	Node* a2 = &test2();
	Node& b2 = *test1();
    }

모든 반환형은 연산자를 통해 접근 가능하다. (동작도 같다)

Node & operator() return *this;
Node * operator() return this;

둘의 차이는 전혀 없다. (동일하다)

But,

Node operator() return *this; // 반환형이 참조나 주소가 아닌 값을 반환할 경우

복사 생성자가 호출된다.

Precautions - move()의 예외사항

1. Copy 발생

const value가 std::move()의 인수가 되면 반드시 Copy가 일어난다.
즉, move에 const 상수가 인수로 들어온다면 move가 일어나지 않고 새로 복사된 채 반환된다.

func(const value & other) { // 이는 const 참조자도 마찬가지다.
  string str = std::move(other); 
  }
  
value a = "abcd";
func(a);
cout << a << endl; // "abcd"
  

func(a)를 통해 other가 main의 a를 참조하고 있지만, func 스택 상에서 std::move(other)를 사용할 시 other의 const 구문으로 인해 value a의 ownership을 str에 이전하지 않고 대신 deep Copy가 일어난다. 즉 main의 a는 move()로 인해 null이 되지 않는다.

여담으로 const function caller앞에 위치한 변수가 const reference라면 referencing(pointer copy)가 일어나고 그렇지 않으면(const value) value copy가 된다.


2. 참조 횟수 증가

move() 앞에 위치한 변수가 R-value Reference(&&)일 경우 값이 이동되는 대신 참조된다.
Const L-value Reference도 동일하다.

	string str = "asfd";
	string&& str2 = std::move(str); // or const string & str2 = move(str);
  	cout << str << str2 << endl; // str과 str2 모두 출력된다.

R-value 레퍼런스는 고유의 값을 가지는 게 아니고 참조자이기에 move(str)를 통해 str의 값을 건네줘도 값이 이동되는 게 아니라, str가 건네준 r-value가 같이 참조될 뿐이다.
(결국 "asfd"는 str과 str2 둘한테 참조된다.)

하지만 str2가 참조자가 아니고 일반 변수일 경우 str의 값은 str2에게 이동(move) 된다.

string str = "asfd";
string str2 = std::move(str); // 이제, str의 값은 이동된다. (str is null)

Tip : 가장 낮은 copy로 데이터 복사하기

해당 인터페이스는 가장 효율적인 방식으로 인수로 L-value가 넘어올 때는 1-copy,
R-value가 넘어올 때는 0-copy를 달성함으로 가장 좋은 성능의 인터페이스라고 볼 수 있다.
0-copy가 일어나는 이유는 Copy Elision 때문이다.

Copy Elision (optimization)
다음과 같이 일반 변수에 Rvalue를 받으면

string name = "abcde";

컴파일러가 최적화하여 name이 가리키는 힙영역에 찾아서 "abcde"를 넣는다.
즉, 컴파일러가 메모리 어딘가에 먼저 "abcde"를 써놓고 name이 그 곳을 가리키게 하지 않는다. (느리니까)

setName(string name) {
  mname = std::move(name); // copy가 일어나지 않는다.
}

RVO (함수 리턴 최적화)

RVO는 컴파일러가 지원하는 함수의 리턴을 최적화시키는 최적화 기법 중 하나이다.
코드 상으로 일어나는 불필요한 복사와 할당을 제거하기 위해 컴파일러가 어셈블리어를 수정하여 사용된다.

string getString() {
	string s = "strs";
	return s; // return value address는 
}
string a = getString(); // RVO가 개입되어 실제론 복사가 일어나지 않고 a가 "str"을 바로 가르키게 한다.

스택 프레임에는 지역변수 뿐만 아니라 args와 반환 주소를 저장하는 return value adress도 가지고 있는데 string s가 가르키는 "strs"는 반환 후 결국 rva가 가르킬 예정이니 return value address의 목적지인 a한테 처음부터 직접적으로 "strs"를 가르키게 하고, 대신 지역변수 s를 처음부터 존재하지 않게 지워준다. (a가 직접 "strs"를 가르키면 s는 존재하지 않아도 되니깐)

즉, 보이는 것과 달리 실제로는 0-copy가 일어나게 된다. (반환을 위해 move()가 없어도 된다!)
하지만 함수 상에서 if문이 있을 경우 return 값이 불명확하여 RVO가 적용되지 않는 단점이 있다. RVO의 핵심은 반환시 0-copy를 위해 굳이 std::move()를 사용할 필요가 없다는 거다. (컴파일러가 도와준다.)

profile
언리얼 엔진 매니아입니다.

0개의 댓글