복사 생략 Copy Elision

class A {
  int data_;

 public:
  A(int data) : data_(data) { std::cout << "일반 생성자 호출!" << std::endl; }

  A(const A& a) : data_(a.data_) {
    std::cout << "복사 생성자 호출!" << std::endl;
  }
};

int main() {
  A a(1);  // 일반 생성자 호출
  A b(a);  // 복사 생성자 호출

  // 일반 생성자 호출!
  // 복사 생성자 호출!
  // 일반 생성자 호출!
  A c(A(2));
}
  • 마지막 코드에서 “복사 생성자 호출!” 이 한 번 더 호출되어야 했을 것 같은데, 불리지 않았다
  • 컴파일러에서 불필요한 복사 생성자 호출을 생략하는 작업을 복사 생략이라 한다

Note

C++ 17 부터 일부 경우에 대해서 (예를 들어서 함수 내부에서 객체를 만들어서 return 할 경우) 반드시 복사 생략을 해야되는 것으로 바뀌었다고 한다.
https://en.cppreference.com/w/cpp/language/copy_elision

Value Category

  • 정체를 알 수 있는가
    • 해당 식이 다른 식과 같은 것인지 아닌지 구분할 수 있는가?
    • 변수 - 주소값을 통해 구분
    • 함수 - 이름으로 구분
  • 이동 시킬 수 있는가
    • 해당 식을 안전하게 다른 곳으로 이동할 수 있는가?
    • 이동 생성자, 이동 대입 연산자를 사용할 수 있는지의 여부
      이동 가능 이동 불가능
    정체를 알 수 있다 xvalue lvalue
    정체를 알 수 없다 prvalue 쓸모없음

좌측값과 우측값의 정의

  • C++에서는 아래와 같이 정의한다고 한다

좌측값은 어떠한 메모리 위치를 가리킨다. & 연산자로 그 주소를 참조할 수 있다
우측값은 좌측값이 아닌 값들이다

int a = 3; // a는 좌측값, 3은 우측값
int& l_a = a;  // l_a 는 좌측값 레퍼런스, 좌측값 레퍼런스도 좌측

int& r_b = 3;  // 3 은 우측값. 따라서 오류

Note

const T& 타입에 한해서, 우측값도 레퍼런스로 받을 수 있다
어차피 const 이기 때문에, 임시 객체의 값을 참조만 할 뿐 변경이 불가능하기 때문

move sementic

  • 임시 객체를 생성한 뒤, 복사 생성자를 호출하는 케이스가 있다
X foo();  // foo 는 X 타입의 객체를 리턴하는 함수 이다!
X x;
x = foo();

/*
 * 1. x 가 가지고 있는 리소스가 소멸된다.
 * 2. (foo 가 리턴한) 임시 객체의 리소스의 복제된 버전이 생성된다.
 * 3. x 가 복제된 리소스를 가리키고, 임시로 생성된 객체의 리소스는 소멸된다.
 */
  • 그러나 위의 경우에서, 굳이 임시 객체의 리소스를 또 복제할 필요가 없다
  • 최종 할당 되어야 할 객체의 리소스가 임시 객체의 리소스를 가리키도록 연결하고, 기존 리소스는 임시 객체의 그것이 가리키도록 하면 된다
    • 일종의 swap
X& X::operator=(<미지의 타입> rhs) 
{
  // [...]
  //  m_pResource 와 rhs.m_pResource 를 교환
  // [...]
}
  • 이는 복사 대입 연산자의 overload를 구현하는 것이다
    • 이 때 인자의 타입은 당연히 교환을 위해 레퍼런스(&)로 받아야 한다
    • 동시에, 인자로 들어온 게 좌측값인 경우 일반 레퍼런스, 우측값인 경우 저 “미지의 타입”을 사용해야 한다
  • 그리고 이를 위해 고안된 것이 우측값 참조이다

우측값 참조

void foo(X& x);   // 좌측값 참조 오버로드
void foo(X&& x);  // 우측값 참조 오버로드

X x;
X foobar();

foo(x);  // 인자에 좌측값이 들어 갔으므로 좌측값 참조 함수가 오버로딩
foo(foobar());  // 인자에 우측값이 들어 갔으므로 우측값 참조 함수가 오버로딩
  • 위의 코드에서 좌측값 인자는 좌측값 함수를 오버로드하고, 우측값 인자는 우측값 레퍼런스를 사용한다
  • 즉, 컴파일 시 인자로 좌측값이 오는지 우측값이 오는지 판단하고 오버로드 한다

Note

우측값 레퍼런스라 정의한 것들도 좌측갑 또는 우측값이 될 수 있다
이를 판단하는 기준은, 이름이 있다면 좌측값, 없다면 우측값이다

lvalue (Left Value)

  • 좌측값은 어떠한 표현식의 왼쪽, 오른쪽 모두에 올 수 있음
  • 이동(move)시킬 수 없음
  • 이름을 가진 대부분의 객체들은 모두 lvalue
    • 객체의 주소값을 취할 수 있기 때문
    • 왼쪽에만 올수 있고 그런 거 아님
  • 생명 주기가 표현식의 범위를 넘어선다. 즉, 표현식이 끝나도 객체는 계속 존재
int a;
a;

int&& x = a; // error
  • 주소값 연산자(&) 를 통해 해당 식의 주소값을 알아 낼 수 있다
    • &++i&std::endl 은 모두 올바른 작업
    • 또한 lvalue 들은 좌측값 레퍼런스를 초기화 하는데에 사용할 수 있다
  • 주의할 점
void f(int&& a) {
  a;  // lvalue다
}

f(3);

// 식 a의 타입은 우측값 레퍼런스
// 식 a의 값 카테고리는 lvalue -> 이름이 있기 때문
// 따라서, 아래의 식은 컴파일이 가능함

// main.c
#include <iostream>

void f(int&& a) { std::cout << &a; } // a의 주소값 추적 가능
int main() { f(3); }
  • lvalue의 범주
    1. 변수, 함수의 이름, 어떤 타입의 데이터 멤버
        // ex)
        int i;
        void func1();
        std::endl;
      
    2. 좌측값 레퍼런스를 리턴하는 함수의 호출식
        // ex)
        std::cout << i;
        ++it;
      
    3. 복합 대입 연산자식
        // ex)
        a = b; a+=b; a*=b;
      
    4. 전위 증감 연산자식
        // ex)
        ++a; --a;
      
    5. 클래스의 멤버 메서드/변수를 참조할 때 (단, static 메서드도, enum도 아닌 경우)
        // ex)
       class A {
         int f();         // static 이 아닌 멤버 함수
         static int g();  // static 인 멤버 함수
       }
      
       A a;
       a.g;  // <-- lvalue
       a.f;  // <-- lvalue 아님 (아래 나올 prvalue)
      
    6. 배열 참조식
        // ex)
        int a[n];
      
    7. 문자 리터럴
        // ex)
        "hi"
      
      • 문자 리터럴의 케이스는 조금 특별하다
      • 문자열은 character의 const static 배열이며, 프로그램의 메모리 상에 존재한다
      • 분류상 glvalue에 속한다
      • xvalues는 될 수 없기에, lvalues로 분류된다

prvalue (Pure Value)

  • 이동시킬 수 있음
  • 주소값을 취할 수 없음
  • 우측값은 식의 오른쪽에만, read-only (식의 좌측에 올 수 없음)
  • Take the variables themselves, not copying them
  • 생명 주기가 짧다. 보통 표현식이 완료되는 즉시 소멸
  • prvalue의 범주
    1. 문자열을 제외한 리터럴들
        // ex)
        42, true, nullptr;
      
    2. 레퍼런스가 아닌 것을 리턴하는 함수의 호출식
        // ex)
        str.substr(1,2);
        str1 + str2;
      
    3. 후위 증감 연산자식
        // ex)
        a++, b--;
      
    4. 산술 연산자식, 논리 연산자식
        // ex)
        // 오버로딩된 연산자 아닌 디폴트 연산자
        a + b, a && b, a < b;
      
    5. 주소값 연산자식
        // ex)
        &a;
      
    6. 클래스의 멤버 메서드/변수를 참조할 때 (단, m이 enum이거나 static 메서드인 경우)
    7. this
    8. enum 값
    9. 익명함수 (람다식)
        // ex)
        []() { return 0;};
      

xvalue (eXpiring Value)

  • 좌측값처럼 주소값을 트래킹할 수 있지만, 또한 이동도 할 수 있는 카테고리
  • lvalue와 prvalue의 중간 형태
  • std::move()를 이용해서 우측값 레퍼런스를 리턴하는 호출식
    std::move(x);
    
    vector<int> a, b = {1,2,3,4,5};
    a = b; // a: 1,2,3,4,5 b:1,2,3,4,5
    a = std::move(b); // b-> rvalue, a:1,2,3,4,5 b:{}
    
  • 생명 주기가 표현식의 범위를 넘어설 수도 있지만, 그 자원은 더 이상 유효하지 않을 수 있다

출처

Categories: ,

Updated: