Abseil Tip 166 복사가 복사가 아닐 때

Richard Smith (richardsmith@google.com) 작성
최초 게시일: 2019년 8월 28일
최종 업데이트: 2020년 4월 6일

빠른 링크: abseil.io/tips/166


“필요하지 않은 한 존재를 늘리지 말라.” – 윌리엄 오컴
“어디로 가야 할지 모르면 잘못된 길로 갈 가능성이 높다.” – 테리 프래쳇


개요

C++17부터 객체는 가능한 경우 “제자리에서(in place)” 생성됩니다.

class BigExpensiveThing {
 public:
  static BigExpensiveThing Make() {
    // ...
    return BigExpensiveThing();
  }
  // ...
 private:
  BigExpensiveThing();
  std::array<OtherThing, 12345> data_;
};

BigExpensiveThing MakeAThing() {
  return BigExpensiveThing::Make();
}

void UseTheThing() {
  BigExpensiveThing thing = MakeAThing();
  // ...
}

이 코드에서 BigExpensiveThing은 몇 번 복사 또는 이동될까요?

C++17 이전에는 최대 세 번 복사 또는 이동될 수 있었습니다. 각 return 문과 thing 초기화 시 한 번씩입니다. 이는 함수가 각각 다른 위치에 BigExpensiveThing을 배치할 수 있기 때문에 발생하며, 최종적으로 호출자가 원하는 위치로 이동해야 할 수 있기 때문입니다. 그러나 실제로는 객체가 항상 변수 thing에 “제자리에서” 생성되었으며, 이동 연산이 수행되지 않았습니다. C++ 언어 규칙은 이러한 이동 연산을 생략하여 최적화를 허용했습니다.

C++17부터는 이 코드는 복사 또는 이동을 보장하지 않습니다. 위의 코드는 BigExpensiveThing이 이동 가능하지 않더라도 유효합니다. BigExpensiveThing::Make 내의 생성자 호출은 UseTheThing의 로컬 변수 thing을 직접 생성합니다.


동작 원리

컴파일러가 BigExpensiveThing()와 같은 표현식을 볼 때, 즉시 임시 객체를 생성하지 않습니다. 대신, 해당 표현식을 최종 객체를 초기화하는 방법으로 간주하며, 임시 객체의 생성(공식적으로 “materializing”)을 가능한 한 늦춥니다.

대개 객체 생성은 해당 객체에 이름이 지정될 때까지 연기됩니다. 이름이 지정된 객체(thing)는 초기화자 평가를 통해 직접 초기화됩니다. 이름이 참조라면, 값을 저장할 임시 객체가 생성됩니다.

결과적으로 객체는 다른 곳에서 생성되어 복사되는 대신, 올바른 위치에 직접 생성됩니다. 이를 “복사 생략(copy elision) 보장”이라고 부르지만, 정확히는 복사가 처음부터 존재하지 않았습니다.


복사가 발생하는 경우

기본 클래스 생성 시

생성자에서 기본 클래스를 초기화할 때, 이름 없는 기본 클래스 표현식으로 초기화하더라도 복사가 발생할 수 있습니다. 이는 기본 클래스 사용 시 레이아웃이 달라질 수 있기 때문입니다.

class DerivedThing : public BigExpensiveThing {
 public:
  DerivedThing() : BigExpensiveThing(MakeAThing()) {}  // 데이터 복사 가능성 있음
};

작은 트리비얼 객체 전달 시

트리비얼(trivial) 복사가 가능한 작은 객체를 함수에 전달하거나 반환할 때, 레지스터를 통해 전달될 수 있으며, 전달 전후에 주소가 다를 수 있습니다.

struct Strange {
  int n;
  int* p = &n;
};

void f(Strange s) {
  CHECK(s.p == &s.n);  // 실패 가능성 있음
}

void g() { f(Strange{0}); }

값 카테고리

C++에는 두 가지 종류의 표현식이 있습니다:

  • 값을 생성하는 표현식: 예를 들어 1이나 MakeAThing()처럼 비참조 타입을 가진 것.
  • 기존 객체의 위치를 생성하는 표현식: 예를 들어 sthing.data_[5]처럼 참조 타입을 가진 것.

전자는 prvalue(pure rvalue), 후자는 glvalue(generalized lvalue)라고 불립니다.

BigExpensiveThing thing = MakeAThing();

여기서 MakeAThing()thing 변수를 초기화하는 prvalue 표현식으로 평가됩니다. 마찬가지로,

return BigExpensiveThing();

에서는 컴파일러가 초기화할 객체의 포인터를 직접 초기화합니다.


관련 자료