У 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є рівнозначним, тому v буде побудовано за допомогою конструктора переміщення std::vector, припускаючи, що (N) RVO не має місце.

Ще до початку C ++ 0x перша форма часто була б "ефективною" через (N) RVO. Однак (N) RVO на розсуд укладача. Тепер, коли у нас є посилання на рецензію, гарантується, що жодної глибокої копії не буде.

Редагувати : Питання справді не стосується оптимізації. Обидві показані форми мають майже однакову ефективність у реальних програмах. В той час, як раніше, перша форма могла мати на порядок гірші показники. У результаті перша форма тривалий час мала основний запах коду в програмуванні на C ++. Я вже не сподіваюся?


18
Хто коли-небудь казав, що це було поганою формою для початку?
Едвард Странд

7
Це, безумовно, був поганий запах коду в "старовинні дні", звідки я родом. :-)
Нейт

1
Я впевнений, що так! Мені хотілося б, щоб прохідна цінність стала все більш популярною. :)
sellibitze

Відповіді:


73

Дейв Абрахамс має досить всебічний аналіз швидкості передачі / повернення значень .

Коротка відповідь, якщо вам потрібно повернути значення, тоді поверніть значення. Не використовуйте вихідні посилання, оскільки компілятор так чи інакше робить це. Звичайно, є застереження, тому слід прочитати цю статтю.


24
"компілятор так чи інакше": компілятору не потрібно робити цього == невизначеність == погана ідея (потрібна 100% визначеність). "всебічний аналіз" Існує величезна проблема з цим аналізом - він покладається на недокументовані / нестандартні мовні особливості у невідомому компіляторі ("Хоча копіювання елізію ніколи не вимагає стандарт"). Тож навіть якщо це працює, використовувати його не дуже добре - немає абсолютно жодної гарантії того, що він буде працювати за призначенням, і немає гарантії, що кожен компілятор завжди працюватиме таким чином. Покладатися на цей документ - погана практика кодування, ІМО. Навіть якщо ви втратите продуктивність.
SigTerm

5
@SigTerm: Це чудовий коментар !!! Більшість згаданих статей занадто розпливчасті, щоб навіть розглянути їх для використання у виробництві. Люди думають, що автор, який написав книгу «Червона глибина», є євангелією, і його слід дотримуватися без будь-якого подальшого роздуму чи аналізу. АТМ на ринку не існує компілятора, який забезпечує копію-елісон настільки ж різноманітними, як приклади, які Абрахамс використовує у статті.
Хіпікодер

13
@SigTerm, багато чого не потрібно робити компілятору, але ви припускаєте, що це все-таки є. Укладачі не "потрібно» , щоб зміни x / 2в x >> 1протягом intсекунд, але припустимо , що це буде. Стандарт також нічого не говорить про те, як потрібні компілятори для реалізації посилань, але ви припускаєте, що з ними ефективно обробляються за допомогою покажчиків. Стандарт також нічого не говорить про v-таблиці, тому ви не можете бути впевнені, що виклики віртуальних функцій також ефективні. По суті, вам потрібно часом довіряти компілятору.
Петро Олександр

16
@Sig: Насправді гарантується дуже мало, за винятком фактичного випуску вашої програми. Якщо ви хочете на 100% впевненість у тому, що відбуватиметься 100% часу, тоді вам краще перейти на іншу мову прямо.
Dennis Zickefoose

6
@SigTerm: Я працюю над "фактичним сценарієм". Я перевіряю, що робить компілятор, і працюю з цим. Немає "може працювати повільніше". Він просто не працює повільніше, оскільки компілятор НЕ реалізує RVO, незалежно від того, вимагає цього стандарт чи ні. Немає ifs, buts або maybes, це просто простий факт.
Петро Олександр

37

Принаймні ІМО, це зазвичай погана ідея, але ні з міркувань ефективності. Це погана ідея, оскільки відповідна функція, як правило, повинна записуватися як загальний алгоритм, який виробляє свій вихід через ітератор. Майже будь-який код, який приймає або повертає контейнер замість роботи на ітераторах, слід вважати підозрілим.

Не зрозумійте мене неправильно: буває, що є сенс обходити об'єкти, схожі на колекцію (наприклад, рядки), але в наведеному прикладі я вважаю передачу або повернення вектора поганою ідеєю.


6
Проблема підходу ітератора полягає в тому, що він вимагає, щоб ви робили шаблони функцій і методів, навіть коли тип елемента колекції відомий. Це дратує, і коли розглянутий метод віртуальний, неможливо. Зауважте, я не погоджуюся з вашою відповіддю як такою, але на практиці вона просто стає трохи громіздкою в C ++.
jon-hanson

22
Я повинен не погодитися. Використання ітераторів для виведення іноді доцільно, але якщо ви не пишете загального алгоритму, загальні рішення часто дають неминучі накладні витрати, що важко виправдати. Як щодо складності коду, так і фактичної продуктивності.
Dennis Zickefoose

1
@ Денніс: Я мушу сказати, що мій досвід був зовсім протилежним: я пишу досить багато речей у вигляді шаблонів, навіть коли я знаю типи, які стосуються достроково, тому що це простіше і покращує продуктивність.
Джеррі Труну

9
Я особисто повертаю контейнер. Намір зрозумілий, код простіше, я не дуже дбаю про ефективність, коли пишу його (я просто уникаю ранньої песимізації). Я не впевнений, чи зробить ітератор виводу більш чітким мій намір ... і мені потрібен нешаблонний код якомога більше, тому що у великих проектах залежності вбивають розробку.
Матьє М.

1
@Dennis: Я вважаю, що концептуально ви ніколи не повинні «будувати контейнер, а не писати в діапазон». Контейнер - це саме те, що - контейнер. Ваша турбота (і проблема вашого коду) повинна стосуватися вмісту, а не контейнера.
Джеррі Труну

18

Суть:

Скопіюйте Elision і RVO можна уникнути "страшних копій" (компілятор не потрібен для здійснення цих оптимізацій, а в деяких ситуаціях він не може бути застосований)

C ++ 0x RValue посилання дозволяють реалізацію рядків / векторів, що гарантує .

Якщо ви можете відмовитися від старих компіляторів / реалізацій STL, повертайте вектори вільно (і переконайтеся, що ваші власні об’єкти теж підтримують його). Якщо ваша база коду потребує підтримки "менших" компіляторів, дотримуйтесь старого стилю.

На жаль, це має великий вплив на ваші інтерфейси. Якщо C ++ 0x не є варіантом, і вам потрібні гарантії, ви можете використовувати натомість об'єкти, що рахуються посиланнями, або копіювати при записі в деяких сценаріях. Хоча вони мають і недоліки з багатопотоковою обмоткою.

(Я хочу, щоб лише одна відповідь на C ++ була б простою та зрозумілою і без умов).


11

Дійсно, так як C ++ 11, вартість копіюванняstd::vector зникає в більшості випадків.

Однак слід пам’ятати, що вартість побудови нового вектора (потім його руйнування ) все ще існує, а використання вихідних параметрів замість повернення за значенням все-таки корисно, коли ви хочете повторно використовувати ємність вектора. Це задокументовано як виняток у F.20 Основних Правил C ++.

Порівняємо:

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, оскільки у мене є 16 Гб оперативної пам’яті, і це число забезпечує виділення не більше 8 ГБ (sizeof (int) = 4), гарантуючи, що я не переходжу на диск ( всі інші програми були закриті, я мав ~ 15 ГБ в наявності під час запуску тесту).

Ось код:

#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,20 ГГц; 16 ГБ DDR4 2400 МГц; Kubuntu 18.04)

Позначення: mem (v) = v.size () * sizeof (int) = v.size () * 4 на моїй платформі.

Не дивно, що коли numIter = 1(тобто mem (v) = 8GB), часи ідеально однакові. Дійсно, в обох випадках ми виділяємо лише один раз величезний вектор в 8 ГБ пам'яті. Це також доводить, що при використанні BuildLargeVector1 () жодної копії не сталося: мені не вистачило оперативної пам’яті, щоб зробити копію!

Коли numIter = 2повторне використання ємності вектора замість повторного виділення другого вектора на 1,37x швидше.

Коли numIter = 256, повторне використання ємності вектора (замість того, щоб розподіляти / розставляти вектор знову і знову 256 разів ...), на 2,45 рази швидше :)

Ми можемо помітити, що time1 є майже постійним від numIter = 1до numIter = 256, а це означає, що виділення одного величезного вектора 8 Гб - це майже стільки ж дорого, як і виділення 256 векторів 32 Мб. Однак виділення одного величезного вектора в 8 ГБ, безумовно, дорожче, ніж виділення одного вектора 32 МБ, тому повторне використання ємності вектора забезпечує підвищення продуктивності.

З numIter = 512(mem (v) = 16MB) до numIter = 8M(mem (v) = 1kB) - це солодке місце: обидва способи є настільки ж швидкими та швидшими, ніж усі інші комбінації numIter та vecSize. Це, мабуть, пов'язане з тим, що розмір кеша L3 мого процесора становить 8 МБ, так що вектор майже повністю вписується в кеш. Я не дуже пояснюю, чому раптовий стрибок time1для mem (v) = 16MB, здавалося б, більш логічним це сталося одразу після, коли mem (v) = 8MB. Зауважте, що дивно, що в цьому милому місці не повторне використання ємності насправді трохи швидше! Я не дуже пояснюю це.

Коли numIter > 8Mречі починають потворні. Обидва способи стають повільнішими, але повернення вектора за значенням стає ще повільнішим. У гіршому випадку, коли вектор, що містить лише один єдиний int, повторно використовує ємність, а не повертається за значенням, на 3,3 рази швидше. Імовірно, це пов'язано з постійними витратами malloc (), які починають домінувати.

Зауважте, наскільки крива часу2 є більш плавною, ніж крива часу1: не тільки повторне використання ємності вектора, як правило, швидше, але, що може бути важливіше, воно більш передбачуване .

Також зауважте, що в солодкому місці нам вдалося здійснити 2 мільярди додавання 64-бітових цілих чисел за ~ 0,5 секунди, що цілком оптимально для 64-бітного процесора 4,2 ГГц. Ми могли б зробити краще, паралелізуючи обчислення, щоб використовувати всі 8 ядер (тест вище використовує лише одне ядро ​​за один раз, що я перевірив, повторно запустивши тест під час моніторингу використання процесора). Найкраща продуктивність досягається при mem (v) = 16 кБ, що є порядком величини кешу L1 (кеш даних L1 для i7-7700K становить 4х32 кБ).

Звичайно, відмінності стають все менш актуальними, чим більше обчислень, які ви насправді повинні робити на даних. Нижче наведені результати , якщо замінити sum = std::accumulate(v.begin(), v.end(), sum);на for (int k : v) sum += std::sqrt(2.0*k);:

Орієнтир 2

Висновки

  1. Використання вихідних параметрів замість повернення за значенням може забезпечити підвищення продуктивності за рахунок повторного використання потужності.
  2. На сучасному настільному комп’ютері це здається застосовно лише до великих векторів (> 16 МБ) та малих векторів (<1 КБ).
  3. Уникайте виділення мільйонів / мільярдів малих векторів (<1 КБ). Якщо можливо, повторно використовуйте ємність, а ще краще, розробіть архітектуру по-іншому.

Результати можуть відрізнятися на інших платформах. Як завжди, якщо продуктивність важлива, напишіть орієнтири для конкретного випадку використання.


6

Я все ще думаю, що це погана практика, але варто зазначити, що моя команда використовує MSVC 2008 та GCC 4.1, тому ми не використовуємо останні компілятори.

Раніше багато точок доступу, показаних у vtune з MSVC 2008, перейшли до копіювання рядків. У нас був такий код:

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, і ніколи не доведеться турбуватися, що ми песимізуємо наш код, повертаючи комплекс класи за значенням.

[Редагувати] Як зазначав Нейт, RVO застосовується до повернення тимчасових файлів, створених всередині функції. У моєму випадку таких тимчасових періодів не було (за винятком недійсної гілки, де ми будуємо порожній рядок), і тому RVO не був би застосований.


3
Ось у чому річ: компілятор RVO залежить від компілятора, але компілятор C ++ 0x повинен використовувати семантику переміщення, якщо він вирішує не використовувати RVO (припустимо, що існує конструктор переміщення). За допомогою оператора триграфа перемагає RVO. Дивіться cpp-next.com/archive/2009/09/move-it-with-rvalue-references, на які посилався Петро. Але ваш приклад все одно не підходить для семантики переміщення, оскільки ви не повертаєтеся тимчасово.
Нейт

@ Stinky472: Повернення члена за значенням завжди відбуватиметься повільніше, ніж посилання. Посилання Rvalue все-таки буде повільніше, ніж повернення посилання на початковий член (якщо абонент може взяти посилання замість необхідності копії). Крім того, є ще багато разів, які ви можете зберігати над переоцінкою посилань, оскільки у вас є контекст. Наприклад, ви можете зробити String newstring; newstring.resize (string1.size () + string2.size () + ...); newstring + = string1; newstring + = string2; і т. д. Це все ще значна економія над ревальваціями.
Щеня

@DeadMG істотна економія над бінарним оператором + навіть із компіляторами C ++ 0x, що реалізують RVO? Якщо так, то шкода. Знову ж таки, максе-сенс, оскільки нам все-таки доводиться створювати тимчасовий для обчислення об'єднаної рядки, тоді як + = може з'єднуватися безпосередньо з новим рядком.
stinky472

Як щодо такого випадку, як: string newstr = str1 + str2; У компіляторі, що реалізує семантику переміщення, здається, що це повинно бути таким же швидким або навіть швидшим, ніж: string newstr; newstr + = str1; newstr + = str2; Немає резерву, так би мовити (я припускаю, що ви мали на увазі резерв замість розміру).
stinky472

5
@Nate: Я думаю, що ви плутаєте триграфи, як-от <::або ??!з умовним оператором ?: (іноді його називають потрійним оператором ).
fredoverflow

3

Лише для того, щоб нітрохи вибрати: у багатьох мовах програмування не повертається масив з функцій. У більшості з них повертається посилання на масив. У C ++ найближча аналогія буде повертатисяboost::shared_array


4
@Billy: std :: вектор - тип значення з семантикою копіювання. Нинішній стандарт C ++ не дає гарантій, що (N) RVO коли-небудь застосовується, і на практиці існує багато сценаріїв реального життя, коли його немає.
Неманья Трифунович

3
@Billy: Знову є дуже реальні сценарії, коли навіть останні компілятори не застосовують NRVO: efnetcpp.org/wiki/Return_value_optimization#Named_RVO
Nemanja Trifunovic

3
@Billy ONeal: 99% недостатньо, потрібно 100%. Закон Мерфі - "якщо щось може піти не так, воно буде". Невпевненість - це добре, якщо ви маєте справу з якоюсь нечіткою логікою, але це не дуже гарна ідея для написання традиційного програмного забезпечення. Якщо є навіть 1% ймовірності, що код не працює так, як ви думаєте, тоді слід очікувати, що цей код введе критичну помилку, яка звільнить вас. Плюс це не стандартна функція. Використання недокументованих функцій - це погана ідея - якщо через рік компілятор знань скине функцію (її не вимагає стандарт, правда?), Ви потрапите в біду.
SigTerm

4
@SigTerm: Якби ми говорили про правильність поведінки, я би погодився з вами. Однак ми говоримо про оптимізацію продуктивності. Такі речі добре з менш ніж 100% впевненістю.
Біллі ONeal

2
@Nemanja: Я не бачу, на що тут "покладаються". Ваш додаток працює так само, незалежно від того, використовуються RVO або NRVO. Якщо вони використовуються, вони працюватимуть швидше. Якщо ваш додаток занадто повільний на певній платформі, і ви простежили його назад, щоб копіювати повернене значення, то це неодмінно змінити його, але це не змінить того факту, що найкращою практикою все ж є використання повернутого значення. Якщо вам абсолютно потрібно забезпечити відсутність копіювання, загорніть вектор у «a» shared_ptrта зателефонуйте йому щодня.
Біллі ONeal

2

Якщо продуктивність справжня проблема, ви повинні усвідомити, що семантика переміщення не завжди швидша, ніж копіювання. Наприклад, якщо у вас є рядок, який використовує оптимізацію невеликих рядків, тоді для невеликих рядків конструктор переміщення повинен виконати такий самий об'єм роботи, як і звичайний конструктор копій.


1
NRVO не минає лише тому, що були додані конструктори переміщення.
Біллі ONeal

1
@Billy, правда, але неактуально, питання полягало у тому, чи C ++ 0x змінив кращі практики та NRVO не змінився через C ++ 0x
Motti
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.