모두의 코드 내용을 공부하고 정리한 내용입니다.




복사 생략

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;
}
};
A c(A(2));  // 일반 생성자만 호출됨
  • 위 예시에서 내가 생각한 것은 A(2)에서 일반 생성자 호출후 임시 객체에 c가 복사되며 복사생성자가 호출될 것을 예상했지만 아니네?
    • 사실 굳이 임시 객체 1번 만들고 여기에 복사를 할 필요가 없다.
    • 그냥 A(2)를 c로 만들꺼면 처음부터 c자체를 A(2)로 만들어진 객체로 해버리자
      • 그래서 컴파일러는 굳이 복사 생성을 하지 않고 만들어진 A(2) 자체를 c로 만든다


  • 이렇게 똑똑한 컴파일러는 복사 생성을 굳이 수행하지 않고, 임시 객체를 바로 객체를 만들어버리는 것이 복사 생략이다.

  • 복사 생략을 하는 경우는 (함수의 인자가 아닌) 함수 내부에서 생성된 객체를 그대로 리턴할 때이다.
  • 경우에 따라 생략을 할 수 도 있고 안할 수 도 있다
    • 그럼 안 하면 우짜지… 비효율적인 복사가 많아 꼭 필요한데…
      • C++17 부터 일부 경우에 대해서 무조건 복사생략을 해야하는 것으로 변경되었다.
      • 여기 참고


  • 참고로 복사 생성자 호출을 막고 싶다면 복사 생성자를 private에 정의하면 된다.

좌측값(lvalue) 우측값(rvalue)

  • 모든 C++ 표현식 (expression) 의 경우 두 가지 카테고리로 구분한다.
  • 하나는 이 구문이 어떤 타입을 가지냐 이고, 다른 하나는 어떠한 종류의 ‘값’ 을 가지냐?.


int a = 3;

  • 위의 a는 메모리 상에서 존재하는 변수, &연산자로 주소값을 알아낼 수 있는 이런 값은 좌측값(lvalue) 라고 부른다.
  • 좌측값은 표현식의 왼쪽, 오른쪽 모두 올 수 있다.


  • 위 3은 주소값을 취할 수 없다. a와 다르게 표현식을 연산할 때 잠깐 존재할 뿐 사라지는 값이다.
  • 실체가 없난 값, 주소값을 취할 수 없는 값을 우측값(rvalue) 이라고 한다.
  • 우측값은 항상 오른쪽만 와야 한다.


l-value reference (좌측값 레퍼런스)

int a;         // a 는 좌측값
int& l_a = a;  // l_a 는 좌측값 레퍼런스
int& r_b = 3;  // 3 은 우측값. 따라서 오류
  • 이때동안 다룬 레퍼런스는 ‘좌측값’에만 레퍼런스를 생각했다.
    • 위의 a는 좌측값이여서 a의 좌측값 레퍼런스인 l_a 를 만들 수 있다.
  • 반면에 3 의 경우 우측값이기 때문에, 우측값의 레퍼런스인 r_b 를 만들 수 없다.
    • 오류!
    • 하지만 const T& 타입의 한해서만 우측값도 레퍼런스로 받을 수 있다.
      • 이유는 const 레퍼런스 이기 때문에 임시로 존재하는 객체의 값을 참조만 할 뿐 변경할 수 없기 때문이다.
  • 이와 같이 &를 이용해서 정의하는 레퍼런스를 좌측값 레퍼런스 (lvalue reference) 라고 부르고, 좌측값 레퍼런스 자체도 좌측값이다다.


  • 이렇게 좌측값 레퍼런스를 받게 된다면 주소를 가지고 놀게 되어 좌측값 레퍼런스를 받는 함수는 임시 객체를 생성하지 않는다.


r-value reference (우측값 레퍼런스)

  • const를 사용해 우측값 레퍼런스, 좌측값 레퍼런스 모두 받아서 사용할 수 있지만 const이기에 변경할 수 없다는 문제가 있다.
  • 그러면 좌측값 말고 우측값만 특별하게 받을 수 있는 방법은 없을까?


  • C++ 11에는 우측값 레퍼런스를 파라미터로 갖는 생성자가 추가
    • &&
    • 우측값 레퍼런스는 &를 2번 사용해서 정의한다.
      • T && t
int&& r_b = 3; // 우측값
int&& rr_b = a; // 불가능 , 좌측값


  • 우측값 레퍼런스는 좌측값이 아닌 우측값만 받을 수 있다. -> 임시 객체를 받는 함수를 정의할 수 있다
    • 만약 어떤 클래스에 우측값 레퍼런스를 받는 생성자만 만들었다면 이름을 가지는, 즉 좌측값을 받는 생성자를 호출할 수 없다.


  • 그렇다면 그냥 & 이나 &&를 받는 함수를 만들지 않으면 되지 않냐?
    • 매번 임시 객체를 생성하는 작업이 이루어져 비효율적이다.


  • 그러면 &만을 사용하면 안되나?
    • 실제로 좌측값 레퍼런스를 받는다면 우측값을 넘겨줘도 에러없이 실행은 된다.
    • 그런데 표준 C++ 라이브러리들은 임시 객체를 굉장히 많이 생성하는데, 이런 임시 객체를 제대로 유지하고 계속 복사되는 오버헤드를 처리하기 위해 &&가 생겨난 것이다.


move constructor(이동 생성자)

  • 동적으로 데이터를 관리하는 경우 복사되는 관점에서 문제가 되기 때문에 복사 생성자를 사용했다.
  • 또한 대입에서 생기는 문제점(댕글링 레퍼런스 생성) 때문에 복사 대입 연산자를 사용했다.
    • 하지만 이 과정에서 임시 객체의 복사 그리고 대입되는 상황으로 인해 메모리 할당과 해제가 너무 빈번하게 일어나는 문제 때문에 이동 생성자가 생겨났다.
  • 그냥 복사 없이 연산된 객체 자체를 리턴을 한다면 좋지 않을까?
  • 임시 객체를 복사해서 새로운 객체를 만드는 것이 아닌 임시 객체를 추적하여 접근을 한다.
class A{
    private:
        // data
    public:
        A();            // 디폴드 생성자
        A(const A &a);  // 복사 생성자
        A(A &&a)       // 이동 생성자
        {
          // 어떠한 작업 수행
          // 데이터를 그냥 대입
          // a(기존 임시 객체)는 nullptr로 초기화화
        }
}
  • 이동 생성자의 a는 좌측값? 우측값?
    • 좌측값이다.
    • aA의 우측값 레퍼런스인 좌측값 이라고 보면 된다.
  • 우측값 레퍼런스는 사라지는 값인 우측값을 계속 갖고 있는 것 처럼 보이는데
  • 우선 임시 객체에서 데이터를 가져와서 새로 생성될 객체에 주소를 전달하고 임시객체의 데이터는 nullptr를 할당해서
    • 임시 객체의 데이터는 사라않고 새로 생성된 객체에 이동이 되어서 접근이 계속가능하고 임시객체는 nullptr이기에 접근을 못하게 한다.


이동 대입 연산자

  • 기존의 대입 연사자 처럼 우측값 레퍼런스를 기준으로 한 대입 연산자도 만들어준다
  • 기존 대입 연산자와 차이점은 메모리 할당 없이 그대로 데이터를 넣어주고, 기존 객체는 초기화를 해준다.

문제점이 전부 해결된 것 같지만 그렇지 않다 해당 이야기는 perfect forwarding에서 이야기한다.

이동 생성자 주의 점

  • 이동 생성자 작성시 해당 클래스를 C++ 컨테이너에 적용할 때는 반드시 이동 생성자를 noexcept로 명시 필요.


  • vector로 예시를 보자
  • 새로운 원소 추가시 메모리가 부족하면 새로운 메모리를 할당한 후에. 기존 원소를 새로운 메모리로 옮긴다.
  • 복사 생성자의 경우 원소가 하나씩 복사되는데, 이 과정에서 예외가 발생한다면, 새로 할당한 메모리를 소멸시킨후 사용자에게 예외를 전달하게 될 것이다.


  • 히지만 이동 생성자는 상황이 다르다
  • 복사 생성은 복사를 하고 새로운 공간에 할당해서 그 공간을 소멸시켜도 기존 원소는 존재하지만
  • 이동 생성의 경우 기존 메모리에서 원소들이 모두 이동되어 사라지기에 섯불리 새 메모리를 해제를 하면 원소 자체가 사라진다.
    • 따라서 이동 생성자에서 예외 발생시 처리를 못한다.
    • 그렇기에 이동 생성자는 noexcept가 아닌 이상 사용이 불가하다.

A(A &&a) noexcept;


참고 복사 생략 경우

개인 공부 기록용 블로그입니다.
틀린 부분 있으다면 지적해주시면 감사하겠습니다!!

카테고리:

업데이트:

댓글남기기