중요한! 임시 객체 #1_nrvo 수정_240706

보물창고·2022년 3월 31일
2
  • 코드누리 강의를 보고 공부, 정리한 내용입니다.
  • 업데이트 240320

https://chogyujin-study.tistory.com/76

추가 : 값반환 함수에서 객체를 반환한다면? 240803

: 임시객체를 반환해서 rvo가 적용되도록 하자.

통상적인 정의

: 필요에 이해 컴파일러가 만드는 임시 메모리 공간.

언제 만들어질까? : 중요!

  1. 객체를 값으로 반환할 경우.
  2. 객체를 값으로 매개변수로 사용할 경우.
  3. 생성자를 함수 호출하는 것처럼 하면 생성됨.
  4. 전역 객체를 값으로 반환하더라도 임시객체가 생성된다..

1) 직접 임시 객체 만들기

  • 일반적인 객체를 만드는 것이 아닌, 객체명 제외하고, 생성자를 호출하는 식으로 작성하자.

    Point(1,2);

  • 중요한 부분은 간접적으로 호출되는 임시객체!!
    객체 반환, 객체 인자로 전달할 때 생성된다는 것임.

2) 소멸 시점.

: 문장의 끝인 세미콜론의 끝에서 소멸됨. // 함수의 블록이 아닌.

problem

Point(1,2)로 임시 객체를 만든후, x y 값을 Print 함수로 출력하라. 이어서 세미 콜론 앞에 콤마 작성후, cout << "main " << endl 을 작성하라.

  • 출력 결과를 보면, Point(1,2) 임시 객체가 pint() 함수 호출후 소멸되는 것이 아니라, main 출력 후, 즉 세미콜론 이후에 소멸되는 것을 확인할 수있다.

임시객체 만드는 방법.

: 클래스의 객체명을 만들지 않음.
-> 생성자를 직접적으로 함수 호출하듯이 만들면 됨.

-> 위의 예제를 통해 임시객체의 수명을 알 수 있음.
-> 위의 예제를 통해 임시객체를 만드는 방법을 알 수 있음.


3) 임시 객체와 참조.

  • 임시 객체는 실체가 없음.

1. 임시 객체는 lvalue가 될수 없다. 즉 대입 불가
: 임시객체는 메모리에 지속적으로 존재하는 것이 아니라, 세미콜론 이후에
사라지는 객체이기 때문에 아래의 예시코드처럼 대입이 불가하다.

2. 메모리 주소를 가질 수 없음.
: 일반적인 생각으로는 불가하다고 생각한다.

  • 하지만 엄청난 혼란을 가지고 오는 방법이다. 아래의 설명과 코드를 보자.

  • 메모리 값을 구할 수 있지만, 포인터에 임시객체를 넣는 동작은 혼동을 가지고 온다.

: 아래 코드를 보면, 8번 줄의 delete 를 하기도 전에 이미 6번줄의
; 세미콜론을 마치고서 소멸됨을 확인할 수 있다.

  • 새로운 실험코드 이고,
    p2가 참조하고 있는 메모리는 임시객체의 영역이어서
    소멸되는 것을 출력을 통해 확인할 수 있다...
    그런데 p2 포인터 객체의 참조 객체의 멤버인 x가 정상적으로? 출력된다..
    c++최적화 기능에 의해 , 포인터 p2에 의해 실제 메모리는 삭제되지 않음을 확인할 수 있다. ,, 그런데 임시 객체니까 소멸자는 호출...

-> 굉장히 혼란스럽다 ..
여기서 우리는 당연히 포인터니까, delete를 해야 한다.
그래서 delete p2;를 해보면....
-> 엑!!! 이런 젠장!!

  • 느낀점
    실험을 위해서 복사 생성자, 복사 대입 , 이동 생성자, 이동 대입을
    = delete 코드를 만들었는데, 이 문제는 아니다.
    마소의 최적화로 인해 생명을 연장한것으로 보이는데,
    임시 객체로 인해 소멸자는 불가피하게 호출하고 있는 것으로 보인다...

결론 : 포인터와 임시객체

: 포인터 객체에 임시객체를 넣지 말자.
포인터에는 반드시 lvalue 와 같이 주소있는 객체를 참조해야 한다.
프로그래머의 생각과는 다르게 동작한다.

3. 임시객체는 lvalue 참조 불가함.
-> 실체가 없기 때문에 당연히 불가하다.

4. 임시객체는 const 참조는 가능함. -> 이때는 상수성을 가지게 됨.
밑의 예제 참고.

    1. 상수 객체로 처리된다. 즉 수정이 불가하다.
      : const& 로 받을 수 있는 이유는 const & 참조는 lvalue와 rvalue 둘다 참조가 가능하기 때문임.
    1. 수명이 연장된다.
      : 추가적으로 임시 객체의 수명은 참조 객체와 동일해진다.
      -> 진짜루 임시객체가 46번줄에서 소멸되지 않고, 47번줄에서 정상적으로 출력됨을 통해 수명이 늘어난 것을 확인할 수 있고,
      r1과 r2가 블록에서 소멸되는 것을 확인할 수 있다.
      --> 위의 포인터로 참조할때보다는 안전하다.

5. 임시객체는 rvalue 참조가 가능함.

    1. 수정이 가능하다.
      rValue 레퍼런스 즉, && 2번 사용하는 친구와 함께할 경우, 값변경 가능.
      -> 수정 가능. 이 때는 rvalue 참조 변수 r5는 lvalue 로 결정된다.
    1. 수명이 연장된다.
      아래 결과를 보면 임시객체의 수명이 r5 객체에 의해 수명 연장된 것을 확인할 수 있다.

정리 / 그리고 생명 연장.

임시 객체는 소멸적인 존재이나, const 참조 객체 또는 const 변수에 대입시
생명이 const 참조 객체 또는 const 참조 변수의 생명으로 연장됨.
그러나 이 때는 상수성을 가지므로, 값 변경이 불가.

임시 객체는 또 rvalue 참조를 통해 수명이 연장되고, 이 때 rvalue 객체는
lvalue로 결정된다. 수정이 가능한 존재가 되어 버린다.

위의 내용을 토대로 포인터에다가 임시객체를 넣으면 안된다.

problem

0) 포인터로 참조하자.
: 가능하지만, 해지해보면, 오류 발생 -> 위에 예시코드 있음.
가) 임시객체의 public 멤버 데이터에 값을 대입해보자.
나) lvalue 참조 객체로 임시객체를 참조하자.
: 이유 -> lvalue 참조 객체는 반드시 메모리 계속 존재하는것만 참조가 가능하기 때문임.
다) rvalue 참조 객체로 임시객체를 대입하라.
: rValue 참조는 상수성이 없음!
라) const 참조 객체로 임시객체를 대입하라.
: 상수성이 추가됨.

4) 임시객체와 함수.

  • 함수에 객체를 인자로 보낼 경우, 객체의 수명이 어떻게 되는지 알아보자.

    -> 위의 코드를 보면, const& 이기 때문에 임시객체는 생성되지 않음을
    알 수 있따.

  • 임시 객체를 함수 인자로 보낼 경우, 함수 호출 완료 후, 객체는 소멸됨.

만약에 객체가 함수의 인자에서만 사용되는 용도라고 한다면,
객체를 만들지 않고, 임시객체로 인자로 보내는 것이 효율적임.
왜냐하면 수명을 호출된 이후까지 가져갈 필요없이, 함수내에서 수명을 마치게 하는 것이 메모리 측면에서 도움이 되기 때문임.

그런데 이 때도 알아야 할 점이 임시 객체를 받는 인자의 타입은
적어도 const & 타입이어야 한다. 또는 && rvalue Ref로 받아야 한다.


-> 위의 그림을 보면, const& 객체이기 때문에 print 함수 호출이 불가하다.
예~~전에 const 객체와 const 함수에 대해서 설명했는데,
여기서 확인해볼수 있는 코드이다.

  • print 함수에 const 성을 추가하면 , 에러 없이 실행되는 것을 확인할 수 있다. 그리고 임시객체이기 때문에 함수 foo 호출 후에 소멸되는 것도 확인할 수 있다.
  • 주의사항.
    : 임시객체를 인자로 받기 위해서는 어떻게 해야할까? 이를 고민하면서 밑의
    problem을 작성하라. 2가지 방법이 있음!

problem

가) 객체를 함수 인자로 보내고, 다음 행에 cout << "hello" 출력하라.
나) 임시객체를 함수 인자로 보내고, 다음 행에 cout << "hello" 출력하라.

Point p1;
foo(p1);
foo(Point());
cout << "Wow";

  • foo(what type Point p)
    : 타입에 따라서 소멸자 호출되는 갯수 달라짐.
  • 알아야 할점은 인자 타입도 중요함.
    1) value 일 경우, 복사 발생
    -> 위 코드를 호출하면 4번의 소멸자
    2) &일 경우, lvalue 만 받을 수 있음.
    -> lvalue 만 받을 수 있음.
    3) && 일 경우, rvalue , 임시객체만 받기 때문에, 복사 발생 안함.
    -> rvalue 만 받을 수 있음.
    4) const Point& 일 경우,
    -> lvalue , rValue 다 받을수 있고, 이때는 2번의 소멸자 호출.

stl에서의 예시

  • sort(v.begin(), v.end(), greater< int >) 에서
    greater 비교함수도 사실은 임시객체임.

임시객체와 반환값.

  • 규칙

    함수의 인자를 일반 객체로 선언하면, 임시객체가 생성되는 것 처럼.
    c++ 에서는 객체를 값 타입으로 반환하면, 임시객체가 생성됨.

problem

가) objcect 클래스를 만들고, 대입 연산자. 복사 생성자를 만들자, 디폴트 ,
소멸자도 만들자.
그리고 func()이라는 함수 내부에서 객체를 만들고, 값 타입으로 반환하고,
그리고 Object obj = func() 하자.

  • 예상되는 결과
    : 임시객체가 만들어지고, 만약에 이를 반환 받는 객체가 있다면?
    대입연산자도 호출되는 현상이 발생함.
    // 대입연산자 호출을 위해, 초기화 이후에 반환 받음.
    1. 대입연산자가 영향을 주는 코드
      : func 함수에서 o를 생성함., o는 이미 제거되지만,-> 대입으로 받아야하므로, 임시객체를 생성함. -> explicit 처리를 안했기 때문에, 컴파일러가 변환처리했기 때문에, 복사 생성 Object(o); 만들어짐.
      -> 이어서 main에 이미 만들어진 객체 a 에 대입을 하고 있으니까, 대입연산자 진행.
    • 결과 : 값리턴으로 임시객체 만들어짐 -> 비효율적임...
    1. 초기화로 바로 받으면 , 조금 좋아짐.
      : main에서 객체 a 생성과 동시에 대입하니까, 복사 생성자 호출함.
    1. 그냥 임시객체로 보내버리는 코드
      : 생성자와 소멸자가 단 한번만 호출됨!

    rvo에 대한 결론

    객체를 값반환한다고 한다면, 차라리 임시객체를 보내버리자!
    그러면 함수에서의 암시적 형변환으로 인한 복사 호출
    -> 임시 객체가 안 만들어짐.
    그리고 받는 곳에서는 선언과 동시에 대입하도록 해서
    대입 연산자 호출되지 않게 하자.

  • 그리고 위의 object 예시의 경우, 최적화설정이 안되어 있어서 copy가 이루어진 것 같다.


RVO

정의

: 선언된 객체가 임시객체 반환함수를 대입연산자으로 받으면, 컴파일러가 임시객체 생성 없이 해당 객체를 직접 초기화하게 하는 최적화 방법.

알아야 한 점이 반환하는 함수에서는 객체 선언 없이
임시객체 호출한 것을 바로 반환해야 함!

240721 추가

: standard library p.58
: 사용할 수 있는 복사 생성자나 이동 생성자를 제공한다면, 컴파일러는 복사를 생략하고, 반환값 최적화를 제공한다.

  • 그런데??? 복사 생성자 없어도 소멸자 하나만 호출되는 것을 보니 반환값 최적화 되었다.

    240706 수정됨.

    객체 반환할때 그냥 값으로 반환하자.
    : 컴파일러가 rvo ,nrvo 처리를 알아서 한다.

  • 해당 예시가 적합.
    : 컴파일러가 알아서 rvo 처리하는 것을 확인할 수 있다. 240716

  • 원래 대로 라면, 아래와 같이 복사가 나와야 한다.

    https://dydtjr1128.github.io/cpp/2019/08/10/Cpp-RVO(Return-Value-Optimization).html

rvo를 확인하기 위한 빌드업.

  • 아 뭔솔이냐?? 코드를 봅세

func이 반환하는 객체를 대입하고 있는데.. 반환값은 지역 객체이므로 해제되는 값인데? 반환하고 있으므로 컴파일러는 필수적으로 임시객체를 만들어야 함.
따라서 복사 생성자를 호출하고 있음.
: 이때는 암묵적으로 만듬.

rvo 버전.

: 반환하는 값타입 함수에서 임시객체를 바로 반환하고 있어서,
위의 빌드업 그림처럼 생성 , 복사가 이루어지지 않고 있음.

NRVO

: 이름이 있는 객체도 RVO가 가능하게 하는 것.
cl 컴파일러는 설정을 해야 함.

나. 임시객체를 인자로 보내는 경우인데,,,

보통 이러한 경우에는 많이 없지만,
위와 같이 외부의 객체가 함수의 인자로 보내는 용도로 사용할 때 사용.

5) 알아야 할 지식.

: 전역 객체라고 하더라도, 값으로 리턴할 경우, 어쨋든 임시객체가 만들어짐.
왜냐하면? c++에서 값반환은 임시객체를 만들기 때문임.

  • 함수에서 전역객체에 값을 넣어보면?

    : 멤버값 변경이 되는 것은 당연, 위 그림 처럼 반환할 경우에는 임시 객체가 생성된다는 것임.
profile
🔥🔥🔥

0개의 댓글