Copy elision, rvalue referen, move constructor
모두의 코드 내용을 공부하고 정리한 내용입니다.
복사 생략
class A {
int data_;
public:
A(int data) : data_(data) { std::cout << "일반 생성자 호출!" << std::endl; }
A(const A& a) : data_(a.data_) {
std::cout << "복사 생성자 호출!" << std::endl;
}
};
A c(A(2)); // 일반 생성자만 호출됨
- 위 예시에서 내가 생각한 것은 A(2)에서 일반 생성자 호출후 임시 객체에 c가 복사되며 복사생성자가 호출될 것을 예상했지만 아니네?
- 사실 굳이 임시 객체 1번 만들고 여기에 복사를 할 필요가 없다.
- 그냥 A(2)를 c로 만들꺼면 처음부터 c자체를 A(2)로 만들어진 객체로 해버리자
- 그래서 컴파일러는 굳이 복사 생성을 하지 않고 만들어진 A(2) 자체를 c로 만든다
-
이렇게 똑똑한 컴파일러는 복사 생성을 굳이 수행하지 않고, 임시 객체를 바로 객체를 만들어버리는 것이 복사 생략이다.
- 복사 생략을 하는 경우는 (함수의 인자가 아닌) 함수 내부에서 생성된 객체를 그대로 리턴할 때이다.
- 경우에 따라 생략을 할 수 도 있고 안할 수 도 있다
- 그럼 안 하면 우짜지… 비효율적인 복사가 많아 꼭 필요한데…
- C++17 부터 일부 경우에 대해서 무조건 복사생략을 해야하는 것으로 변경되었다.
- 여기 참고
- 그럼 안 하면 우짜지… 비효율적인 복사가 많아 꼭 필요한데…
- 참고로 복사 생성자 호출을 막고 싶다면 복사 생성자를
private
에 정의하면 된다.
좌측값(lvalue) 우측값(rvalue)
- 모든 C++ 표현식 (expression) 의 경우 두 가지 카테고리로 구분한다.
- 하나는 이 구문이 어떤 타입을 가지냐 이고, 다른 하나는 어떠한 종류의 ‘값’ 을 가지냐?.
int a = 3;
- 위의 a는 메모리 상에서 존재하는 변수,
&
연산자로 주소값을 알아낼 수 있는 이런 값은 좌측값(lvalue) 라고 부른다. - 좌측값은 표현식의 왼쪽, 오른쪽 모두 올 수 있다.
- 위 3은 주소값을 취할 수 없다. a와 다르게 표현식을 연산할 때 잠깐 존재할 뿐 사라지는 값이다.
- 실체가 없난 값, 주소값을 취할 수 없는 값을 우측값(rvalue) 이라고 한다.
- 우측값은 항상 오른쪽만 와야 한다.
l-value reference (좌측값 레퍼런스)
int a; // a 는 좌측값
int& l_a = a; // l_a 는 좌측값 레퍼런스
int& r_b = 3; // 3 은 우측값. 따라서 오류
- 이때동안 다룬 레퍼런스는 ‘좌측값’에만 레퍼런스를 생각했다.
- 위의
a
는 좌측값이여서a
의 좌측값 레퍼런스인l_a
를 만들 수 있다.
- 위의
- 반면에 3 의 경우 우측값이기 때문에, 우측값의 레퍼런스인 r_b 를 만들 수 없다.
- 오류!
- 하지만
const T&
타입의 한해서만 우측값도 레퍼런스로 받을 수 있다.- 이유는
const
레퍼런스 이기 때문에 임시로 존재하는 객체의 값을 참조만 할 뿐 변경할 수 없기 때문이다.
- 이유는
- 이와 같이
&
를 이용해서 정의하는 레퍼런스를 좌측값 레퍼런스 (lvalue reference) 라고 부르고, 좌측값 레퍼런스 자체도 좌측값이다다.
- 이렇게 좌측값 레퍼런스를 받게 된다면 주소를 가지고 놀게 되어 좌측값 레퍼런스를 받는 함수는 임시 객체를 생성하지 않는다.
r-value reference (우측값 레퍼런스)
const
를 사용해 우측값 레퍼런스, 좌측값 레퍼런스 모두 받아서 사용할 수 있지만const
이기에 변경할 수 없다는 문제가 있다.- 그러면 좌측값 말고 우측값만 특별하게 받을 수 있는 방법은 없을까?
- C++ 11에는 우측값 레퍼런스를 파라미터로 갖는 생성자가 추가
&&
- 우측값 레퍼런스는 &를 2번 사용해서 정의한다.
T && t
int&& r_b = 3; // 우측값
int&& rr_b = a; // 불가능 , 좌측값
- 우측값 레퍼런스는 좌측값이 아닌 우측값만 받을 수 있다. -> 임시 객체를 받는 함수를 정의할 수 있다
- 만약 어떤 클래스에 우측값 레퍼런스를 받는 생성자만 만들었다면 이름을 가지는, 즉 좌측값을 받는 생성자를 호출할 수 없다.
- 그렇다면 그냥
&
이나&&
를 받는 함수를 만들지 않으면 되지 않냐?- 매번 임시 객체를 생성하는 작업이 이루어져 비효율적이다.
- 그러면
&
만을 사용하면 안되나?- 실제로 좌측값 레퍼런스를 받는다면 우측값을 넘겨줘도 에러없이 실행은 된다.
- 그런데 표준 C++ 라이브러리들은 임시 객체를 굉장히 많이 생성하는데, 이런 임시 객체를 제대로 유지하고 계속 복사되는 오버헤드를 처리하기 위해
&&
가 생겨난 것이다.
move constructor(이동 생성자)
- 동적으로 데이터를 관리하는 경우 복사되는 관점에서 문제가 되기 때문에 복사 생성자를 사용했다.
- 또한 대입에서 생기는 문제점(댕글링 레퍼런스 생성) 때문에 복사 대입 연산자를 사용했다.
- 하지만 이 과정에서 임시 객체의 복사 그리고 대입되는 상황으로 인해 메모리 할당과 해제가 너무 빈번하게 일어나는 문제 때문에 이동 생성자가 생겨났다.
- 그냥 복사 없이 연산된 객체 자체를 리턴을 한다면 좋지 않을까?
- 임시 객체를 복사해서 새로운 객체를 만드는 것이 아닌 임시 객체를 추적하여 접근을 한다.
class A{
private:
// data
public:
A(); // 디폴드 생성자
A(const A &a); // 복사 생성자
A(A &&a) // 이동 생성자
{
// 어떠한 작업 수행
// 데이터를 그냥 대입
// a(기존 임시 객체)는 nullptr로 초기화화
}
}
- 이동 생성자의
a
는 좌측값? 우측값?- 좌측값이다.
a
는A
의 우측값 레퍼런스인 좌측값 이라고 보면 된다.
- 우측값 레퍼런스는 사라지는 값인 우측값을 계속 갖고 있는 것 처럼 보이는데
- 우선 임시 객체에서 데이터를 가져와서 새로 생성될 객체에 주소를 전달하고 임시객체의 데이터는
nullptr
를 할당해서- 임시 객체의 데이터는 사라않고 새로 생성된 객체에 이동이 되어서 접근이 계속가능하고 임시객체는
nullptr
이기에 접근을 못하게 한다.
- 임시 객체의 데이터는 사라않고 새로 생성된 객체에 이동이 되어서 접근이 계속가능하고 임시객체는
이동 대입 연산자
- 기존의 대입 연사자 처럼 우측값 레퍼런스를 기준으로 한 대입 연산자도 만들어준다
- 기존 대입 연산자와 차이점은 메모리 할당 없이 그대로 데이터를 넣어주고, 기존 객체는 초기화를 해준다.
문제점이 전부 해결된 것 같지만 그렇지 않다 해당 이야기는 perfect forwarding에서 이야기한다.
이동 생성자 주의 점
- 이동 생성자 작성시 해당 클래스를 C++ 컨테이너에 적용할 때는 반드시 이동 생성자를
noexcept
로 명시 필요.
- vector로 예시를 보자
- 새로운 원소 추가시 메모리가 부족하면 새로운 메모리를 할당한 후에. 기존 원소를 새로운 메모리로 옮긴다.
- 복사 생성자의 경우 원소가 하나씩 복사되는데, 이 과정에서 예외가 발생한다면, 새로 할당한 메모리를 소멸시킨후 사용자에게 예외를 전달하게 될 것이다.
- 히지만 이동 생성자는 상황이 다르다
- 복사 생성은 복사를 하고 새로운 공간에 할당해서 그 공간을 소멸시켜도 기존 원소는 존재하지만
- 이동 생성의 경우 기존 메모리에서 원소들이 모두 이동되어 사라지기에 섯불리 새 메모리를 해제를 하면 원소 자체가 사라진다.
- 따라서 이동 생성자에서 예외 발생시 처리를 못한다.
- 그렇기에 이동 생성자는 noexcept가 아닌 이상 사용이 불가하다.
A(A &&a) noexcept;
개인 공부 기록용 블로그입니다.
틀린 부분 있으다면 지적해주시면 감사하겠습니다!!
댓글남기기