2012-07-23 3 views
8

먼저 두 개의 번역 단위로 구성된 다음 코드를 살펴보십시오.g ++ : 여러 번역 단위가 관련된 경우 RVO 작동 방법

--- foo.h --- 

class Foo 
{ 
public: 
    Foo(); 
    Foo(const Foo& rhs); 
    void print() const; 
private: 
    std::string str_; 
}; 

Foo getFoo(); 

--- foo.cpp --- 
#include <iostream> 

Foo::Foo() : str_("hello") 
{ 
    std::cout << "Default Ctor" << std::endl; 
} 

Foo::Foo(const Foo& rhs) : str_(rhs.str_) 
{ 
    std::cout << "Copy Ctor" << std::endl; 
} 

void Foo:print() const 
{ 
    std::cout << "print [" << str_ << "]" << std:endl; 
} 

Foo getFoo() 
{ 
    return Foo(); // Expecting RVO 
} 

--- main.cpp --- 
#include "foo.h" 

int main() 
{ 
    Foo foo = getFoo(); 
    foo.print(); 
} 

foo.cpp와 main.cpp가 다른 번역 단위인지 확인하십시오. 그래서 내 이해에 따라 번역 단위 main.o (main.cpp)에서 사용할 수있는 getFoo()에 대한 구현 세부 사항이 없다고 말할 수 있습니다.

그러나 위에서 컴파일하고 실행하면 RVO가 작동한다는 것을 나타내는 "Copy Ctor"문자열을 볼 수 없습니다.

'getFoo()'의 구현 세부 사항이 번역 단위 main.o에 노출되지 않아도 달성 할 수있는 방법에 대해 알려 주시면 감사하겠습니다.

GCC (g ++) 4.4.6을 사용하여 상기 실험을 수행했다.

+0

-O0 플래그는 예를 컴파일한다. – ForEveR

+0

그리고 컴파일러에서 C++ 11 기능을 사용할 수 있습니까? – ForEveR

+0

-O0 플래그를 넣어도 상황이 변경되지 않습니다. 이 경우 RVO가 작동하는 것 같습니다. 내가 사용하는 컴파일러는 내 이해에 따라 C++ 11 기능을 허용하지 않습니다. – Smg

답변

11

컴파일러는 일관되게 작동해야합니다.

즉, 컴파일러는 유형 만보고 해당 유형에 따라 해당 유형의 객체를 반환하는 함수가 값을 반환하는 방식을 결정해야합니다.

적어도 일반적인 경우, 해당 결정은 입니다. 상당히입니다. 반환 값에 사용할 레지스터 (또는 아마도 2 개)를 따로 설정합니다 (예 : Intel/AMD x86/x64, 보통 EAX 또는 RAX). 그 안에 들어가기에 충분히 작은 어떤 타입이라도 거기에 되돌아 갈 것입니다. 너무 큰 모든 유형의 경우 함수는 반환 결과를 어디에 배치할지 알려주는 숨겨진 포인터/참조 매개 변수를받습니다. RVO/NRVO를 전혀 사용하지 않으면이 정도가 적용된다는 사실에 유의하십시오. 사실 class 객체를 반환하는 C++과 마찬가지로 struct을 반환하는 C 코드에도 똑같이 적용됩니다. struct을 반환하는 것은 아마도 C++ 에서처럼 공통적 인 것은 아니지만 여전히 허용되며 컴파일러는 코드를 컴파일 할 수 있어야합니다.

실제로 제거 할 수있는 두 개의 (가능한) 사본이 있습니다. 하나는 컴파일러가 리턴 값이 될 로컬을위한 스택에 공간을 할당 한 다음 리턴하는 동안 포인터가 참조하는 곳으로 복사하는 것입니다.

두 번째는 반환 값에서 값이 실제로 끝나야하는 다른 위치로의 가능한 복사입니다.

첫 번째 기능은 기능 자체 내에서 제거되지만 외부 인터페이스에는 영향을 미치지 않습니다. 궁극적으로 숨겨진 포인터가 알 수있는 곳이라면 어디든지 데이터를 저장합니다. 유일한 문제는 로컬 복사본을 먼저 만들지, 아니면 항상 반환 지점과 직접 작업하는지 여부입니다. 분명히 [N] RVO는 항상 직접 작동합니다.

두 번째 가능한 복사본은 값이 실제로 끝날 필요가있는 곳으로 임시 (잠재) 임시 복사본입니다. 이것은 함수 자체가 아니라 호출 순서를 최적화함으로써 제거됩니다. 즉, 컴파일러가 값을 목적지로 복사 할 임시 위치가 아닌 함수가 반환 값의 최종 대상에 대한 포인터를 제공합니다. .

+0

제 질문에 대한 명확한 답을 보내 주셔서 감사합니다. 이제 구현 세부 사항을 알지 못하면 컴파일러는 반환 값이 저장되는 방법에 대한 전략을 결정할 수 있음을 올바르게 알았습니다. – Smg

5

main 구현 세부 사항이 필요하지 않습니다. getFoo RVO가 발생합니다. getFoo 종료 후에 반환 값이 일부 레지스터에있을 것으로 기대합니다.

getFoo이 두 옵션을 갖는다 - 그 범위의 객체를 생성하고 복사 (또는 이동) 복귀를 레지스터 또는 그 레지스터 에서 직접 객체를 생성한다. 어떤 일이 일어나는가?

다른 곳에서는보기가 주된 것이 아니며 필요하지도 않습니다. 그것은 단지 반환 레지스터를 직접 사용합니다.

+0

설명해 주셔서 감사합니다. 클래스 객체 크기를 반환 레지스터에 맞지 않는 크기로 확대하면 어떻게됩니까? 이 경우에도 RVO가 발생할 것으로 기대할 수 있습니까? – Smg

+0

@Smg 예, 아마도 포인터 또는 객체에 대한 참조를 반환합니다. 제리의 대답을 참조하십시오. –

2

(N) RVO는 번역 단위와 관련이 없습니다. 이 용어는 일반적으로 함수 내부 (로컬 변수에서 반환 값으로)와 호출자 (반환 값에서 로컬 변수로)에 적용될 수있는 두 개의 서로 다른 복사본을 나타 내기 위해 사용되며, 갈라져.

이 함수 내에서 엄격하게 행하는

RVO 적정 고려 :

T foo() { 
    T local; 
    // operate on local 
    return local; 
} 

개념적으로 두 개체 local 및 리턴 된 객체가있다. 컴파일러는 함수를 로컬에서 분석하여 두 객체의 수명이 바인딩되어 있는지 확인할 수 있습니다. local은 반환 값에 대한 복사본 소스로만 사용됩니다. 그런 다음 컴파일러는 두 변수를 단일 변수에 바인딩하여 사용할 수 있습니다. 발신자 측에서 발신자 측

에서

복사 생략는 T x = foo();을 고려한다. 다시 두 객체가 있는데, 반환 된 객체는 foo()x입니다. 그리고 컴파일러는 수명이 바운드되어 있는지 확인하고 두 개체를 같은 위치에 배치 할 수 있습니다.

는 또한 읽어

+0

원래 RVO는 호출자 측에서 반환 값을 처리하므로 번역 단위와 관련이 있어야한다고 생각했습니다. 하지만 지금은 번역 단위와 관련이 없다는 것을 이해할 수있었습니다. 그리고 내가 정확히 알고 싶었던 것을 충분히 설명하는 당신의 귀중한 출처에 감사드립니다. – Smg