C ++에서 "객체를 반환"하는 방법은 무엇입니까?


167

비슷한 질문이 많을수록 제목이 익숙한 것으로 알고 있지만 문제의 다른 측면을 요구하고 있습니다 (스택에 물건을 두는 것과 더미에 넣는 것의 차이점을 알고 있습니다).

Java에서는 항상 "로컬"객체에 대한 참조를 반환 할 수 있습니다

public Thing calculateThing() {
    Thing thing = new Thing();
    // do calculations and modify thing
    return thing;
}

C ++에서 비슷한 것을하기 위해 두 가지 옵션이 있습니다.

(1) 객체를 "반환"해야 할 때마다 참조를 사용할 수 있습니다

void calculateThing(Thing& thing) {
    // do calculations and modify thing
}

그런 다음 이렇게 사용하십시오

Thing thing;
calculateThing(thing);

(2) 또는 동적으로 할당 된 객체에 대한 포인터를 반환 할 수 있습니다

Thing* calculateThing() {
    Thing* thing(new Thing());
    // do calculations and modify thing
    return thing;
}

그런 다음 이렇게 사용하십시오

Thing* thing = calculateThing();
delete thing;

첫 번째 방법을 사용하면 수동으로 메모리를 비울 필요가 없지만 코드를 읽기가 어렵습니다. 두 번째 접근 방식의 문제점은을 기억해야한다는 것 delete thing;입니다. 비효율적이기 때문에 복사 된 값을 반환하고 싶지 않습니다 (제 생각에). 여기에 질문이 있습니다.

  • 세 번째 해결책이 있습니까 (값을 복사하지 않아도 됨)?
  • 첫 번째 해결책을 고수하면 문제가 있습니까?
  • 두 번째 솔루션을 언제, 왜 사용해야합니까?

32
질문을 멋지게 퍼트려 +1.
Kangkan

1
매우 pedantic하기 위해, "함수가 무엇인가를 돌려 준다"고 말하는 것은 약간 부정확합니다. 보다 정확하게 함수 호출을 평가하면 value가 생성 됩니다. void 함수가 아닌 한 값은 항상 객체입니다. 값이 glvalue인지 prvalue인지 여부는 선언 된 반환 유형 이 참조 인지 여부에 따라 결정됩니다 .
Kerrek SB

답변:


107

비효율적이므로 복사 된 값을 반환하고 싶지 않습니다.

증명 해봐

RVO 및 NRVO 및 C ++ 0x 이동 시맨틱을 검색하십시오. 대부분의 경우 C ++ 03에서 out 매개 변수는 코드를 추악하게 만드는 좋은 방법 일 뿐이며 C ++ 0x에서는 out 매개 변수를 사용하여 실제로 상처를 입을 수 있습니다.

깨끗한 코드를 작성하고 값으로 반환하십시오. 성능에 문제가 있으면 프로파일 링 (추측 중지)하고 수정을 위해 수행 할 수있는 작업을 찾으십시오. 아마도 함수에서 물건을 반환하지 않을 것입니다.


즉, 그런 식으로 글을 쓰지 않으면 out 매개 변수를 사용하고 싶을 것입니다. 보다 안전하고 일반적으로 더 빠른 동적 메모리 할당을 피합니다. 함수를 호출하기 전에 객체를 생성 할 수있는 방법이 필요하지만 모든 객체에 항상 적합한 것은 아닙니다.

동적 할당을 사용하려면 수행 할 수있는 최소한의 방법을 스마트 포인터에 넣으십시오. (이것은 어쨌든 항상 수행되어야합니다.) 그러면 아무것도 삭제하는 것에 대해 걱정하지 않아도됩니다. 예외가 안전합니다. 유일한 문제는 어쨌든 값으로 반환하는 것보다 느릴 것입니다!


10
@ phunehehe : 포인트가 추측되지 않습니다. 코드를 프로파일 링하고 찾아야합니다. (힌트 : 아니오) 컴파일러는 매우 영리하며, 필요하지 않은 경우 주변을 복사하는 데 시간을 낭비하지 않습니다. 복사에 비용 들더 라도 빠른 코드보다 우수한 코드를 만들기 위해 노력해야합니다. 좋은 코드는 속도가 문제가 될 때 최적화하기 쉽습니다. 당신이 모르는 것에 대한 코드를 추악하게 만드는 것은 아무 문제가 없습니다. 특히 실제로 속도를 늦추거나 아무 것도 얻지 못하는 경우. 그리고 C ++ 0x를 사용하는 경우 이동 시맨틱은 문제가되지 않습니다.
GManNickG

1
@GMan, re : RVO : 실제로 이것은 발신자와 수신자가 동일한 컴파일 단위에있는 경우에만 적용됩니다. 실제 세계에서는 그렇지 않습니다. 따라서 코드가 모두 템플릿 화되지 않은 경우 (이 경우 모두 하나의 컴파일 단위에 있음) 링크 시간 최적화가 적용되는 경우 (GCC는 4.5부터 만) 실망합니다.
Alex B

2
@Alex : 여러 번역 단위에서 컴파일러 최적화가 향상되고 있습니다. (VC는 현재 여러 릴리스에 대해이를 수행합니다.)
sbi

9
@Alex B : 이것은 완전한 쓰레기입니다. 많은 매우 일반적인 호출 규칙은 호출자가 큰 반환 값을위한 공간을 할당하고 호출자는 구성을 담당합니다. RVO는 링크 시간 최적화 없이도 컴파일 단위에서 행복하게 작동합니다.
CB Bailey

6
@Charles, 확인하면 올바른 것 같습니다! 명확하게 잘못 알고있는 진술을 철회합니다.
Alex B

41

객체를 생성하고 반환하십시오.

Thing calculateThing() {
    Thing thing;
    // do calculations and modify thing
     return thing;
}

최적화를 잊고 읽을 수있는 코드를 작성하면 나중에 호의를 보일 것이라고 생각합니다 (나중에 프로파일 러를 실행해야하지만 사전 최적화하지는 마십시오).


2
Thing thing();로컬 함수를 선언하고를 반환합니다 Thing.
dreamlax

2
Thing thing ()은 것을 반환하는 함수를 선언합니다. 함수 본문에 구성된 Thing 객체가 없습니다.
CB Bailey

@dreamlax @Charles @GMan 조금 늦었지만 수정되었습니다.
Amir Rachum

이 작업은 C ++ 98에서 어떻게 작동합니까? CINT 인터프리터에서 오류가 발생하고 C ++ 98 또는 CINT 자체 때문인지 궁금합니다 ...!
xcorat

16

다음과 같은 객체를 반환하십시오.

Thing calculateThing() 
{
   Thing thing();
   // do calculations and modify thing
   return thing;
}

이것은 Things에 대한 복사 생성자를 호출하므로 직접 구현할 수 있습니다. 이처럼 :

Thing(const Thing& aThing) {}

약간 느리게 수행 될 수 있지만 전혀 문제가되지 않을 수 있습니다.

최신 정보

컴파일러는 아마도 복사 생성자에 대한 호출을 최적화하므로 추가 오버 헤드가 없습니다. (dreamlax와 마찬가지로 의견에서 지적했습니다).


9
Thing thing();를 반환하는 로컬 함수를 선언합니다 Thing. 또한 표준은 컴파일러가 제시 한 경우 복사 생성자를 생략 할 수 있도록합니다. 현대의 컴파일러라면 아마 그렇게 할 것입니다.
dreamlax

1
특히 딥 카피가 필요한 경우 카피 생성자를 구현하는 데 도움이됩니다.
mbadawi23

@dreamlax가 말했듯이 컴파일러는 복사 생성자에 대한 실제 호출을 피하면서 함수에 대한 반환 코드를 "최적화"할 것입니다.
jose.angel.jimenez

2018 년 VS 2017에서는 이동 생성자를 사용하려고합니다. 이동 생성자가 삭제되고 복사 생성자가 없으면 컴파일되지 않습니다.
앤드류

11

auto_ptr과 같이 스마트 포인터를 사용하려고 했습니까 (Thing이 실제로 크고 무거운 개체 인 경우).


std::auto_ptr<Thing> calculateThing()
{
  std::auto_ptr<Thing> thing(new Thing);
  // .. some calculations
  return thing;
}


// ...
{
  std::auto_ptr<Thing> thing = calculateThing();
  // working with thing

  // auto_ptr frees thing 
}

4
auto_ptrs는 더 이상 사용되지 않습니다. 사용 shared_ptr또는 unique_ptr대신.
MBraedley

여기에 이것을 추가 할 것입니다 ... 나는 C ++을 사용하여 전문적으로는 아니지만 수년 동안 C ++을 사용해 왔습니다. 더 이상 스마트 포인터를 사용하지 않기로 결심했습니다. 그들은 절대 엉망입니다. 코드의 속도를 크게 높이는 데 도움이되지 않는 일종의 문제. RAII를 사용하여 데이터를 복사하고 포인터를 직접 관리하는 것이 좋습니다. 따라서 가능하면 스마트 포인터를 사용하지 않는 것이 좋습니다.
앤드류

8

복사 생성자가 호출되는지 확인하는 빠른 방법 중 하나는 클래스의 복사 생성자에 로깅을 추가하는 것입니다.

MyClass::MyClass(const MyClass &other)
{
    std::cout << "Copy constructor was called" << std::endl;
}

MyClass someFunction()
{
    MyClass dummy;
    return dummy;
}

전화 someFunction; 얻을 수있는 "복사 생성자 호출"행의 수는 0, 1 및 2 사이에서 다양합니다. 아무 것도 얻지 못하면 컴파일러는 반환 값을 최적화했습니다 (허용 된 값). 0을하지 않는 얻을, 당신의 복사 생성자가 터무니없이 비싼 경우, 다음 당신의 기능에서 인스턴스를 반환하는 다른 방법을 검색합니다.


1

첫째로 당신은 코드에서 오류를 가지고, 당신은 의미 Thing *thing(new Thing());만, return thing;.

  • 사용하십시오 shared_ptr<Thing>. 포인터라고 간주하십시오. 포함 된 항목에 대한 마지막 참조 Thing가 범위를 벗어나면 삭제됩니다 .
  • 첫 번째 솔루션은 순진한 라이브러리에서 매우 일반적입니다. 그것은 약간의 성능과 구문상의 오버 헤드를 가지고 있으며 가능하면 피하십시오
  • 예외가 발생하지 않거나 성능이 절대적으로 중요하지 않은 경우에만 두 번째 솔루션을 사용하십시오 (이와 관련되기 전에 C 또는 어셈블리와 인터페이스합니다).

0

C ++ 전문가가 더 나은 답변을 얻을 것이라고 확신하지만 개인적으로 두 번째 접근법을 좋아합니다. 스마트 포인터를 사용하면 잊어 버리는 문제를 피하는 데 도움이됩니다. delete손으로 미리 객체를 생성 해야하는 것보다 더 깔끔해 보입니다 (그리고 힙에 할당하려는 경우 여전히 삭제해야 함).

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.