C++ 이것저것 3
lValue 와 rValue
‘값’에 대한 ‘분류’이며
일종의 ‘표현식’에 가깝다
lValue
- 메모리에 주소를 가질 수 있는 값, ‘참조’ 가능한 값
(&를 통해 ‘참조’ 가능)
(ex : 변수, 배열의 원소 등)
int a = 10; // a는 lvalue
int* p = &a; // a는 메모리에 있으므로 주소를 얻을 수 있음
rValue
- 임시 값이나 이름 없는 값 (수정하거나 참조 불가한 것들)
(ex : 리터럴, 연산 결과 등)
int x = 1 + 2; // 1+2는 rvalue
x = x + 3; // x는 lvalue, x+3은 rvalue
int* p = &(5 + 2); // ❌ 컴파일 에러 — rvalue는 주소가 없음
- rvalue는 크게 2가지로 나뉜다
- prValue(Pure Value) : 값 그 자체(42,”Hello”,SomeClaee())
- xValue(Expiring Value) : 소유권을 잃을 예정인 값 (std::move(),std::string().substr(0,2))
(xValue 자체는 ‘컴파일러’가 판단)
- prValue(Pure Value) : 값 그 자체(42,”Hello”,SomeClaee())
rValue Reference
개념
기존의 ‘참조’(T&)는 lVaue만 참조가 가능했음
int a = 10;
int& ref1 = a; // OK
int& ref2 = 5; // ❌ rvalue를 참조할 수 없음
C++ 11 부터 rValue Reference의 도입으로
rValue에 대한 참조가 가능해짐 (T&&)
int&& rref = 5; // OK: 5는 rvalue
포인터 처럼 ‘일종의 타입’으로 인식
(ex : int&&, string&&)
사용처
-
Move : 자원을 ‘복사’하는 것이 아닌 ‘이전’
-
Perfect Forwarding : 템플릿 함수에서 ‘전달받은 인자’의 값 속성(lValue/rValue)을 다른 함수에 전달 가능
필요한 이유??
- 기존 참조가 rValue를 받을 수 없어서 ‘임시 객체’ 등에 대한 참조가 불가능
- 임시 객체를 더 효율적으로 다루기 위함
(큰 크기의 임시 변수를 생성하였을 때, 그에 대한 ‘복사’ 대신
소유권을 이전함으로서 최적화)
이동(Move)
불필요한 복사를 피하고, 리소스를 ‘이동’시켜
성능 향상을 하기 위한 기능
std::Move()
std::string a = "text";
std::string b = std::move(a); // 이동 생성자 호출
std::move() 자체는 값을
rvalue로 캐스팅해준다(정확히는 xvalue)
이 때, string의 이동 생성자 / 이동 대입 연산자의 T&&가 선택되어
실제로 값을 옮기는 역할을 수행
(다만 보통 이 과정에서 원본의 데이터를 유효하지 않게 만듦)
(move 이후, 원본 값은 사용하지 않는걸 권장)
- 이 때, a와 b는 ‘같은 클래스 타입’이여야 한다
(정확히 말하자면 ‘업캐스팅’은 가능하지만 ‘다운 캐스팅’은 에러가 발생)
(애초에 ‘다운 캐스팅’을 의도하였다면 그냥 캐스팅을 한 이후에 사용하자)
B b;
A a = std::move(b); // OK → **Object Slicing 발생**
→ A의 멤버만 남고, B 고유 멤버는 잘림
void takeA(A&&) { std::cout << "A&&\n"; }
void takeB(B&&) { std::cout << "B&&\n"; }
A a;
B b;
takeA(std::move(a)); // A&&
takeA(std::move(b)); // A&& (업캐스팅)
takeB(std::move(b)); // B&&
takeB(std::move(a)); // ❌ 컴파일 에러
- TMI : string이 저장되는 위치는 어디?
- 리터럴인 “hello”라면 ‘읽기 전용 데이터 영역’에 그 값이 위치된다
- string(“Hello”) 를 통해 문자열 선언한 경우, 그 값을 복사하여 heap에 메모리 할당 된다
- 만약 위에서 a를 지역변수로 사용했다면 a의 메모리 자체는 스택에 위치
- 리터럴인 “hello”라면 ‘읽기 전용 데이터 영역’에 그 값이 위치된다
이동 생성자/ 이동 대입 연산자
항목 | 설명 |
---|---|
이동 생성자 | 새 객체를 만들 때, 기존 객체의 자원을 훔쳐오기 위한 생성자 |
이동 대입 연산자 | 기존 객체에 다른 객체의 자원을 옮겨 심는 연산자 |
두 방식 모두 T&&(rvalue reference)를 매개 변수로 받음
(임시 객체 or std::move()를 통해 호출)
#include <iostream>
#include <string>
class MyClass {
public:
std::string data;
// 생성자
MyClass(const std::string& s) : data(s) {
std::cout << "Constructor\n";
}
// 복사 생성자
MyClass(const MyClass& other) : data(other.data) {
std::cout << "Copy Constructor\n";
}
// 이동 생성자
MyClass(MyClass&& other) noexcept : data(std::move(other.data)) {
std::cout << "Move Constructor\n";
}
// 복사 대입 연산자
MyClass& operator=(const MyClass& other) {
std::cout << "Copy Assignment\n";
data = other.data;
return *this;
}
// 이동 대입 연산자
MyClass& operator=(MyClass&& other) noexcept {
std::cout << "Move Assignment\n";
data = std::move(other.data);
return *this;
}
};
당연하지만 class 내부에서 heap과 연관된 것이 있다면
내부에서도 move를 통해, 그 포인터 주소를 넘겨받아야 한다
추가로 ‘상속’과 연관된 경우
class Base {
public:
std::string base_str;
Base(Base&& other) noexcept : base_str(std::move(other.base_str)) {
std::cout << "Base Move\n";
}
};
class Derived : public Base {
public:
std::string derived_str;
Derived(Derived&& other) noexcept
: Base(std::move(other)), // 명시적으로 부모 이동 생성자 호출
derived_str(std::move(other.derived_str)) {
std::cout << "Derived Move\n";
}
};
부모 클래스의 이동 생성자/ 대입 연산자를 ‘자식’에서 호출해 주어야 한다
Perfect Forwarding
템플릿 함수가 전달받은 인자의 “값 특성”을 그대로 유지해
다른 함수로 전달하는 방식
필요한 이유?
void process(std::string& s) { std::cout << "Lvalue\n"; }
void process(std::string&& s) { std::cout << "Rvalue\n"; }
template <typename T>
void wrapper(T arg) {
process(arg); // 항상 lvalue로 처리됨!
}
std::string s = "hello";
wrapper(s); // 기대: Lvalue → 결과: Lvalue ✅
wrapper(std::move(s)); // 기대: Rvalue → 결과: ❌ Lvalue
따라서
template <typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg)); // ⭕ Forwarding
}
wrapper 함수를 std::forward
값의 속성을 보존한 상태로 전달 가능
std::forward
주어진 타입에 따라 일관되게 캐스팅을 해주는 함수
lvalue는 lvalue로, rvalue 는 rvalue(xvalue)로
그대로 캐스팅해주며
보통 다른 함수에게 넘기기 위한 용도로 사용
- 템플릿 래퍼 함수
- STL 함수 등에서 인자 최적화를 위해
- emplace_back() 같이 내부에서 값을 생성하는 경우,
각자의 생성자 와 이동 생성자를 분류할 수 있도록
댓글남기기