Plite
전자오락 공방
Plite
전체 방문자
오늘
어제
  • 분류 전체보기 (274)
    • 프로젝트 (18)
      • 완성 프로젝트 (3)
      • 프로젝트 진행 내역 (15)
    • 공부 및 정리 (241)
      • 백준 코드 (222)
      • C++ (8)
      • DirectX (2)
      • Unreal Engine (6)
      • 프로그래밍 패턴 (3)
    • 기타 (12)
      • 기타 주저리 (10)
    • 게임과 취미 (1)
    • 대문 (1)

블로그 메뉴

  • 홈
  • 프로젝트
  • 취미, 일상
  • 백준 프로필

공지사항

  • [Read Me]
  • 제 블로그에 방문하신 것을 환영합니다.

인기 글

태그

  • 스택
  • 동적계획법
  • LCA
  • KMP
  • 수학
  • 구현
  • 브루트포스
  • SCC
  • 큐
  • 투포인터
  • 유니온 파인드
  • 기하
  • 백트래킹
  • 분할정복
  • 세그먼트 트리
  • 정렬
  • C++
  • 이분탐색
  • 우선순위큐
  • 최소 스패닝 트리
  • 조합론
  • 그래프
  • 백준
  • 트리
  • UC++
  • 문자열
  • 트라이
  • 위상 정렬
  • 누적합
  • 정수론

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
Plite

전자오락 공방

[메모] l-value와 r-value
공부 및 정리/C++

[메모] l-value와 r-value

2022. 12. 20. 21:58

 

* 서론

 

- 고전적으로 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
    '공부 및 정리/C++' 카테고리의 다른 글
    • C++의 기초 - 7
    • C++의 기초 - 6
    • C++의 기초 - 5
    • C++의 기초 - 4
    Plite
    Plite
    개발 일지, 게임 이야기 등을 적어두는 공간입니다.

    티스토리툴바