이번글은 오른값 참조와 전달(보편)참조에 대한 글입니다.
(전달 참조는 보편참조로도 불립니다)
오른값의 개념이 Mordern C++ C++11부터 생겼으며 해당개념의 등장으로 C++속도의 엄청난 치아를 가져다 주었습니다.
먼저 오른값이 무엇인지 알아보기 전에 "왼값"이 무엇인지 개념부터 살펴보도록 하겠습니다.
왼값(lvalue, l-value)은 단일식을 넘어서 계속 유지되는 개체이며,
오른값(rvalue, r-value)는 "전체 개체에서 - 왼값" 인 개체입니다. 즉 임시객체나, 상수 등이 포함됩니다.
기존의 참조는 '&'하나만 사용하고 오른값 참조의 경우 '&&'두개를 사용합니다.
template <typename T>
void fucn(T&& param)
{
...
}
"param의 형식은 오른값이지만 param자체는 왼값이다"
저는 위의 문장을 보고 처음에는 무슨말인지 도통 이해가 안 갔었습니다.
위의 문장이 이해가 간다면 오른값이 무엇인지는 이해했다고 봐도 무방할거 같습니다ㅎㅎ
오른값 참조 사용하는 이유
일단 먼저,
int a = 10; // a는 왼값
float f1 = 20.f; // f는 왼값
vector<int> v;
v.push_back(10); // 10은 오른값
간단한 왼값 오른값 예를 보면 위와 같이 볼 수 있습니다.
그러면 오른값의 개념이 성능에 좋은지 코드로 예를 들어보면
class Dog
{
public:
std::string name = "개";
};
class Person
{
public:
Person()
{
cout << "Person()" << endl;
}
Person(const Person& p)
{
cout << "Person(const Person* p)" << endl;
}
public:
Dog* myDog = nullptr;
int age = 20;
};
int main()
{
ios_base::sync_with_stdio(NULL);
cin.tie(NULL);
cout.tie(NULL);
Dog* p1Dog = new Dog();
Person p1;
p1.myDog = p1Dog;
return 0;
}
위처럼 Person클래스가 Dog라는 클래스를 멤버 포인터 변수로 물고 있다고 가정을 합시다.
그리고 main함수에서 아래와 같이 코드를 작성하는 경우
int main()
{
ios_base::sync_with_stdio(NULL);
cin.tie(NULL);
cout.tie(NULL);
Dog* p1Dog = new Dog();
Person p1;
p1.myDog = p1Dog;
Person p2;
p2 = p1;
return 0;
}
"문제"가 발생합니다. 바로 얕은 복사의 문제점입니다. Dog라는 클래스가 Person클래스 하나당 한마리 밖에 못가진다고 가정할 경우 p1과 p2는 같은 반려동물을 가지는 셈이 됩니다.
위처럼 하나밖에 존재하지 못하는 반려동물을 같이 가지고 있는 셈이됩니다.
따라서 위의 문제를 복사 대입 연산자를 깊은 복사 형태로 바꾸어 이를 해결할 수 있습니다.
void operator = (const Person& p)
{
cout << "operator(const Person& p)" << endl;
age = p.age;
if (p.myDog != nullptr)
{
myDog = new Dog(*p.myDog);
}
}
위처럼 Person클래스 멤버 함수로(복사 대입 연산자)를 정의 해주어 기존의 p1의 Dog의 복사생성자를 통해서 같은 "개"라는 이름으로 서로다른 Dog를 만들어 줄 수 있습니다.
(같은 코드이지만 이제는 서로 다른 Dog를 가지는 것을 볼 수 있습니다.)
하지만 여기에서 또 다른 문제가 하나 더 있습니다. 만약 Dog라는 클래스가 엄청나게 크다면 어떻게 해야할까요? 그리고 해당 operator = 복사 대입연산자가 자주 발생한다면 어떻게 해야할까요?
여기서 발생하는 비용을 무시할 수 는 없습니다.
그래서 오른값 참조가 나오게 된 것입니다.
오른값 참조는 약간 기존의 왼값이 필요없다고 간주를 해버리는 느낌으로 저는 이해를 했습니다.
그래서 반려동물도 이세상에 한마리 밖에 존재 하지 않기때문에 복사 대인 연산자를 진행한다는 말을 이해하기 쉽게 p2라는 사람이 파양한 반려동물을 입양한다고 가정을 하겠습니다.
여기서 "이동"의 개념이 나오는데 기존에 객체를 말그대로 이동시킨다라고 일단 이해를 해주시면됩니다.
즉, "왼값으로 전달된(== 인수)녀석은 이제 필요 없으니 너가 마음대로 사용해라~~" 느낌입니다.
void func(Widget w) {} // w는 매개변수이며
func(widget); // widget은 인수입니다.
이동에는 복사생성자, 복사 대입 연산자와 마친가지로 이동생성자, 이동 대입 연산자가 존재합니다.
void operator = (Person&& p) noexcept
{
cout << "operator(const Person&& p)" << endl;
age = p.age;
if (p.myDog != nullptr)
{
myDog = p.myDog;
p.myDog = nullptr;
}
};
이동 대입 연산자는 위처럼 간단하게 만들 수 있습니다. 기존의 Dog를 얕은복사 방식으로 복사를 한뒤 직접 기존의 p의 Dog는 nullptr로 밀어버린 부분입니다.
이러한 얕은 복사 방식의 이점 덕분에 깊은 복사를 진행할경우 Dog의 멤버에 사용자 정의 타입이 엄청많을 경우 하나하나 다 깊은복사를 진행할텐데 그러한 부분들 없이 그냥 바로 얕은 복사를 진행하니 성능에 이점이 있을 수 밖에 없습니다.
그래서 오른값 참조를 사용하는 것입니다.
왼값 오른값 구분 방법
template <typename T>
void TestFunc(T&& p)
{
}
int main()
{
ios_base::sync_with_stdio(NULL);
cin.tie(NULL);
cout.tie(NULL);
Person p1; // p1은 왼값
Person& p2 = p1;
TestFunc(p2);
TestFunc(p1);
TestFunc(std::move(p1));
return 0;
}
위의 코드가 있을 경우 TestFunc를 호출하게 되면 p2는 왼값이므로 T의 타입이 Persin&이고
p1도 왼값이라 마찬가지 입니다.
위의 std::move == static_cast<Person&&>의 경우에는 오른값 참조로 전달되는 모습을 볼 수 있습니다.
그러면 TestFunc내부에서 오른값 참조와 왼값 참조를 구분할 수 있어야하는데 이 경우는 어떻게 해야할까요?
해당 경우에는 "전달 참조"의 개념이 들어갑니다.
전달 참조의 경우 다른 글에서 정리 하도록 하겠습니다.
감사합니다 :)
'CPP' 카테고리의 다른 글
[C++] stream buffer와 표준 입출력 (0) | 2023.08.02 |
---|---|
[C++] 전달참조 (0) | 2023.07.31 |
[C++] Effective C++ 항목 1~27 정리 (0) | 2023.07.03 |
[C++] C++ 컴파일 의존성 (2) | 2023.05.28 |
[C++] vftable (0) | 2023.04.21 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!