C ++에서 함수에서 벡터를 반환하는 것은 여전히 ​​나쁜 습관입니까?


103

짧은 버전 : 많은 프로그래밍 언어에서 벡터 / 배열과 같은 큰 개체를 반환하는 것이 일반적입니다. 클래스에 이동 생성자가있는 경우이 스타일이 이제 C ++ 0x에서 허용됩니까? 아니면 C ++ 프로그래머가 이상 / 추악 / 혐오스러운 것으로 간주합니까?

긴 버전 : C ++ 0x에서 여전히 잘못된 형식으로 간주됩니까?

std::vector<std::string> BuildLargeVector();
...
std::vector<std::string> v = BuildLargeVector();

기존 버전은 다음과 같습니다.

void BuildLargeVector(std::vector<std::string>& result);
...
std::vector<std::string> v;
BuildLargeVector(v);

최신 버전에서 반환 된 값 BuildLargeVector은 rvalue이므로 std::vector(N) RVO가 발생하지 않는다고 가정 하고의 이동 생성자를 사용하여 v가 생성됩니다 .

C ++ 0x 이전에도 첫 번째 형식은 (N) RVO 때문에 종종 "효율적"이었습니다. 그러나 (N) RVO는 컴파일러의 재량에 달려 있습니다. 이제 우리를 rvalue 참조를 가지고 그것을됩니다 보장 더 깊은 복사가 발생하지 것이다.

편집 : 질문은 실제로 최적화에 관한 것이 아닙니다. 표시된 두 형식 모두 실제 프로그램에서 거의 동일한 성능을 보입니다. 반면, 과거에는 첫 번째 형식이 성능이 훨씬 더 나쁠 수있었습니다. 그 결과 첫 번째 형태는 오랫동안 C ++ 프로그래밍에서 주요 코드 냄새였습니다. 더 이상은 아니겠습니까?


18
처음 시작하는 것이 나쁜 형태라고 누가 말했습니까?
Edward Strange

7
제가 출신 인 "오래된 시절"에는 확실히 나쁜 코드 냄새였습니다. :-)
Nate

1
나는 그렇게 희망한다! 가치 별 통과가 점점 더 인기를 얻고 싶습니다. :)
sellibitze

답변:


73

Dave Abrahams는 값 전달 / 반환 속도에 대해 매우 포괄적 인 분석을 제공 합니다 .

짧은 대답, 값을 반환해야하는 경우 값을 반환합니다. 어쨌든 컴파일러가 수행하므로 출력 참조를 사용하지 마십시오. 물론주의 사항이 있으므로 해당 기사를 읽어야합니다.


24
"컴파일러는 어쨌든 그것을한다": 컴파일러는 그렇게하기 위해 필요하지 않다 == 불확실성 == 나쁜 생각 (100 % 확실성 필요). "포괄적 분석"그 분석에는 큰 문제가 있습니다. 알 수없는 컴파일러의 문서화되지 않은 / 비표준 언어 기능에 의존합니다 ( "표준에서는 복사 제거가 절대 필요하지 않음"). 따라서 작동하더라도 사용하는 것은 좋은 생각이 아닙니다. 의도 한대로 작동 할 것이라는 보장은 전혀 없으며 모든 컴파일러가 항상 이런 방식으로 작동 할 것이라는 보장도 없습니다. 이 문서에 의존하는 것은 나쁜 코딩 관행입니다. 성능이 저하 되더라도.
SigTerm

5
@SigTerm : 훌륭한 댓글입니다 !!! 참조 된 문서의 대부분은 프로덕션에 사용하기 위해 고려하기에는 너무 모호합니다. 사람들은 Red In-Depth 책을 쓴 작가가 복음이라고 생각하며 더 이상 생각이나 분석없이 고수해야합니다. ATM은 아브라함이 기사에서 사용한 예제만큼 다양한 카피 엘리슨을 제공하는 컴파일러가 시장에 없습니다.
Hippicoder 2010-06-28

13
@SigTerm, 컴파일러가 수행 할 필요가없는 것이 많이 있지만 어쨌든 그렇게한다고 가정합니다. 컴파일러는 변화 "요구"하지 않습니다 x / 2x >> 1대한 int의,하지만 당신은 것입니다 가정합니다. 표준은 또한 컴파일러가 참조를 구현하는 데 필요한 방법에 대해 언급하지 않지만 포인터를 사용하여 효율적으로 처리된다고 가정합니다. 표준은 또한 v-tables에 대해 아무것도 말하지 않기 때문에 가상 함수 호출이 효율적이라고 확신 할 수 없습니다. 본질적으로, 때때로 컴파일러를 신뢰해야합니다.
Peter Alexander

16
@Sig : 프로그램의 실제 출력을 제외하고는 실제로 거의 보장되지 않습니다. 어떤 일이 일어날 지 100 % 확실하게 알고 싶다면 다른 언어로 완전히 전환하는 것이 좋습니다.
Dennis Zickefoose

6
@SigTerm : "실제 시나리오"작업을합니다. 컴파일러가하는 일을 테스트하고 그 작업을합니다. "더 느리게 작동 할 수 있음"은 없습니다. 표준에서 요구하는지 여부에 관계없이 컴파일러가 RVO를 구현하기 때문에 느리게 작동하지 않습니다. ifs, buts 또는 maybes는 없습니다. 단순한 사실입니다.
Peter Alexander

37

적어도 IMO는 일반적으로 좋지 않지만 효율성상의 이유로는 아닙니다 . 문제의 함수는 일반적으로 반복기를 통해 출력을 생성하는 일반 알고리즘으로 작성되어야하므로 좋지 않습니다. 반복기에서 작동하는 대신 컨테이너를 수락하거나 반환하는 거의 모든 코드는 의심스러운 것으로 간주되어야합니다.

오해하지 마세요. 컬렉션과 같은 객체 (예 : 문자열)를 전달하는 것이 합리적 일 때가 있지만 인용 된 예에서는 벡터를 전달하거나 반환하는 것이 좋지 않은 생각입니다.


6
반복기 접근 방식의 문제점은 컬렉션 요소 유형이 알려진 경우에도 함수와 메서드를 템플릿 화해야한다는 것입니다. 이것은 짜증스럽고 문제의 방법이 가상이면 불가능합니다. 나는 당신의 대답 자체에 동의하지 않지만 실제로는 C ++에서 약간 번거 롭습니다.
jon-hanson 2010-06-28

22
동의하지 않습니다. 출력을 위해 반복기를 사용하는 것이 때때로 적절하지만, 일반 알고리즘을 작성하지 않는 경우 일반 솔루션은 종종 정당화하기 어려운 피할 수없는 오버 헤드를 제공합니다. 코드 복잡성과 실제 성능 측면에서.
Dennis Zickefoose

1
@Dennis : 내 경험이 정반대라고 말해야합니다. 미리 관련 유형을 알고 있어도 템플릿으로 많은 것을 작성합니다. 왜냐하면 그렇게하는 것이 더 간단하고 성능을 향상시키기 때문입니다.
Jerry Coffin

9
나는 개인적으로 용기를 반환합니다. 의도는 명확하고 코드가 더 쉬우 며 작성시 성능에 크게 신경 쓰지 않습니다 (초기 비관 화는 피합니다). 출력 반복기를 사용하면 내 의도가 더 명확 해 질지 확실하지 않습니다. 큰 프로젝트 종속성에서 개발이 중단되기 때문에 가능한 한 템플릿이 아닌 코드가 필요합니다.
Matthieu M.

1
@Dennis : 개념적 으로 "범위에 쓰기보다는 컨테이너를 구축" 해서는 안됩니다 . 컨테이너는 바로 컨테이너입니다. 귀하의 우려 (및 코드의 우려)는 컨테이너가 아닌 내용에 있어야합니다.
Jerry Coffin

18

요점은 다음과 같습니다.

Copy Elision 및 RVO "무서운 복사본"을 피할 수 있습니다 (컴파일러는 이러한 최적화를 구현하는 데 필요하지 않으며 일부 상황에서는 적용 할 수 없습니다).

C ++ 0x RValue 참조 이를 보장 하는 문자열 / 벡터 구현을 허용 합니다.

오래된 컴파일러 / STL 구현을 포기할 수 있다면 벡터를 자유롭게 반환하십시오 (그리고 자신의 개체도이를 지원하는지 확인하십시오). 코드베이스가 "더 적은"컴파일러를 지원해야하는 경우 이전 스타일을 고수하십시오.

불행히도 이는 인터페이스에 큰 영향을 미칩니다. C ++ 0x가 옵션이 아니고 보증이 필요한 경우 일부 시나리오에서 참조 카운트 또는 쓰기시 복사 객체를 대신 사용할 수 있습니다. 하지만 멀티 스레딩에는 단점이 있습니다.

(C ++에서 단 하나의 답변이 조건없이 간단하고 간단하기를 바랍니다.)


11

사실, C ++ (11) 이후의 비용 복사std::vector대부분의 경우에 사라 졌어요.

그러나 새로운 벡터 를 생성 하는 비용 (그런 다음이를 파괴 )은 여전히 ​​존재하며, 벡터의 용량을 재사용하려는 경우 값으로 반환하는 대신 출력 매개 변수를 사용하는 것이 여전히 유용하다는 점을 명심해야합니다 . 이것은 C ++ 핵심 지침의 F.20 에 예외로 문서화되어 있습니다.

비교해 봅시다 :

std::vector<int> BuildLargeVector1(size_t vecSize) {
    return std::vector<int>(vecSize, 1);
}

와:

void BuildLargeVector2(/*out*/ std::vector<int>& v, size_t vecSize) {
    v.assign(vecSize, 1);
}

이제이 메서드를 numIter타이트 루프에서 호출 하고 몇 가지 작업을 수행 해야한다고 가정 합니다. 예를 들어 모든 요소의 합을 계산해 봅시다.

를 사용 BuildLargeVector1하면 다음을 수행 할 수 있습니다.

size_t sum1 = 0;
for (int i = 0; i < numIter; ++i) {
    std::vector<int> v = BuildLargeVector1(vecSize);
    sum1 = std::accumulate(v.begin(), v.end(), sum1);
}

를 사용 BuildLargeVector2하면 다음을 수행 할 수 있습니다.

size_t sum2 = 0;
std::vector<int> v;
for (int i = 0; i < numIter; ++i) {
    BuildLargeVector2(/*out*/ v, vecSize);
    sum2 = std::accumulate(v.begin(), v.end(), sum2);
}

첫 번째 예에서는 불필요한 동적 할당 / 할당 해제가 많이 발생하며, 두 번째 예에서는 이전 방식으로 이미 할당 된 메모리를 재사용하여 출력 매개 변수를 사용하여 방지합니다. 이 최적화가 가치가 있는지 여부는 값을 계산 / 변이하는 비용과 비교하여 할당 / 할당 해제의 상대적 비용에 따라 다릅니다.

기준

vecSize및 의 값을 가지고 놀아 봅시다 numIter. vecSize * numIter를 일정하게 유지하여 "이론상"동일한 시간이 걸리고 (= 정확히 동일한 값을 가진 동일한 수의 할당 및 추가가 있음) 시간 차이는 다음 비용에서만 발생할 수 있습니다. 할당, 할당 해제 및 캐시의 더 나은 사용.

더 구체적으로, vecSize * numIter = 2 ^ 31 = 2147483648을 사용하겠습니다. 왜냐하면 16GB의 RAM이 있고이 숫자는 8GB 이하가 할당되도록 보장하기 때문입니다 (sizeof (int) = 4). 다른 모든 프로그램은 닫 혔고 테스트를 실행할 때 15GB를 사용할 수있었습니다.)

다음은 코드입니다.

#include <chrono>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <vector>

class Timer {
    using clock = std::chrono::steady_clock;
    using seconds = std::chrono::duration<double>;
    clock::time_point t_;

public:
    void tic() { t_ = clock::now(); }
    double toc() const { return seconds(clock::now() - t_).count(); }
};

std::vector<int> BuildLargeVector1(size_t vecSize) {
    return std::vector<int>(vecSize, 1);
}

void BuildLargeVector2(/*out*/ std::vector<int>& v, size_t vecSize) {
    v.assign(vecSize, 1);
}

int main() {
    Timer t;

    size_t vecSize = size_t(1) << 31;
    size_t numIter = 1;

    std::cout << std::setw(10) << "vecSize" << ", "
              << std::setw(10) << "numIter" << ", "
              << std::setw(10) << "time1" << ", "
              << std::setw(10) << "time2" << ", "
              << std::setw(10) << "sum1" << ", "
              << std::setw(10) << "sum2" << "\n";

    while (vecSize > 0) {

        t.tic();
        size_t sum1 = 0;
        {
            for (int i = 0; i < numIter; ++i) {
                std::vector<int> v = BuildLargeVector1(vecSize);
                sum1 = std::accumulate(v.begin(), v.end(), sum1);
            }
        }
        double time1 = t.toc();

        t.tic();
        size_t sum2 = 0;
        {
            std::vector<int> v;
            for (int i = 0; i < numIter; ++i) {
                BuildLargeVector2(/*out*/ v, vecSize);
                sum2 = std::accumulate(v.begin(), v.end(), sum2);
            }
        } // deallocate v
        double time2 = t.toc();

        std::cout << std::setw(10) << vecSize << ", "
                  << std::setw(10) << numIter << ", "
                  << std::setw(10) << std::fixed << time1 << ", "
                  << std::setw(10) << std::fixed << time2 << ", "
                  << std::setw(10) << sum1 << ", "
                  << std::setw(10) << sum2 << "\n";

        vecSize /= 2;
        numIter *= 2;
    }

    return 0;
}

결과는 다음과 같습니다.

$ g++ -std=c++11 -O3 main.cpp && ./a.out
   vecSize,    numIter,      time1,      time2,       sum1,       sum2
2147483648,          1,   2.360384,   2.356355, 2147483648, 2147483648
1073741824,          2,   2.365807,   1.732609, 2147483648, 2147483648
 536870912,          4,   2.373231,   1.420104, 2147483648, 2147483648
 268435456,          8,   2.383480,   1.261789, 2147483648, 2147483648
 134217728,         16,   2.395904,   1.179340, 2147483648, 2147483648
  67108864,         32,   2.408513,   1.131662, 2147483648, 2147483648
  33554432,         64,   2.416114,   1.097719, 2147483648, 2147483648
  16777216,        128,   2.431061,   1.060238, 2147483648, 2147483648
   8388608,        256,   2.448200,   0.998743, 2147483648, 2147483648
   4194304,        512,   0.884540,   0.875196, 2147483648, 2147483648
   2097152,       1024,   0.712911,   0.716124, 2147483648, 2147483648
   1048576,       2048,   0.552157,   0.603028, 2147483648, 2147483648
    524288,       4096,   0.549749,   0.602881, 2147483648, 2147483648
    262144,       8192,   0.547767,   0.604248, 2147483648, 2147483648
    131072,      16384,   0.537548,   0.603802, 2147483648, 2147483648
     65536,      32768,   0.524037,   0.600768, 2147483648, 2147483648
     32768,      65536,   0.526727,   0.598521, 2147483648, 2147483648
     16384,     131072,   0.515227,   0.599254, 2147483648, 2147483648
      8192,     262144,   0.540541,   0.600642, 2147483648, 2147483648
      4096,     524288,   0.495638,   0.603396, 2147483648, 2147483648
      2048,    1048576,   0.512905,   0.609594, 2147483648, 2147483648
      1024,    2097152,   0.548257,   0.622393, 2147483648, 2147483648
       512,    4194304,   0.616906,   0.647442, 2147483648, 2147483648
       256,    8388608,   0.571628,   0.629563, 2147483648, 2147483648
       128,   16777216,   0.846666,   0.657051, 2147483648, 2147483648
        64,   33554432,   0.853286,   0.724897, 2147483648, 2147483648
        32,   67108864,   1.232520,   0.851337, 2147483648, 2147483648
        16,  134217728,   1.982755,   1.079628, 2147483648, 2147483648
         8,  268435456,   3.483588,   1.673199, 2147483648, 2147483648
         4,  536870912,   5.724022,   2.150334, 2147483648, 2147483648
         2, 1073741824,  10.285453,   3.583777, 2147483648, 2147483648
         1, 2147483648,  20.552860,   6.214054, 2147483648, 2147483648

벤치 마크 결과

(Intel i7-7700K @ 4.20GHz, 16GB DDR4 2400Mhz, Kubuntu 18.04)

표기법 : mem (v) = v.size () * sizeof (int) = v.size () * 4 on my platform.

당연히 numIter = 1(즉, mem (v) = 8GB) 시간이 완벽하게 동일합니다. 실제로 두 경우 모두 메모리에 8GB의 거대한 벡터를 한 번만 할당합니다. 이것은 또한 BuildLargeVector1 ()을 사용할 때 복사가 발생하지 않았 음을 증명합니다. 복사를 수행하기에 충분한 RAM이 없을 것입니다!

numIter = 2경우 두 번째 벡터를 다시 할당하는 대신 벡터 용량을 재사용하면 1.37 배 더 빠릅니다.

경우 numIter = 256(... 반복해서 256 회 벡터를 할당 해제 / 대신 할당) 벡터 용량을 재사용하는 것은 빠른 2.45x이다 :

우리는 시간 1가에서 거의 일정한 것을 알 수 있습니다 numIter = 1numIter = 2568기가바이트 중 하나 큰 벡터를 할당하는 것은 거의 32메가바이트의 256 벡터를 할당하는만큼 비용이 많이 드는 것을 의미한다. 그러나 8GB의 거대한 벡터 하나를 할당하는 것은 32MB의 벡터 하나를 할당하는 것보다 확실히 더 비싸므로 벡터의 용량을 재사용하면 성능이 향상됩니다.

에서 numIter = 512(MEM (V) = 16메가바이트)에 numIter = 8M(MEM은 (V) = 1KB) 달콤한 장소입니다 : 두 가지 방법이 빠르게 numIter 및 vecSize의 모든 다른 조합보다 더 빨리 정확하게, 그리고. 이것은 아마도 내 프로세서의 L3 캐시 크기가 8MB라는 사실과 관련이 있으므로 벡터가 캐시에 거의 완벽하게 맞습니다. 나는 time1mem (v) = 16MB 의 갑작스러운 점프가 왜 mem (v) = 8MB 인 직후에 일어나는 것이 더 논리적으로 보일지 설명하지 않습니다 . 놀랍게도이 스위트 스팟에서는 용량을 재사용하지 않는 것이 실제로 약간 더 빠릅니다! 나는 이것을 정말로 설명하지 않는다.

numIter > 8M상황이 추악 해지기 시작할 때 . 두 방법 모두 느려지지만 값으로 벡터를 반환하는 것은 더 느려집니다. 최악의 경우 단일 단일을 포함하는 벡터의 경우 int값으로 반환하는 대신 용량을 재사용하는 것이 3.3 배 더 빠릅니다. 아마도 이것은 지배하기 시작하는 malloc ()의 고정 비용 때문일 것입니다.

time2에 대한 곡선이 time1에 대한 곡선보다 더 매끄럽다는 점에 유의하십시오. 벡터 용량을 재사용하는 것이 일반적으로 더 빠를뿐만 아니라 더 중요한 것은 더 예측 가능 하다는 것입니다 .

또한 스윗 스팟에서는 ~ 0.5 초 내에 20 억 개의 64 비트 정수를 추가 할 수 있었는데, 이는 4.2Ghz 64 비트 프로세서에서 매우 최적입니다. 8 개의 코어를 모두 사용하기 위해 계산을 병렬화하면 더 잘할 수 있습니다 (위의 테스트에서는 한 번에 하나의 코어 만 사용하며 CPU 사용량을 모니터링하면서 테스트를 다시 실행하여 확인했습니다). 최고의 성능은 mem (v) = 16kB 일 때 달성되며, 이는 L1 캐시의 크기입니다 (i7-7700K의 L1 데이터 캐시는 4x32kB).

물론 데이터에 대해 실제로 수행해야하는 계산이 많을수록 차이는 점점 더 관련성이 낮아집니다. 우리가 대체 할 경우 아래의 결과입니다 sum = std::accumulate(v.begin(), v.end(), sum);으로 for (int k : v) sum += std::sqrt(2.0*k);:

벤치 마크 2

결론

  1. 값으로 반환하는 대신 출력 매개 변수를 사용하면 용량을 재사용하여 성능을 향상 시킬 수 있습니다 .
  2. 최신 데스크톱 컴퓨터에서는 큰 벡터 (> 16MB)와 작은 벡터 (<1kB)에만 적용 할 수 있습니다.
  3. 수백만 / 십억 개의 작은 벡터 (<1kB)를 할당하지 마십시오. 가능하면 용량을 재사용하거나 더 나은 방법으로 아키텍처를 다르게 설계하십시오.

다른 플랫폼에서는 결과가 다를 수 있습니다. 평소처럼 성능이 중요한 경우 특정 사용 사례에 대한 벤치 마크를 작성하십시오.


6

여전히 나쁜 습관이라고 생각하지만 우리 팀이 MSVC 2008 및 GCC 4.1을 사용하고 있으므로 최신 컴파일러를 사용하지 않는다는 점에 주목할 가치가 있습니다.

이전에는 MSVC 2008과 함께 vtune에 표시된 많은 핫스팟이 문자열 복사로 내려갔습니다. 다음과 같은 코드가 있습니다.

String Something::id() const
{
    return valid() ? m_id: "";
}

... 우리는 자체 String 유형을 사용했습니다 (플러그인 작성자가 다른 컴파일러를 사용할 수있는 소프트웨어 개발 키트를 제공하므로 std :: string / std :: wstring의 서로 다른 호환되지 않는 구현을 제공하기 때문에 필요했습니다).

String :: String (const String &)이 상당한 시간을 차지하는 것을 보여주는 호출 그래프 샘플링 프로파일 링 세션에 대한 응답으로 간단한 변경을 수행했습니다. 위의 예에서와 같은 방법은 가장 큰 기여자였습니다 (실제로 프로파일 링 세션은 메모리 할당 및 할당 해제가 가장 큰 핫스팟 중 하나로 표시되었으며 String 복사 생성자가 할당의 주요 기여자입니다).

내가 만든 변경은 간단했습니다.

static String null_string;
const String& Something::id() const
{
    return valid() ? m_id: null_string;
}

그러나 이것은 차이의 세계를 만들었습니다! 핫스팟은 후속 프로파일 러 세션에서 사라졌고이 외에도 애플리케이션 성능을 추적하기 위해 많은 철저한 단위 테스트를 수행합니다. 이러한 간단한 변경 이후 모든 종류의 성능 테스트 시간이 크게 단축되었습니다.

결론 : 우리는 절대적인 최신 컴파일러를 사용하고 있지 않지만, 안정적으로 값을 반환하기 위해 복사를 최적화하는 컴파일러에 의존 할 수없는 것 같습니다 (적어도 모든 경우에). MSVC 2010과 같은 최신 컴파일러를 사용하는 사람들에게는 그렇지 않을 수 있습니다. C ++ 0x를 사용하고 rvalue 참조를 사용할 수있을 때를 고대하고 있으며 복잡한 코드를 반환하여 코드를 비관적으로 사용하는 것에 대해 걱정할 필요가 없습니다. 가치에 의한 클래스.

[편집] Nate가 지적했듯이 RVO는 함수 내부에서 생성 된 임시 반환에 적용됩니다. 제 경우에는 그러한 임시 (빈 문자열을 구성하는 유효하지 않은 분기 제외)가 없었으므로 RVO를 적용 할 수 없었을 것입니다.


3
RVO는 컴파일러에 따라 다르지만 C ++ 0x 컴파일러 RVO를 사용하지 않기로 결정한 경우 이동 의미 체계를 사용해야합니다 (이동 생성자가 있다고 가정). trigraph 연산자를 사용하면 RVO가 무효화됩니다. Peter가 언급 한 cpp-next.com/archive/2009/09/move-it-with-rvalue-references 를 참조하십시오 . 그러나 일시적인 것을 반환하지 않기 때문에 귀하의 예제는 어쨌든 이동 의미 체계에 적합하지 않습니다.
Nate

@ Stinky472 : 값으로 멤버를 반환하는 것은 항상 참조보다 느립니다. Rvalue 참조는 원래 멤버에 대한 참조를 반환하는 것보다 여전히 느립니다 (호출자가 복사본이 필요하지 않은 참조를받을 수있는 경우). 또한 컨텍스트가 있기 때문에 rvalue 참조를 통해 저장할 수있는 횟수가 여전히 많습니다. 예를 들어, String newstring을 수행 할 수 있습니다. newstring.resize (string1.size () + string2.size () + ...); 새 문자열 + = string1; newstring + = string2; 등. 이것은 여전히 ​​rvalues에 비해 상당한 절약입니다.
Puppy

@DeadMG는 RVO를 구현하는 C ++ 0x 컴파일러에서도 이진 연산자 +에 비해 상당한 절감 효과가 있습니까? 그렇다면 그것은 부끄러운 일입니다. 그런 다음 다시 연결 문자열을 계산하기 위해 임시를 생성해야하는 반면 + =는 newstring에 직접 연결할 수 있기 때문에 다시 의미가 있습니다.
stinky472 2010-06-28

다음과 같은 경우는 어떻습니까? string newstr = str1 + str2; 이동 시맨틱을 구현하는 컴파일러에서는 다음과 같거나 더 빠를 것 같습니다. string newstr; newstr + = str1; newstr + = str2; 즉, 예약하지 않습니다 (크기 조정 대신 예약을 의미한다고 가정합니다).
stinky472 2010-06-28

5
@Nate : 나는 당신이 혼란 생각 trigraph를을 같이 <::또는 ??!조건 연산자 ?: (때로는라는 삼항 연산자를 ).
fredoverflow 2010-06-28

3

간단히 말해서, 많은 프로그래밍 언어에서 함수에서 배열을 반환하는 것은 일반적이지 않습니다. 대부분의 경우 배열에 대한 참조가 반환됩니다. C ++에서 가장 가까운 비유는 다음과 같습니다.boost::shared_array


4
@Billy : std :: vector는 복사 의미가있는 값 유형입니다. 현재 C ++ 표준은 (N) RVO가 적용된다는 보장을 제공하지 않으며 실제로 적용되지 않는 실제 시나리오가 많이 있습니다.
Nemanja Trifunovic

3
@Billy : 다시, 아주 실제 시나리오도 최신 컴파일러 NRVO을 적용하지 않는 곳이 있습니다 : efnetcpp.org/wiki/Return_value_optimization#Named_RVO
네마냐 Trifunovic

3
@Billy ONeal : 99 %로는 충분하지 않습니다. 100 %가 필요합니다. 머피의 법칙- "무언가 잘못 될 수있는 경우, 그럴 것입니다". 어떤 종류의 퍼지 논리를 다루는 경우 불확실성은 괜찮지 만 기존 소프트웨어를 작성하는 데는 좋은 생각이 아닙니다. 코드가 생각한대로 작동하지 않을 가능성이 1 %라도 있다면,이 코드가 당신을 해고시킬 중대한 버그를 도입 할 것이라고 예상해야합니다. 또한 표준 기능이 아닙니다. 문서화되지 않은 기능을 사용하는 것은 나쁜 생각입니다. 만약 컴파일러가 1 년 안에 기능을 떨어 뜨린다면 ( 표준에서 요구 하지 않습니까?), 당신은 문제에 빠질 것입니다.
SigTerm

4
@SigTerm : 우리가 행동의 정확성에 대해 이야기하고 있다면, 나는 당신에게 동의 할 것입니다. 그러나 우리는 성능 최적화에 대해 이야기하고 있습니다. 그런 것들은 100 % 미만의 확실성으로 괜찮습니다.
Billy ONeal

2
@Nemanja : 여기서 "의뢰"되는 것이 무엇인지 모르겠습니다. RVO 또는 NRVO를 사용하더라도 앱은 동일하게 실행됩니다. 그래도 사용하면 더 빨리 실행됩니다. 앱이 특정 플랫폼에서 너무 느리고 반환 값 복사를 위해 역 추적 한 경우 반드시 변경해야하지만 이것이 모범 사례가 여전히 반환 값을 사용한다는 사실을 변경하지는 않습니다. 복사가 발생하지 않는지 확인해야하는 경우 벡터를 a로 감싸고 shared_ptr하루를 호출하십시오.
Billy ONeal 2010-06-29

2

성능이 진짜 문제라면 이동 의미론이 항상 복사보다 빠르지 는 않다는 것을 알아야합니다 . 예를 들어 작은 문자열 최적화 를 사용하는 문자열이있는 경우 작은 문자열의 경우 이동 생성자가 일반 복사 생성자와 똑같은 작업을 수행해야합니다.


1
NRVO는 이동 생성자가 추가되었다고해서 사라지지 않습니다.
Billy ONeal

1
@Billy, 사실이지만 관련이 없습니다. 질문은 C ++ 0x가 모범 사례를 변경했고 NRVO는 C ++ 0x로 인해 변경되지 않았습니다
Motti
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.