Чи погана практика писати код, який спирається на оптимізацію компілятора?


99

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

Тепер я відчуваю, що синтаксис набагато зрозуміліший, коли об'єкт явно повертається за значенням, і компілятор, як правило, використовує RVO і зробить процес більш ефективним. Чи погана практика покладатися на цю оптимізацію? Це робить код більш зрозумілим і читабельним для користувача, що надзвичайно важливо, але чи варто насторожитися, якщо компілятор зможе скористатися можливістю RVO?

Це мікрооптимізація чи щось я маю пам’ятати при розробці коду?


7
Щоб відповісти на вашу редакцію, це мікрооптимізація, тому що навіть якби ви спробували порівняти те, що ви заробляєте в наносекунд, ви це ледве не побачите. В іншому я занадто гнилий у С ++, щоб дати точну відповідь, чому це не вийде. Один з них, якщо є ймовірність, що є випадки, коли вам потрібно динамічне виділення і, таким чином, використовувати нове / покажчик / посилання.
Вальфрат

4
@Walfrat навіть якщо об’єктів досить великі, на порядок мегабайт? Мої масиви можуть стати величезними через характер проблем, які я вирішую.
Метт

6
@Matt, я б не став. Для цього існують посилання / покажчики. Оптимізація компілятора повинна виходити за рамки того, що програмісти повинні враховувати при складанні програми, хоча це так, часто в два рази перекриваються два світи.
Ніл

5
@Matt Якщо ви не робите щось надзвичайно специфічне, яке, начебто, вимагає від розробників, які мають 10 + ish досвіду роботи на C / ядрах, низьких апаратних взаємодіях, вам це не потрібно. Якщо ви думаєте, що ви належите до чогось дуже конкретного, відредагуйте свою публікацію та додайте точний опис того, що ви повинні робити (у режимі реального часу? Важкі математичні обчислення? ...)
Вальфрат

37
У конкретному випадку RVO C ++ (N), так, покладаючись на цю оптимізацію цілком справедливо. Це тому, що стандарт C ++ 17 конкретно вимагає, щоб це відбувалося, у ситуаціях, коли це вже робили сучасні компілятори.
Калет

Відповіді:


130

Використовуйте принцип найменшого здивування .

Це ти і тільки коли-небудь ти будеш використовувати цей код, і ти впевнений, що той самий, який ти і через 3 роки, не здивуєш тим, що робиш?

Тоді йти вперед.

У всіх інших випадках використовуйте стандартний спосіб; інакше ви та ваші колеги зіткнетесь із важкими помилками.

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


88
@Neil, це моя думка, всі покладаються на оцінку короткого замикання. І вам не доведеться думати про це двічі, це слід увімкнути. Це стандарт дефакто. Так, ви можете це змінити, але не слід.
Пітер Б

49
"Я змінив, як працює мова, і ваш брудний гнилий код зламався! Ого. Шляпання було б доречно, відправляйте колегу на тренування в Дзен, там його багато.

109
@PieterB Я впевнений, що специфікації мови C та C ++ гарантують оцінку короткого замикання. Так що це не тільки де - факто стандартом, це стандарт. Без нього ви вже навіть не використовуєте C / C ++, але щось подібне підозріло подобається: P
marcelm

47
Тільки для довідки, стандартним способом тут є повернення за значенням.
DeadMG

28
@ dan04 так, це було в Delphi. Хлопці, не зациклюйтеся на прикладі, це стосується моменту, який я зробив. Нічого іншого не роби.
Пітер Б

81

У цьому конкретному випадку, безумовно, просто повернутись на значення.

  • RVO та NRVO - це добре відомі та надійні оптимізації, які справді повинен бути зроблений будь-яким порядним компілятором, навіть у режимі C ++ 03.

  • Семантика переміщення забезпечує виведення об'єктів з функцій, якщо (N) RVO не відбулося. Це тільки корисно , якщо ваш об'єкт використовує динамічні дані всередині (як std::vectorробить), але це повинно бути дійсно в разі , якщо це що велике - переповнення стеки є ризиком , пов'язаним з великими автоматичними об'єктами.

  • C ++ 17 виконує RVO. Тож не хвилюйтесь, він не зникне на вас і закінчить утверджуватися повністю після того, як компілятори будуть оновлені.

І врешті-решт, змушуючи додатковий динамічний розподіл повернути вказівник або змусити тип результату бути конфігуруваним за замовчуванням просто для того, щоб ви могли передавати його як вихідний параметр - це некрасиві та неідіоматичні рішення проблеми, яку, ймовірно, ніколи не буде мати.

Просто напишіть код, який має сенс, і подякуйте авторам-компіляторам за правильну оптимізацію коду, який має сенс.


9
Для задоволення дивіться, як Borland Turbo C ++ 3.0 з 1990-х років обробляє RVO . Спойлер: В основному це працює просто чудово.
nwp

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

7
Ця оптимізація не настільки надійна, як може хотіти. Так, це досить надійно в найбільш очевидних випадках, але шукаючи, наприклад, багзіллу gcc, існує багато ледь менш очевидних випадків, коли це пропускається.
Марк Глісс

62

Тепер я відчуваю, що синтаксис набагато зрозуміліший, коли об'єкт явно повертається за значенням, і компілятор, як правило, використовує RVO і зробить процес більш ефективним. Чи погана практика покладатися на цю оптимізацію? Це робить код більш зрозумілим і читабельним для користувача, що надзвичайно важливо, але чи варто насторожитися, якщо компілятор зможе скористатися можливістю RVO?

Це не мало відома, люб’язна мікро-оптимізація, про яку ви читали в невеликому блозі, що перебуває під загрозою торгівлі людьми, і тоді ви відчуваєте себе розумним та чудовим у використанні.

Після C ++ 11 RVO - це стандартний спосіб написання цього коду коду. Зазвичай, очікуване, викладене, згадане в переговорах, згадане в блогах, згадане в стандарті, повідомлятиметься про помилку компілятора, якщо не буде реалізовано. У мові C ++ 17 мова йде на крок далі і вимагає копіювати елісію в певних сценаріях.

Ви повинні абсолютно покладатися на цю оптимізацію.

Крім того, повернення за значенням просто призводить до набагато простішого читання та управління кодом, ніж код, який повертається за посиланням. Семантична цінність - це потужна річ, яка сама по собі може призвести до збільшення можливостей для оптимізації.


3
Дякую, це має багато сенсу і відповідає "згаданому вище" принципу найменшого здивування ". Це зробило б код дуже зрозумілим і зрозумілим, а також ускладнить возитися з покажчиками шенагіганами.
Метт

3
@Matt Частина причини, якою я підтримав цю відповідь, полягає в тому, що в ній згадується "значення семантика". Оскільки ви отримаєте більше досвіду роботи з C ++ (та програмуванням загалом), ви знайдете випадкові ситуації, коли семантику значень не можна використовувати для певних об'єктів, оскільки вони є змінними, і їх зміни потрібно зробити видимими для іншого коду, який використовує той самий об'єкт ( приклад "спільної змінності"). Коли ці ситуації трапляються, пошкодженим об’єктам потрібно буде ділитися за допомогою (розумних) покажчиків.
rwong

16

Правильність написаного коду ніколи не повинна залежати від оптимізації. Він повинен вивести правильний результат при виконанні на C ++ "віртуальній машині", який вони використовують у специфікації.

Однак те, про що ви говорите, - це скоріше питання ефективності. Ваш код працює краще, якщо він оптимізований за допомогою компілятора оптимізації RVO. Це добре, з усіх причин, зазначених в інших відповідях.

Однак якщо вам потрібна ця оптимізація (наприклад, якщо конструктор копій насправді призведе до відмови вашого коду), тепер ви перебуваєте в примхах компілятора.

Я думаю, що найкращим прикладом цього в моїй власній практиці є оптимізація хвостових викликів:

   int sillyAdd(int a, int b)
   {
      if (b == 0)
          return a;
      return sillyAdd(a + 1, b - 1);
   }

Це дурний приклад, але він показує хвостовий виклик, де функція називається рекурсивно правою в кінці функції. Віртуальна машина C ++ покаже, що цей код працює належним чином, хоча я можу викликати невелику плутанину з приводу того, чому я взагалі турбував писати таку процедуру додавання. Однак у практичних реалізаціях C ++ у нас є стек, і він має обмежений простір. Якщо це зробити педантично, ця функція повинна буде штовхати принаймні b + 1рамки стека на стек, як це робиться з її доповненням. Якщо я хочу порахувати sillyAdd(5, 7), це не велика справа. Якщо я хочу обчислити sillyAdd(0, 1000000000), я можу зіткнутися зі справжньою проблемою, викликаючи StackOverflow (а не хороший вид ).

Однак ми можемо побачити, що як тільки ми досягнемо останньої лінії повернення, ми дійсно готові до всього, що знаходиться в поточній рамці стека. Нам дійсно не потрібно тримати це навколо. Оптимізація виклику хвоста дозволяє "повторно використовувати" існуючий кадр стека для наступної функції. Таким чином нам потрібен лише 1 стек-кадр, а не b+1. (Нам залишається робити всі ці дурні додавання та віднімання, але вони не займають більше місця.) Насправді оптимізація перетворює код у:

   int sillyAdd(int a, int b)
   {
      begin:
      if (b == 0)
          return a;
      // return sillyAdd(a + 1, b - 1);
      a = a + 1;
      b = b - 1;
      goto begin;  
   }

У деяких мовах специфікація прямо вимагає оптимізацію хвостових викликів. C ++ не з таких. Я не можу розраховувати на компілятори C ++, щоб розпізнати цю можливість оптимізації хвостових викликів, якщо я не переходжу до кожного випадку. У моїй версії Visual Studio версія версії робить оптимізацію хвостового виклику, але версія для налагодження не робить (за задумом).

Таким чином, для мене було б погано залежати від можливості розрахувати sillyAdd(0, 1000000000).


2
Це цікавий кутовий випадок, але я не думаю, що ви можете узагальнити його до правила у своєму першому абзаці. Припустимо, у мене є програма для невеликого пристрою, яка завантажуватиметься і тоді, і лише в тому випадку, якщо я використовую оптимізацію зменшення розміру компілятора - це неправильно робити це? видається досить педантичним сказати, що єдиний мій вірний вибір - переписати його в асемблері, особливо якщо це перезапис робить те саме, що робить оптимізатор для вирішення проблеми.
sdenham

5
@sdenham Я вважаю, що в аргументі мало місця. Якщо ви більше не пишете для "C ++", а скоріше пишете для "WindRiver C ++ компілятора версії 3.4.1", то я можу побачити логіку там. Однак, як правило, якщо ви пишете щось, що не працює належним чином відповідно до специфікації, ви маєте зовсім інший сценарій. Я знаю, що в бібліотеці Boost є такий код, але вони завжди розміщують його в #ifdefблоки і мають доступне стандартне рішення.
Корт Аммон

4
це друкарська помилка у другому блоці коду, де написано b = b + 1?
стиб

2
Ви можете пояснити, що ви маєте на увазі під "віртуальною машиною C ++", оскільки це не термін, який використовується в будь-якому стандартному документі. Я думаю, ви говорите про модель виконання C ++, але не зовсім певну - і ваш термін оманливо схожий на "віртуальну машину байт-коду", що стосується чогось зовсім іншого.
Toby Speight

1
@supercat Scala також має явний синтаксис рекурсії хвоста. C ++ - це власний звір, але я думаю, що хвоста-рекурсія є недіоматичною для нефункціональних мов, і обов'язковою для функціональних мов, залишаючи невеликий набір мов, де розумно мати синтаксис явного рекурсії хвоста. Буквальний переклад хвостової рекурсії в петлі і явна мутація просто є кращим варіантом для багатьох мов.
профілі

8

На практиці програми C ++ очікують певних оптимізацій компілятора.

Особливо загляньте в стандартні заголовки ваших стандартних реалізацій контейнерів . За допомогою GCC ви можете запитати попередньо оброблену форму ( g++ -C -E) та внутрішнє представлення g++ -fdump-tree-gimpleGIMPLE ( або Gimple SSA з -fdump-tree-ssa) більшості вихідних файлів (технічно одиниць перекладу) за допомогою контейнерів. Ви будете здивовані кількістю оптимізації, яка зроблена (з g++ -O2). Таким чином, реалізатори контейнерів покладаються на оптимізацію (і в більшості випадків реалізатор стандартної бібліотеки C ++ знає, яка оптимізація відбудеться, і записує реалізацію контейнера, маючи на увазі; іноді він також записує пропуск для оптимізації в компілятор до мати справу з функціями, необхідними тоді стандартною бібліотекою C ++).

На практиці саме оптимізація компілятора робить C ++ та його стандартні контейнери досить ефективними. Тож можна покластися на них.

І так само у випадку з RVO, згаданим у вашому запитанні.

Стандарт C ++ був розроблений спільно (зокрема, експериментуючи досить хороші оптимізації, пропонуючи нові функції), щоб добре працювати з можливими оптимізаціями.

Наприклад, розгляньте програму нижче:

#include <algorithm>
#include <vector>

extern "C" bool all_positive(const std::vector<int>& v) {
  return std::all_of(v.begin(), v.end(), [](int x){return x >0;});
}

складіть його g++ -O3 -fverbose-asm -S. Ви дізнаєтесь, що створена функція не виконує жодної CALLінструкції з машини. Тож оптимізовано більшість кроків C ++ (побудова лямбда-замикання, його повторне застосування, отримання beginта endітераторів тощо). Машинний код містить лише цикл (який явно не відображається у вихідному коді). Без таких оптимізацій C ++ 11 не буде успішним.

доповнення

(Додано 31 грудня вул 2017)

Дивіться CppCon 2017: Метт Годбольт «Що зробив для мене останній час моїй компілятор? Розв’язання розмови про кришку компілятора » .


4

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

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

Багато зусиль докладається для того, щоб компілятори давали ефективний результат, і, очевидно, наміром є використання цих можливостей.

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


3

Я думаю, що інші добре охопили конкретний кут щодо С ++ та RVO. Ось більш загальна відповідь:

Що стосується правильності, то не слід покладатися на оптимізацію компілятора чи поведінку, що стосується компілятора в цілому. На щастя, ви, здається, не робите цього.

Що стосується продуктивності, то ви повинні розраховувати на поведінку компілятора в цілому та оптимізацію компілятора зокрема. Компілятор, сумісний зі стандартами, може безкоштовно компілювати ваш код будь-яким способом, доки скомпільований код поводиться відповідно до мовної специфікації. І я не знаю жодної специфікації для мови мови, яка визначає, наскільки швидкою повинна бути кожна операція.


1

Оптимізація компілятора повинна впливати лише на продуктивність, а не на результати. Покладатися на оптимізацію компілятора для задоволення нефункціональних вимог не лише розумно, це часто є причиною того, що один компілятор перебирається на інший.

Прапори, які визначають, як виконуються конкретні операції (наприклад, індекс або переповнення), часто стикаються з оптимізаціями компілятора, але цього не повинно бути. Вони чітко впливають на результати розрахунків.

Якщо оптимізація компілятора викликає різні результати, то помилка - помилка у компіляторі. Покладаючись на помилку у компіляторі, в довгостроковій перспективі є помилкою - що відбувається, коли вона виправляється?

Використання прапорів компілятора, які змінюють спосіб роботи обчислень, повинні бути добре задокументовані, але використовуватися за потребою


На жаль, велика кількість документації на компілятор робить погану роботу щодо визначення того, що є чи не гарантується в різних режимах. Далі, "сучасні" автори компіляторів, здається, не звертають уваги на комбінації гарантій, які програмісти роблять і не потребують. Якщо програма спрацює нормально, якщо x*y>zдовільно дасть 0 або 1 у разі переповнення, за умови, що вона не має інших побічних ефектів , вимагаючи, що програміст повинен або запобігти переповненням за будь-яку ціну, або змусити компілятора оцінити вираз певним чином непотрібні погіршення оптимізацій порівняно з тим, що ...
supercat

... компілятор може у вільний час вести себе так, ніби x*yпросуває свої операнди до якогось довільного більш тривалого типу (таким чином, дозволяючи формам підйому та зменшенню міцності, які б змінили поведінку деяких випадків переповнення). Однак багато компіляторів вимагають, щоб програмісти або запобігали переповненню за будь-яку ціну, або змушували компіляторів усікати всі проміжні значення у разі переповнення.
supercat

1

Немає.

Цим я весь час займаюся. Якщо мені потрібно отримати доступ до довільного 16-бітового блоку в пам'яті, я це роблю

void *ptr = get_pointer();
uint16_t u16;
memcpy(&u16, ptr, sizeof(u16)); // ntohs omitted for simplicity

... і покладайтеся на те, що компілятор робить все можливе, щоб оптимізувати цей фрагмент коду. Код працює на ARM, i386, AMD64 і практично на кожній архітектурі. Теоретично, неоптимізуючий компілятор може насправді викликати memcpy, що призводить до абсолютно поганої продуктивності, але це не є проблемою для мене, оскільки я використовую оптимізації компілятора.

Розглянемо альтернативу:

void *ptr = get_pointer();
uint16_t *u16ptr = ptr;
uint16_t u16;
u16 = *u16ptr;  // ntohs omitted for simplicity

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

Різниця між -O2 та -O0 при використанні memcpyтрюку велика: 3,2 Гбіт / с продуктивність контрольної суми IP проти 67 Гбіт / с в якості контрольної суми IP. За порядок різниці величин!

Іноді вам може знадобитися допомогти компілятору. Так, наприклад, замість того, щоб покладатися на компілятор для розгортання циклів, ви можете зробити це самостійно. Або шляхом впровадження відомого пристрою Даффа , або більш чистим способом.

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

Що б ви не робили, будь ласка, переконайтеся, що ваш шлях насправді не є визначеним поведінкою. Безумовно, доступ до якогось випадкового блоку пам'яті як 16-бітного цілого числа є невизначеним поведінкою через проблеми з вивільненням та вирівнюванням.


0

Усі спроби ефективного коду, написаного в чому завгодно, крім складання, дуже і дуже покладаються на оптимізацію компілятора, починаючи з найпростішого, наприклад ефективного розподілу реєстру, щоб уникнути зайвих розсипань стека в усьому місці і, принаймні, досить хорошого, якщо не відмінного, вибору інструкцій. Інакше ми повернемося до 80-х років, де нам довелося розміщувати registerпідказки всюди і використовувати мінімальну кількість змінних у функції, щоб допомогти архаїчним компіляторам C або навіть раніше, коли gotoбула корисною оптимізацією розгалуження.

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

Це справді питання про те, наскільки надійно ви відчуваєте, що оптимізація може бути зроблена, що найкраще розібратися за допомогою профілювання та вивчення можливостей компіляторів, які у вас є, і, можливо, навіть розбирання, якщо є точка доступу, ви не можете зрозуміти, де здається компілятор не вдалося зробити очевидну оптимізацію.

RVO - це те, що існує протягом багатьох століть, і, принаймні, виключаючи дуже складні випадки, - це те, що компілятори надійно застосовують протягом століть. Однозначно не варто працювати над проблемою, яка не існує.

Помилка на стороні того, що покладатися на оптимізатор, не боячись цього

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

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

Профілювання

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


-1

Програмне забезпечення можна писати на C ++ на дуже різних платформах і для безлічі різних цілей.

Це повністю залежить від призначення програмного забезпечення. Чи слід легко обслуговувати, розширювати, виправляти, рефактор тощо. або інші важливіші речі, як-от продуктивність, вартість або сумісність з певним обладнанням або час, необхідний для розробки.


-2

Я думаю, нудна відповідь на це: «це залежить».

Чи погана практика писати код, який спирається на оптимізацію компілятора, яка , ймовірно, буде вимкнена, і коли вразливість не зафіксована, і де код, про який йде мова, не перевіряється одиницею, щоб, якби він зламався, ви знали це ? Мабуть.

Чи погана практика писати код, який спирається на оптимізацію компілятора, яка , швидше за все, не буде вимкнена , тобто документально підтверджена і перевірена одиницею ? Можливо, не.


-6

Якщо ви не скажете нам більше, це погана практика, але не з тієї причини, яку ви пропонуєте.

Можливо, на відміну від інших мов, якими ви користувалися раніше, повернення значення об'єкта в C ++ дає копію об'єкта. Якщо ви потім модифікуєте об'єкт, ви змінюєте інший об'єкт . Тобто, якщо я маю Obj a; a.x=1;і Obj b = a;, то роблю b.x += 2; b.f();, то a.xвсе одно дорівнює 1, а не 3.

Отже, ні, використання об'єкта як значення замість посилання або вказівника не забезпечує однакову функціональність, і ви можете виявити помилки у вашому програмному забезпеченні.

Можливо, ви це знаєте, і це не впливає негативно на конкретний випадок використання. Однак, виходячи з формулювання у вашому запитанні, виявляється, що ви можете не знати про відмінність; формулювання типу "створити об'єкт у функції".

"створити об'єкт у функції" звучить так, як звучить new Obj;"повернення об'єкта за значенням"Obj a; return a;

Obj a;і Obj* a = new Obj;це дуже, дуже різні речі; перший може призвести до пошкодження пам’яті, якщо її не правильно використовувати і не зрозуміти, а другий може призвести до витоку пам’яті, якщо її не правильно використовувати і не зрозуміти.


8
Оптимізація зворотного значення (RVO) - це чітко визначена семантика, де компілятор будує повернутий об'єкт на один рівень вгору на кадрі стека, уникаючи, зокрема, зайвих копій об'єктів. Це чітко визначена поведінка, яку підтримували задовго до того, як вона отримала мандат у C ++ 17. Ще 10-15 років тому всі основні компілятори підтримували цю функцію і робили це послідовно.

@Snowman Я не говорю про фізичне управління пам'яттю низького рівня, і я не обговорював роздуття або швидкість пам'яті. Як я спеціально показав у своїй відповіді, я кажу про логічні дані. За логікою , подання значення об'єкта - це створення його копії, незалежно від того, як реалізований компілятор або яка збірка використовується поза кадром. Закулісні речі на низькому рівні - це одне, а логічна структура та поведінка мови - інше; вони споріднені, але вони не одне й те саме - обидва слід розуміти.
Аарон

6
у вашій відповіді сказано, що "повернення значення об'єкта в C ++ дає копію об'єкта", яка є абсолютно помилковою в контексті RVO - об'єкт побудований безпосередньо в місці виклику, і жодна копія ніколи не робиться. Ви можете перевірити це, видаливши конструктор копій і повернувши об'єкт , побудований в returnоператорі, який є вимогою для RVO. Крім того, ви продовжуєте говорити про ключові слова newта покажчики, а не про RVO. Я вважаю, ви або не розумієте питання, або RVO, або, можливо, і те й інше.

-7

Пітер В абсолютно коректний, рекомендуючи щонайменше здивування.

Щоб відповісти на ваше конкретне запитання, що це (швидше за все) означає в C ++, що ви повинні повернути a std::unique_ptrдо побудованого об'єкта.

Причина в тому, що для розробника C ++ це зрозуміліше, що відбувається.

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

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


Коли в Римі, робіть так, як роблять римляни.

14
Це не є гарною відповіддю для типів, які самі не виконують динамічні розподіли. Те, що ОП вважає природним у своєму випадку використання, це повернення за значенням, означає, що його об'єкти мають автоматичну тривалість зберігання на стороні абонента. Для простих, не надто великих об'єктів навіть наївне реалізація копію-повернення вартості буде на порядок швидше, ніж динамічне розподіл. (Якщо, з іншого боку, функція повертає контейнер, то повернення унікального_показника може бути навіть вигідним порівняно з поверненням наївного компілятора за значенням.)
Пітер А. Шнайдер

9
@Matt Якщо ви цього не усвідомлювали, це не найкраща практика. Непотрібно робити розподіл пам’яті та примушувати семантику покажчиків до користувачів погано.
nwp

5
Перш за все, при використанні розумних покажчиків слід повертатися std::make_unique, а не std::unique_ptrбезпосередньо. По-друге, RVO - це не якась езотерична оптимізація для конкретних постачальників: вона входить у стандарт. Навіть тоді, коли цього не було, його широко підтримували і очікували поведінки. Немає сенсу повертати a, std::unique_ptrколи вказівник не потрібен в першу чергу.

4
@Snowman: Немає "коли не було". Хоча це нещодавно стало обов'язковим , кожен стандарт C ++ коли-небудь визнавав [N] RVO і робив умови для його ввімкнення (наприклад, компілятору завжди було надано явний дозвіл пропускати використання конструктора копій на повернене значення, навіть якщо воно має видимі побічні ефекти).
Джеррі Труну
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.