[C++] lvalue와 rvalue
복사 생략 Copy Elision
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;
}
};
int main() {
A a(1); // 일반 생성자 호출
A b(a); // 복사 생성자 호출
// 일반 생성자 호출!
// 복사 생성자 호출!
// 일반 생성자 호출!
A c(A(2));
}
- 마지막 코드에서 “복사 생성자 호출!” 이 한 번 더 호출되어야 했을 것 같은데, 불리지 않았다
- 컴파일러에서 불필요한 복사 생성자 호출을 생략하는 작업을 복사 생략이라 한다
Note
C++ 17 부터 일부 경우에 대해서 (예를 들어서 함수 내부에서 객체를 만들어서 return 할 경우) 반드시 복사 생략을 해야되는 것으로 바뀌었다고 한다.
https://en.cppreference.com/w/cpp/language/copy_elision
Value Category
- 정체를 알 수 있는가
- 해당 식이 다른 식과 같은 것인지 아닌지 구분할 수 있는가?
- 변수 - 주소값을 통해 구분
- 함수 - 이름으로 구분
- 이동 시킬 수 있는가
- 해당 식을 안전하게 다른 곳으로 이동할 수 있는가?
- 이동 생성자, 이동 대입 연산자를 사용할 수 있는지의 여부
이동 가능 이동 불가능 정체를 알 수 있다 xvalue lvalue 정체를 알 수 없다 prvalue 쓸모없음
좌측값과 우측값의 정의
- C++에서는 아래와 같이 정의한다고 한다
좌측값은 어떠한 메모리 위치를 가리킨다. & 연산자로 그 주소를 참조할 수 있다
우측값은 좌측값이 아닌 값들이다
int a = 3; // a는 좌측값, 3은 우측값
int& l_a = a; // l_a 는 좌측값 레퍼런스, 좌측값 레퍼런스도 좌측
int& r_b = 3; // 3 은 우측값. 따라서 오류
Note
const T& 타입에 한해서, 우측값도 레퍼런스로 받을 수 있다
어차피 const 이기 때문에, 임시 객체의 값을 참조만 할 뿐 변경이 불가능하기 때문
move sementic
- 임시 객체를 생성한 뒤, 복사 생성자를 호출하는 케이스가 있다
X foo(); // foo 는 X 타입의 객체를 리턴하는 함수 이다!
X x;
x = foo();
/*
* 1. x 가 가지고 있는 리소스가 소멸된다.
* 2. (foo 가 리턴한) 임시 객체의 리소스의 복제된 버전이 생성된다.
* 3. x 가 복제된 리소스를 가리키고, 임시로 생성된 객체의 리소스는 소멸된다.
*/
- 그러나 위의 경우에서, 굳이 임시 객체의 리소스를 또 복제할 필요가 없다
- 최종 할당 되어야 할 객체의 리소스가 임시 객체의 리소스를 가리키도록 연결하고, 기존 리소스는 임시 객체의 그것이 가리키도록 하면 된다
- 일종의 swap
X& X::operator=(<미지의 타입> rhs)
{
// [...]
// m_pResource 와 rhs.m_pResource 를 교환
// [...]
}
- 이는 복사 대입 연산자의 overload를 구현하는 것이다
- 이 때 인자의 타입은 당연히 교환을 위해 레퍼런스(&)로 받아야 한다
- 동시에, 인자로 들어온 게 좌측값인 경우 일반 레퍼런스, 우측값인 경우 저 “미지의 타입”을 사용해야 한다
- 그리고 이를 위해 고안된 것이 우측값 참조이다
우측값 참조
void foo(X& x); // 좌측값 참조 오버로드
void foo(X&& x); // 우측값 참조 오버로드
X x;
X foobar();
foo(x); // 인자에 좌측값이 들어 갔으므로 좌측값 참조 함수가 오버로딩
foo(foobar()); // 인자에 우측값이 들어 갔으므로 우측값 참조 함수가 오버로딩
- 위의 코드에서 좌측값 인자는 좌측값 함수를 오버로드하고, 우측값 인자는 우측값 레퍼런스를 사용한다
- 즉, 컴파일 시 인자로 좌측값이 오는지 우측값이 오는지 판단하고 오버로드 한다
Note
우측값 레퍼런스라 정의한 것들도 좌측갑 또는 우측값이 될 수 있다
이를 판단하는 기준은, 이름이 있다면 좌측값, 없다면 우측값이다
lvalue (Left Value)
- 좌측값은 어떠한 표현식의 왼쪽, 오른쪽 모두에 올 수 있음
- 이동(move)시킬 수 없음
- 이름을 가진 대부분의 객체들은 모두 lvalue
- 객체의 주소값을 취할 수 있기 때문
- 왼쪽에만 올수 있고 그런 거 아님
- 생명 주기가 표현식의 범위를 넘어선다. 즉, 표현식이 끝나도 객체는 계속 존재
int a;
a;
int&& x = a; // error
- 주소값 연산자(&) 를 통해 해당 식의 주소값을 알아 낼 수 있다
- &++i 나 &std::endl 은 모두 올바른 작업
- 또한 lvalue 들은 좌측값 레퍼런스를 초기화 하는데에 사용할 수 있다
- 주의할 점
void f(int&& a) {
a; // lvalue다
}
f(3);
// 식 a의 타입은 우측값 레퍼런스
// 식 a의 값 카테고리는 lvalue -> 이름이 있기 때문
// 따라서, 아래의 식은 컴파일이 가능함
// main.c
#include <iostream>
void f(int&& a) { std::cout << &a; } // a의 주소값 추적 가능
int main() { f(3); }
- lvalue의 범주
- 변수, 함수의 이름, 어떤 타입의 데이터 멤버
// ex) int i; void func1(); std::endl;
- 좌측값 레퍼런스를 리턴하는 함수의 호출식
// ex) std::cout << i; ++it;
- 복합 대입 연산자식
// ex) a = b; a+=b; a*=b;
- 전위 증감 연산자식
// ex) ++a; --a;
- 클래스의 멤버 메서드/변수를 참조할 때 (단, static 메서드도, enum도 아닌 경우)
// ex) class A { int f(); // static 이 아닌 멤버 함수 static int g(); // static 인 멤버 함수 } A a; a.g; // <-- lvalue a.f; // <-- lvalue 아님 (아래 나올 prvalue)
- 배열 참조식
// ex) int a[n];
- 문자 리터럴
// ex) "hi"
- 문자 리터럴의 케이스는 조금 특별하다
- 문자열은 character의 const static 배열이며, 프로그램의 메모리 상에 존재한다
- 분류상 glvalue에 속한다
- xvalues는 될 수 없기에, lvalues로 분류된다
- 변수, 함수의 이름, 어떤 타입의 데이터 멤버
prvalue (Pure Value)
- 이동시킬 수 있음
- 주소값을 취할 수 없음
- 우측값은 식의 오른쪽에만, read-only (식의 좌측에 올 수 없음)
- Take the variables themselves, not copying them
- 생명 주기가 짧다. 보통 표현식이 완료되는 즉시 소멸
- prvalue의 범주
- 문자열을 제외한 리터럴들
// ex) 42, true, nullptr;
- 레퍼런스가 아닌 것을 리턴하는 함수의 호출식
// ex) str.substr(1,2); str1 + str2;
- 후위 증감 연산자식
// ex) a++, b--;
- 산술 연산자식, 논리 연산자식
// ex) // 오버로딩된 연산자 아닌 디폴트 연산자 a + b, a && b, a < b;
- 주소값 연산자식
// ex) &a;
- 클래스의 멤버 메서드/변수를 참조할 때 (단, m이 enum이거나 static 메서드인 경우)
- this
- enum 값
- 익명함수 (람다식)
// ex) []() { return 0;};
- 문자열을 제외한 리터럴들
xvalue (eXpiring Value)
- 좌측값처럼 주소값을 트래킹할 수 있지만, 또한 이동도 할 수 있는 카테고리
- lvalue와 prvalue의 중간 형태
- std::move()를 이용해서 우측값 레퍼런스를 리턴하는 호출식
std::move(x); vector<int> a, b = {1,2,3,4,5}; a = b; // a: 1,2,3,4,5 b:1,2,3,4,5 a = std::move(b); // b-> rvalue, a:1,2,3,4,5 b:{}
- 생명 주기가 표현식의 범위를 넘어설 수도 있지만, 그 자원은 더 이상 유효하지 않을 수 있다