* 서론
- 고전적으로 l-value, r-value는 대입문 왼편, 오른편에 올 수 있는 값들을 의미했습니다.
- 최근에는 l-value는 참조가 가능한 객체라고 정의합니다.
즉,
l-value : &연산자를 붙일 수 있는 주소 값을 취할 수 있는 값
r-value : 주소값을 취할 수 없는 (임시) 값
으로 정의할 수 있겠습니다.
실제로, 컴파일러는 r-value의 연산자를 허용하지 않습니다.
* l-value 참조자
int& func(int& a) { return a; }
int main()
{
int a = 3;
int b = 2;
func(a) = 4;
printf("%d\n", a); //4
printf("%d\n", func(a)); //4
printf("%d\n", func(b)); //2
}
- func함수는 a의 참조를 매개변수로 받아 return 값을 참조로 돌려주었습니다.
=> func(a) = a(l-value)의 참조입니다.
만약 func 함수에 참조자가 붙지 않는 경우,
=> int func(int &a) {return a;}
func()는 int r-value를 반환하는 함수가 되어 값을 대입할 수 없습니다.
* r-value 참조자
- r-value는 임시 객체로, 참조할 주소가 없다고 했습니다.
그런데도 참조를 할 수가 있다면?
우선, 참조자 매개변수에 r-value인 4를 넣어보면 비 const 참조에 대한 초기 값은 l-value여야 한다고 설명합니다.
그렇다면, const 참조라면 r-value를 참조할 수 있는 걸까요?
int func(int const& a) { return a; }
int main()
{
int a = 3;
int b = 2;
b = func(4);
}
- 예외적으로 가능합니다.
다만, const가 붙어있기 때문에 해당 값을 변경할 수 없이 상수로 사용해야 합니다.
const &로 매개변수를 받는 것은, l-value와 r-value를 모두 받을 수 있어 매우 유용합니다.
또한, const& 매개변수는 값을 복사 전달하지 않기 때문에 함수의 매개변수 전달 시간을 상당히 줄여줍니다.
- r-value의 참조
놀랍게도, &&을 사용하면 r-value에 대한 참조가 가능합니다.
r_value의 참조자는 해당 r_value가 소멸하지 않도록 붙들고 있습니다.
int func(int&& a) { return a; }
int main()
{
int var = 4;
int &&r_value = 4;
func(r_value); //에러 : r-value를 참조하는 l-value이기 때문
}
하지만, int &&형이 r-value인 것은 아닙니다.
어디까지나, r-value에 대한 참조로 사용됩니다.
#include <iostream>
int func(int const& a) { printf("const value\n"); return a;}
int func(int& a) { printf("l-value\n"); return a; }
int func(int&& a) { printf("r-value\n"); return a; }
int main()
{
const int a = 4;
int var = 4;
int &&r_value = 4;
func(a); //const value
func(var); //l-value
func(r_value); //l-value
func(4); //r-value
}
함수를 오버로딩 하여, 여러 l-value와 r-value의 참조를 받도록 했습니다.
이제, 우리는 r-value에 대한 참조 매개변수를 받을 수 있게 되었습니다.
* 여기까지 요약
l-value의 참조자 : &
r-value의 참조자 : &&
* 값 복사
1. l-value 복사의 문제점
#include <iostream>
#include <utility>
class A
{
public:
int value;
public:
A() { printf("일반 생성자\n"); value = 0;}
A(int const& InValue) {printf("일반 생성자\n"); value = InValue;}
~A() { printf("소멸자\n");}
A(A const &a) { printf("복사 생성자\n"); value = a.value; }
A(A&& a) { printf("이동 생성자\n"); value = a.value; }
bool operator=(const A& Other)
{
printf("값 복사 호출\n");
value = Other.value;
}
public:
static void Swap(A &a, A &b)
{
printf("Swap 호출\n");
A temp = a;
a = b;
b = temp;
}
};
int main()
{
A a(4);
A b(5);
A::Swap(a, b);
printf("a : %d b : %d\n", a.value, b.value);
}
Swap 호출 후 복사만 3번 일어나고 있습니다.
지금 클래스 구조는 int 하나니까 괜찮지만, 만약 거대한 크기의 클래스였다면?
상당한 퍼포먼스 저하를 일으킬 것입니다.
* Utiltiy - std::move()
- C++ STL 내부에는 move()라는 함수가 있습니다.
- 이 함수는 l-value를 r-value로 (정확히는 x-value)로 변경해주는 함수입니다.
#include <iostream>
#include <utility>
class A
{
public:
int value;
public:
A() { printf("일반 생성자\n"); value = 0;}
A(int const& InValue) {printf("일반 생성자\n"); value = InValue;}
~A() { printf("소멸자\n");}
A(A const &a) { printf("복사 생성자\n"); value = a.value; }
A(A&& a) { printf("이동 생성자\n"); value = a.value; }
bool operator=(const A& Other)
{
printf("값 복사 호출\n");
value = Other.value;
}
bool operator=(A&& Other)
{
printf("값 이동 호출\n");
value = Other.value;
}
public:
static void Swap(A &a, A &b)
{
printf("Swap 호출\n");
A temp = std::move(a);
a = std::move(b);
b = std::move(temp);
}
};
int main()
{
A a(4);
A b(5);
A::Swap(a, b);
printf("a : %d b : %d\n", a.value, b.value);
}
실제로 Swap 내부를 move() 함수를 통해 변경해보면
결과가 변합니다.
* 보편 참조 / 퍼펙트 포워딩
- 우리는 템플릿을 사용하여 범용적으로 함수를 사용하고 싶습니다.
템플릿 함수에 좌측값, 우측값이 다 들어올 수 있으니 다음과 같이 오버로딩 합니다.
#include <iostream>
template<typename T>
void wrapper(T& u)
{
printf("%d 좌측값 호출\n", u);
}
template<typename T>
void wrapper(T const& u)
{
printf("%d 우측 상수값 호출\n", u);
}
int main()
{
int x = 87;
wrapper(4);
wrapper(x);
return 0;
}
실제로 잘 됩니다. 다만, 매개변수가 여러개면..
template<typename T>
void wrapper(T& u, T &v) {}
template<typename T>
void wrapper(T & u, T const& v) {}
template<typename T>
void wrapper(T const& u, T& v) {}
template<typename T>
void wrapper(T const& u, T const& v) {}
이런식으로 매개변수 개수에 따라 모든 조합을 모두 정의하는건 너무 힘들것 같습니다.
그래서 템플릿의 경우에는 보편 참조를 지원합니다.
template<typename T>
void wrapper(T&& u)
{
printf("%d 우측값? 호출\n", u);
}
int main()
{
int x = 87;
wrapper(4); //4 우측값? 호출
wrapper(x); //87 우측값? 호출
return 0;
}
&&를 붙였을 뿐인데, l-value도 r-value도 받아들이는 함수가 되었습니다.
그렇다면 이 함수는 매개변수가 l-value인지 r-value인지도 구분하는 걸까요?
void func(int &value) { printf("%d 좌측값 레퍼런스\n", value); }
void func(int const&value) { printf("%d 좌측 상수값 레퍼런스\n", value); }
void func(int &&value) { printf("%d 우측값 레퍼런스\n", value); }
template<typename T>
void wrapper(T&& u)
{
func(u);
}
int main()
{
int x = 87;
const int y = 33;
wrapper(4); //4 좌측값 레퍼런스
wrapper(y); //33 좌측 상수값 레퍼런스
wrapper(x); //87 좌측값 레퍼런스
return 0;
}
그건 아닙니다.
우측값 레퍼런스를 인식하지 못하는군요.
이런 문제를 해결하기 위해, 우리는
std::forward<T>()를 사용할 수 있습니다.
void func(int &value) { printf("%d 좌측값 레퍼런스\n", value); }
void func(int const&value) { printf("%d 좌측 상수값 레퍼런스\n", value); }
void func(int &&value) { printf("%d 우측값 레퍼런스\n", value); }
template<typename T>
void wrapper(T&& u)
{
func(std::forward<T>(u));
}
int main()
{
int x = 87;
const int y = 33;
wrapper(4);
wrapper(y);
wrapper(x);
return 0;
}
마침내 원하는 결과를 얻어냈군요.
forward함수는 매개변수가 우측값일 때에만 move 함수처럼 동작합니다.
결론
우리는 template처럼 매개변수의 타입을 알 수 없어도
l-value인지 r-value인지 const l-value인지 몰라도 포워딩이 가능하게 되었습니다.
이를 퍼펙트 포워딩이라 할 수 있습니다.
'공부 및 정리 > C++' 카테고리의 다른 글
C++의 기초 - 7 (0) | 2022.12.20 |
---|---|
C++의 기초 - 6 (0) | 2022.12.12 |
C++의 기초 - 5 (0) | 2022.12.12 |
C++의 기초 - 4 (0) | 2022.11.30 |
C++의 기초 - 3 (0) | 2022.07.12 |