C ++에서 std :: vector를 반환하는 효율적인 방법


108

함수에서 std :: vector를 반환 할 때 복사되는 데이터의 양과 std :: vector를 free-store (힙에)에 배치하고 대신 포인터를 반환하는 것이 최적화 될 것입니다.

std::vector *f()
{
  std::vector *result = new std::vector();
  /*
    Insert elements into result
  */
  return result;
} 

다음보다 더 효율적입니다.

std::vector f()
{
  std::vector result;
  /*
    Insert elements into result
  */
  return result;
} 

?


4
벡터를 참조로 전달한 다음 내부에 채우는 것은 f어떻습니까?
Kiril Kirov

4
RVO 는 대부분의 컴파일러가 언제든지 수행 할 수있는 매우 기본적인 최적화입니다.
Remus Rusanu 2013 년

답변이 들어 오면 C ++ 03을 사용하는지 C ++ 11을 사용하는지 명확히하는 데 도움이 될 수 있습니다. 두 버전 간의 모범 사례는 상당히 다릅니다.
Drew Dormann 2013 년


@Kiril Kirov, 함수의 인수 목록에 넣지 않고 할 수 있습니까? void f (std :: vector & result)?
Morten 2013 년

답변:


141

C ++ 11에서는 이것이 선호되는 방법입니다.

std::vector<X> f();

즉, 값으로 반환합니다.

C ++ 11에서는 std::vector이동 시맨틱이 있습니다. 즉, 함수에 선언 된 로컬 벡터가 반환시 이동 되며 경우에 따라 컴파일러에서 이동조차 제거 할 수 있습니다.


13
@LeonidVolnitsky : 로컬 인 경우 예 . 실제로 return std::move(v);.NET만으로 가능하더라도 이동 제거를 비활성화합니다 return v;. 따라서 후자가 선호됩니다.
Nawaz

1
@juanchopanza : 그렇게 생각하지 않습니다. C ++ 11 이전에는 벡터가 이동되지 않기 때문에 반대 할 수있었습니다. RVO는 컴파일러에 의존하는 것입니다! 80 년대와 90 년대의 것들에 대해 이야기하십시오.
Nawaz

2
반환 값 (값 기준)에 대한 나의 이해는 '이동 됨'대신 호출 수신자의 반환 값이 호출자의 스택에 생성되므로 호출 수신자의 모든 작업이 제자리에 있고 RVO에서 이동할 것이 없습니다. . 그 맞습니까?
r0ng

2
@ r0ng : 네, 사실입니다. 이것이 컴파일러가 일반적으로 RVO를 구현하는 방법입니다.
Nawaz

1
@Nawaz 그렇지 않습니다. 더 이상 움직임조차 없습니다.
궤도의 가벼운 경주

71

값으로 반환해야합니다.

이 표준에는 가치 반환의 효율성을 향상시키는 특정 기능이 있습니다. 이를 "복사 제거"라고하며 더 구체적으로이 경우에는 "명명 된 반환 값 최적화 (NRVO)"라고합니다.

컴파일러는 그것을 구현하지 않지만, 다시 컴파일러는하지 않습니다 인라인 기능을 구현하기 위해 (또는 전혀 최적화를 수행). 그러나 컴파일러가 최적화하지 않고 모든 심각한 컴파일러가 인라인 및 NRVO (및 기타 최적화)를 구현하면 표준 라이브러리의 성능이 상당히 떨어질 수 있습니다.

NRVO가 적용되면 다음 코드에 복사가 없습니다.

std::vector<int> f() {
    std::vector<int> result;
    ... populate the vector ...
    return result;
}

std::vector<int> myvec = f();

그러나 사용자는 이것을 원할 수 있습니다.

std::vector<int> myvec;
... some time later ...
myvec = f();

복사 제거는 초기화가 아닌 할당이기 때문에 여기서 복사를 방지하지 않습니다. 그러나 여전히 값으로 반환 해야 합니다. C ++ 11에서 할당은 "이동 의미론"이라는 다른 것에 의해 최적화됩니다. C ++ 03에서 위의 코드는 복사를 일으키며 이론적으로 는 최적화 프로그램이이를 피할 수 있지만 실제로는 너무 어렵습니다. 따라서 대신 myvec = f()C ++ 03에서 다음과 같이 작성해야합니다.

std::vector<int> myvec;
... some time later ...
f().swap(myvec);

사용자에게보다 유연한 인터페이스를 제공하는 또 다른 옵션이 있습니다.

template <typename OutputIterator> void f(OutputIterator it) {
    ... write elements to the iterator like this ...
    *it++ = 0;
    *it++ = 1;
}

그런 다음 기존 벡터 기반 인터페이스를 지원할 수도 있습니다.

std::vector<int> f() {
    std::vector<int> result;
    f(std::back_inserter(result));
    return result;
}

기존 코드가 고정 된 양보다 더 복잡한 방식으로 사용하는 경우 기존 코드보다 효율성 이 떨어질 수 있습니다 reserve(). 그러나 기존 코드가 기본적으로 push_back벡터를 반복적 으로 호출 하는 경우이 템플릿 기반 코드도 마찬가지로 좋습니다.


정말 최고이고 상세한 답변을 찬성했습니다. 그러나 스왑에서 () 변형 ( NRVO없이 C ++ 03에 대한 변수에서 : 당신은 여전히 하나의 복사 생성자가됩니다)는 () f를 내부에 만들어 복사 결과 로 스왑 마지막에있을 것입니다 숨겨진 임시 개체에 myvec .
JenyaKh

@JenyaKh : 물론, 그것은 구현 품질 문제입니다. 표준은 함수 인라인이 필요하지 않은 것처럼 C ++ 03 구현이 NRVO를 구현할 것을 요구하지 않았습니다. 함수 인라인과의 차이점은 인라인이 의미론이나 프로그램을 변경하지 않는 반면 NRVO는 변경하지 않는다는 것입니다. 이식 가능한 코드는 NRVO와 함께 또는없이 작동해야합니다. 특정 구현 (및 특정 컴파일러 플래그)에 대해 최적화 된 코드는 구현 자체 문서에서 NRVO에 대한 보증을 찾을 수 있습니다.
Steve Jessop

3

RVO에 대한 답변을 게시 할 때입니다. 저도 ...

값으로 객체를 반환하는 경우 컴파일러는 종종이를 최적화하여 두 번 생성되지 않도록합니다. 이는 함수에서 임시로 생성 한 다음 복사하는 것이 불필요하기 때문입니다. 이를 반환 값 최적화라고합니다. 생성 된 개체는 복사되는 대신 이동됩니다.


1

일반적인 C ++ 11 이전 관용구는 채워지는 객체에 대한 참조를 전달하는 것입니다.

그런 다음 벡터 복사가 없습니다.

void f( std::vector & result )
{
  /*
    Insert elements into result
  */
} 

3
그것은 더 이상 C ++ 11의 관용구가 아닙니다.
Nawaz

1
@Nawaz 동의합니다. C ++에 대한 질문과 관련하여 현재 모범 사례가 무엇인지 잘 모르겠지만 구체적으로 C ++ 11은 아닙니다. 나는 내가 학생에게 C ++ 11 답을주고, C ++ 03은 프로덕션 코드에있는 누군가에게 답을주고 싶어해야한다고 생각한다. 의견이 있으십니까?
Drew Dormann 2013 년

7
실제로 C ++ 11 (19 개월)이 출시 된 후, C ++ 03 질문으로 명시 적으로 언급되지 않는 한 모든 질문은 C ++ 11 질문으로 간주합니다.
Nawaz

1

컴파일러가 명명 된 반환 값 최적화 ( http://msdn.microsoft.com/en-us/library/ms364057(v=vs.80).aspx )를 지원하는 경우 다음이없는 경우 벡터를 직접 반환 할 수 있습니다.

  1. 다른 명명 된 개체를 반환하는 다른 경로
  2. EH 상태가 도입 된 여러 반환 경로 (모든 경로에서 동일한 명명 된 개체가 반환되는 경우에도).
  3. 반환 된 명명 된 객체는 인라인 asm 블록에서 참조됩니다.

NRVO는 중복 복사 생성자 및 소멸자 호출을 최적화하여 전반적인 성능을 향상시킵니다.

귀하의 예에는 실제 차이가 없어야합니다.


0
vector<string> getseq(char * db_file)

그리고 만약 당신이 그것을 main ()에 인쇄하고 싶다면 당신은 그것을 루프로해야합니다.

int main() {
     vector<string> str_vec = getseq(argv[1]);
     for(vector<string>::iterator it = str_vec.begin(); it != str_vec.end(); it++) {
         cout << *it << endl;
     }
}

-2

"값으로 반환"이 좋은 것처럼 오류로 이어질 수있는 종류의 코드입니다. 다음 프로그램을 고려하십시오.

    #include <string>
    #include <vector>
    #include <iostream>
    using namespace std;
    static std::vector<std::string> strings;
    std::vector<std::string> vecFunc(void) { return strings; };
    int main(int argc, char * argv[]){
      // set up the vector of strings to hold however
      // many strings the user provides on the command line
      for(int idx=1; (idx<argc); ++idx){
         strings.push_back(argv[idx]);
      }

      // now, iterate the strings and print them using the vector function
      // as accessor
      for(std::vector<std::string>::interator idx=vecFunc().begin(); (idx!=vecFunc().end()); ++idx){
         cout << "Addr: " << idx->c_str() << std::endl;
         cout << "Val:  " << *idx << std::endl;
      }
    return 0;
    };
  • Q : 위의 내용이 실행되면 어떻게 되나요? A : 코어 덤프.
  • Q : 컴파일러가 실수를 포착하지 못한 이유는 무엇입니까? A : 프로그램이 의미 론적으로는 아니지만 구문 론적으로 정확하기 때문입니다.
  • Q : 참조를 반환하도록 vecFunc ()를 수정하면 어떻게됩니까? A : 프로그램이 완료 될 때까지 실행되고 예상 결과가 생성됩니다.
  • Q : 차이점은 무엇입니까? A : 컴파일러는 익명 개체를 만들고 관리 할 필요가 없습니다. 프로그래머는 깨진 예제처럼 두 개의 다른 개체가 아닌 반복기와 끝점 결정에 정확히 하나의 개체를 사용하도록 컴파일러에 지시했습니다.

위의 잘못된 프로그램은 GNU g ++보고 옵션을 사용하더라도 오류가 없음을 나타냅니다. -Wall -Wextra -Weffc ++

값을 생성해야하는 경우 vecFunc ()를 두 번 호출하는 대신 다음이 작동합니다.

   std::vector<std::string> lclvec(vecFunc());
   for(std::vector<std::string>::iterator idx=lclvec.begin(); (idx!=lclvec.end()); ++idx)...

위의 코드는 루프를 반복하는 동안 익명의 객체를 생성하지 않지만 가능한 복사 작업이 필요합니다 (일부 상황에서 최적화 될 수 있음). 그러나 참조 메서드는 복사가 생성되지 않음을 보장합니다. 컴파일러가 믿으면 perform RVO는 당신이 할 수있는 가장 효율적인 코드를 만드는 것을 대체 할 수 없습니다. 컴파일러가 RVO를 수행 할 필요성을 의심 할 수 있다면, 당신은 게임보다 앞서 있습니다.


3
이것은 사용자가 일반적으로 C ++에 익숙하지 않은 경우 무엇이 잘못 될 수 있는지에 대한 예입니다. .net 또는 javascript와 같은 객체 기반 언어에 익숙한 사람은 문자열 벡터가 항상 포인터로 전달되므로 예제에서 항상 동일한 객체를 가리킬 것이라고 가정 할 것입니다. vecfunc (). begin () 및 vecfunc (). end ()는 문자열 벡터의 복사본이어야하므로 예제에서 반드시 일치하지는 않습니다.
Medran

-2
   vector<string> func1() const
   {
      vector<string> parts;
      return vector<string>(parts.begin(),parts.end()) ;
   } 
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.