«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Archives
Today
Total
04-27 06:39
관리 메뉴

lancelot.com

rvalue reference 본문

프로그래밍

rvalue reference

lancelot50 2022. 8. 21. 19:45
  • lvalue vs rvalue
    • "표현식( expression )" 이 등호의 왼쪽에 놓일 수 있으면 lvlaue, 놓일 수 없으면 rvalue
    • 각 언어마다 "정의가 약간씩 다르다."
  • C++ 에서의 특징
lvalue rvalue
등호( = ) 의 왼쪽에 올 수 있다 등호( = )의 왼쪽에 올 수 없다
이름이 있고, 단일식을 벗어나서 사용가능 이름이 없고, 단일 식에서만 사용
주소 연산자로 주소를 구할 수 있다 주소 연산자로 주소를 구할 수 없다
참조를 반환하는 함수
문자열 literal
값을 반환하는 함수
실수/정수 literal
임시객체( temporary )
  • 상수는 rvalue 이다? -> No
  • 모든 rvalue는 상수이다? -> No
#include<iostream>

int x = 10;
int f1() { return x; }	// "10"을 반환
int& f2() { return x; }	// x의 별명을 반환

int main()
{
	int v1 = 0, v2 = 0;

	v1 = 10;	// v1 : lvalue
//	10 = v1;	// error 10 : rvalue
	v2 = v1;

	int* p1 = &v1;	// ok
//	int* p2 = &10;	// error

//	f1() = 20;	// 10=20 error
	f2() = 20;	// x=20 ok

	const int c = 10;
//	c = 20;		// error
				// immutable lvalue
//	10 = 20;	// error. 10은 lvalue가 아니다.
//	"aa"[0] = 'x';	// error. lvalue 문제가 아니라 const char[3] 이므로
	std::cout << "aa"[1] << std::endl;
    
    
	int n = 3;
	
	n = 10;
//	n + 2 = 10;		error
//	n + 2 * 3 = 10;	error
	(n = 20) = 10;	// n은 reference
	++n = 10;		// n은 reference
//	n++ = 10;		n은 값    
}

 

  • lvalue, rvalue
    • 객체, 변수에 부여되는 속성이 아닌, "표현식( expression )" 에 부여되는 속성.
  • 표현식( expression )
    • "하나의 값"을 만들어내는 코드 집합
    • "a sequence of operators and operands that specifies a computation"

value category

  • 표현식이 lvalue 인지 rvalue 인지 조사하는 방법
    • "decltype( expression ) 의 결과로 나오는 타입을 확인" 하면된다
  • decltype( expression ) 의 추론규칙
    • "expression이 등호의 왼쪽에 올 수 있다면 lvalue reference" 타입
    • 단, expression 에 "이름"만 있다면 선언을 보고 결정. - "이름"을 ( ) 로 묶으면 된다.
#include<iostream>
#include<type_traits>

#define value_category(...)							\
		if(std::is_lvalue_reference_v<decltype((__VA_ARGS__))> )	\
			std::cout<<"lvalue"<<std::endl;				\
		else if(std::is_rvalue_reference_v<decltype((__VA_ARGS__))> )	\
			std::cout<<"rvalue(xvalue)"<<std::endl;			\
		else								\
			std::cout << "rvalue(pvalue)" << std::endl;
		
int main()
{
	int n = 10;

	value_category(n);
	value_category(n+2);
	value_category(++n);
	value_category(n++);
	value_category(10);
	value_category("AA");
}

 

  • reference 의 종류
    • lvalue reference ( int& )
    • rvalue reference ( int&& )
  • 규칙 1.
    • non-const lvalue reference 는 lavalue만 가리킬 수 있다
  • 규칙 2.
    • const lvalue reference 는 lvalue와 rvalue를 모두 가리킬 수 있다.
  •  규칙 3. C++11부터
    • rvalue reference 는 rvalue만 가리킬 수 있다.
  • 왜 상수성 없이 rvalue를 가리키는 것이 중요한가?
    • move sementic 와 perfect forwarding을 위해서
int main()
{
	int n = 3;

	int& r1 = n;
//	int& r2 = 3;	//error

	const int& r3 = n;	// ok
	const int& r4 = 3;	// ok

//	int&& r5 = n;	// error
	int&& r6 = 3;	// ok
}

 

  • reference 와 overloading
    • overloading 규칙
      • 값 타입과 참조 타입은 오버로딩 될 수 없다.
      • 참조 타입끼리는 오버로딩 될 수 없다.
    • reference 인자의 의도
foo( X& x ) out parameter
객체를 수정하겠다는 의미
lvalue만 받을 수 있다.
foo( const X& x ) in parameter
객체를 읽기만 하겠다는 의미
lvalue 과 rvalue를 모두 받을 수 있다.
foo( X&& x ) move sementic 을 사용하겠다는 의도
rvalue 만 받을 수 있다.
foo( const X&& x ) rvalue 만 받을 수 있다.
문법적으로 만들 수 있지만, 의미가 없다.
현재 C++에서는 사용되지 않음.
#include<iostream>
class X {};
//void foo(X x) { std::cout << "X" << std::endl; }
void foo(X& x) { std::cout << "X&" << std::endl; }		// 1
void foo(const X& x) { std::cout << "const X&" << std::endl; }	// 2
void foo( X&& x) { std::cout << "X&&" << std::endl; }		// 3
//void foo(const X&& x) { std::cout << "const X&&" << std::endl; }

int main()
{
	X x;
	foo(x);		// lvalue
			// 1번 호출, 없으면 2번

	foo(X());	// rvalue
			// 3번 호출, 없으면 2번
}​

표현식 타입 value category
X() X rvalue
rx X&& lvalue
#include<iostream>

class X {};

void foo(X& x) { std::cout << "X&" << std::endl; }				// 1
void foo(const X& x) { std::cout << "const X&" << std::endl; }	// 2
void foo( X&& x) { std::cout << "X&&" << std::endl; }			// 3

int main()
{
	foo(X());	// 3번

	X&& rx = X();

	foo(rx);	// 1번

	// lvalue => rvalue 캐스팅하면 3번
	foo(static_cast<X&&>(rx));	// 3번
}
  • foo( X&& )
    • rvalue reference 를 받는 것이 아니라 rvalue를 받겠다는 의미
  • static_cast<X&&>(rx);
    • rx가 이미 X&& 타입인데, "같은 타입 캐스팅" 아닌가요? -> 아님
    • 예외적으로 이 표기볍은 "타입 캐스팅이 아닌 value를 변환 하는 캐스팅"

 

  • reference collapsing
    • 참조를 가리키는 참조 타입
      • 참조를 가리키는 참조 변수를 "직접 코드"로 만들 수 없다.
      • 하지만, type deduction 과정에서 참조를 가리키는 참조 타입이 발생하면 "reference collapsing 규칙에 따라 타입이 결정"된다.
    • reference collapsing 규칙
      • Type&  &  : Type&
      • Type&  &&  : Type&
      • Type&&  &  : Type&
      • Type&&  &&  : Type&&
int main()
{
	int n = 3;
	int& lr = n;	// lvalue reference
	int&& rr = 3;	// rvalue reference

//	int&& ref2ref = lr;	// error

	decltype(lr)&	r1 = n;	// int& &	=> int&
	decltype(lr)&&	r2 = n;	// int& &&	=> int&
	decltype(rr)&	r3 = n; // int&& &	=> int&
	decltype(rr)&&	r4 = 3; // int&& && => int&&
}

 

  • "reference collapsing" 이 적용되는 경우
    • typedef
    • using
    • decltype
    • template
template<typename T> void foo(T&& arg)
{
}

int main()
{
	int n = 10;

	typedef int& LREF;
	LREF&& r1 = n;	// int& && => int&

	using RREF = int&&;
	RREF&& r2 = 10;	// int&& && => int&&

	decltype(r2) && r3 = 10;	// int&& && => int&&

	foo<int&>(n);	// foo(int& && arg)
					// foo(int& arg) 의 함수 생성
}

 

  • forwarding ( universal ) reference
    • 함수 파라미터의 모양
      • int& : int 타입의 lvaule 만 전달할 수 있다.
      • int&& : int 타입의 rvalue 만 전달할 수 있다.
      • T& : ?
      • T&& : ?

void f1(int& arg) {}
void f2(int&& arg) {}

template<typename T> void f3(T& arg) {}

int main()
{
	int n = 0;
	f1(n);	// ok
//	f1(0);	// error

//	f2(n);	// error
	f2(0);	// ok

	f3(n);	// ok
//	f3(0);	// error
}

 

  • template 에서의 forward(universal ) reference
    • 사용자가 "템플릿 인자를 직접전달" 하는 경우
      • 사용자가 전달한 타입을 적용해서 함수가 생성된다
전달한타입(T) T& 최종 생성된 함수
int int     &  => int& f3( int& arg )
int& int&   &  => int& f3( int& arg )
int&& int&& &  => int& f3( int& arg )
  • 사용자가 "템플릿 인자를 직접 전달하지 않는" 경우
    • 함수의 인자를 받을 수 있도록 T의 타입을 결정한다
f3( 0 ) T를 int, int&, int&& 중 어떠한 것으로 결정을 해도 rvalue인 0을 받을 수 없다.
컴파일러는 T를 int로 결정하고 함수를 생성하지만 "compile error"
f3( n ) T를 int, int&, int&& 중 어떠한 것으로 결정을 해도 lvalue인 n을 받을 수 있다.
컴파일러는 T를 int로 결정

 

template<typename T> void f3(T& arg) {}

int main()
{
	int n = 0;

	// 1. 사용자가 템플릿 인자를 직접 전달하는 경우
	f3<int>(n);	// T : int	T& : int&	f3( itn& arg ) 함수 생성
	f3<int&>(n);	// T : int&	T& : int&  &	f3( itn& arg ) 함수 생성
	f3<int&&>(n);	// T : int&&	T& : int&& &	f3( itn& arg ) 함수 생성

	// 2. 사용자가 템플릿 인자를 전달하지 않은 경우.
//	f3(0);	// error
	f3(n);	// ok
}

 

  • 함수 파라미터의 모양
int& "int 타입의 lvlaue"만 전달할 수 있다
int&& "int 타입의 rvlaue"만 전달할 수 있다
T& "임의의 타입의 lvlaue"만 전달할 수 있다
T&& ?

템플릿 함수 파라미터의 T&&

  • T&&인 템플릿 함수의 경우
template<typename T> void f4(T&& arg) {}

int main()
{
	int n = 0;

	// 1. 사용자가 템플릿 인자를 직접 전달하는 경우
	f4<int>(0);	// T : int		T&& : int&&		f4( itn&& arg ) 함수 생성
	f4<int&>(n);	// T : int&		T&& : int&  &&	f4( itn&  arg ) 함수 생성
	f4<int&&>(0);	// T : int&&	T&& : int&& &&	f4( itn&& arg ) 함수 생성

	// 2. 사용자가 템플릿 인자를 전달하지 않은 경우.
	f4(0);	// T=int&	f4(int& arg)
	f4(0);	// T=int&&	f4(int&& arg)
}
  • 사용자가 "템플릿 인자를 직접전달" 하는 경우
    • 사용자가 전달한 타입을 적용해서 함수가 생성된다
전달한타입(T) T&& 최종 생성된 함수
int int     &&  => int&& f4( int&& arg )
int& int&   &&  => int& f4( int& arg )
int&& int&& &&  => int&& f4( int&& arg )
  •  사용자가 "템플릿 인자를 직접 전달하지 않는" 경우
    • 함수의 인자를 받을 수 있도록 T의 타입을 결정한다
f4( n ) T를 int& 로 결정해야만 n을 받을 수 있다.
T=int& 로 결정, 최종함수 f4( int& arg )
T=int 로 결정하면 n을 받을 수 없다.
f4( 0 ) T를 int 또는 int&& 로 결정하면 rvalue인 0을 받을 수 있다.
T=int 로 결정, 최종함수 f4( int&& arg )

 

  • 함수 파라미터의 모양
int& "int 타입의 lvlaue"만 전달할 수 있다 lvalue reference
int&& "int 타입의 rvlaue"만 전달할 수 있다 rvalue reference
T& "임의의 타입의 lvlaue"만 전달할 수 있다 lvalue reference
T&& "임의의 타입의 lvalue와 rvalue" 를 모두 전달할 수 있다. forwarding reference : 표준위원회
universal reference : 스캇 마이어스
  • "모두 전달할 수 있다" 의 정확한 의미는?
  T T&& 최종 생성된 함수
f4( n ) int& int& f4( int& arg )
f4( 0 ) int int&& f4( int&& arg )
  • lvalue를 받을 수 있는 "함수가 생성"될 수 있고, rvalue를 받을 수 있는 "함수가 생성"될 수 있다는 의미.
#include<iostream>

template<typename T> void f4(T&& arg) 
{
	std::cout << __FUNCSIG__ << std::endl;
}

int main()
{
	int n = 0;
	f4(n);	// T=int&	f4(int& arg)
	f4(0);	// T=int&&	f4(int&& arg)

	const int c = 0;
	f4(c);
}

forwarding reference 일 경우 함수 생성

  • 함수 템플릿을 만들 때 "forwarding reference"를 사용하면
    • lvalue와 rvalue를 각각 받을 수 있는 "함수 생성"
    • 생성된 각 함수는 "call by value 가 아닌 reference"를 사용해서 전달 받는다.

 

  • lvalue와 rvalue를 모두 받을 수 있는 함수 만들기
    • 방법 1. call by value
      • 인자로 전달된 객체의 "복사본이 생성" 된다.
    • 방법 2. const lvalue reference
      • 복사본은 생성되지 않지만 "const 속성을 추가"해서 가리킨다.
    • 방법 3. lvalue 버전과 rvalue 버전의 함수를 따로 제공
      • 복사본도 없고, const 속성도 추가되지 않는다
      • 함수 "인자로 전달된 객체를 속성의 변화 없이" 받을 수 있다.
    • 방법 4. forwarding reference 사용
      • 방법 3의 함수들을 "컴파일러가 자동 생성"
void foo1(int arg) {}

void foo2(const int& arg) {}

void foo3(int& arg) {}
void foo3(int&& arg) {}

template<typename T> void foo4(T&& arg) {}

int main()
{
	int n = 0;
	foo4(n);
	foo4(0);
}
  • forwarding reference 의 의미
    • 임의 타입의 lvalue와 rvalue를 복사본을 만들지 않고 속성의 변화없이 그대로 받고싶을 때 사용
  • forwarding reference가 활용되는 주된 분야
    • Move sementic
    • Perfect forwarding

 

  • forwarding reference 사용시의 주의사항
    • forwarding reference 를 사용하려면
      • 함수 자체가 템플릿이어야한다.
      • foo함수는 "함수 템플릿이 아니라 클래스 템플릿의 메멉 함수(템플릿이 아닌)"이다.
template<typename T>
class Test
{
public :
	void foo(T&& arg) {}
	template<typename U> void goo(U&& arg) {}
};

int main()
{
	int n = 0;
	
	Test<int> t;	// T=int
			// void foo(int&& arg)

	// 아래 2줄을 생각해봅시다.
	// t.foo(n);	// error
	t.foo(0);	// ok

	t.goo(n);
	t.goo(0);
}

 

  • auto 와 forwarding reference
    • auto는 template과 type deduction 규칙이 동일하다.
auto& lvalue reference
auto&& forwarding reference
int main()
{
	int n = 0;

	auto a1 = n;	// ok
	auto a2 = 0;	// ok

	auto& a3 = n;	// ok
//	auto& a4 = 0;	// error

	// T&& arg = 함수인자  를 적용하면 됨
	auto&& a5 = n;	// auto=int&	int& && a5=n
	auto&& a6 = 0;	// auto=int		int && a6=0;
}
  • auto&&
auto&& a5 = n auto는 int& 로 결정됨
int&  &&  a5 = n     =>  int& a5 = n
auto&& a6 = 0 auto는 int 로 결정됨
int  &&  a6 = n     =>  int&& a6 = 0
  • auto 자리는 T로, 우변을 함수 인자로 생각