Коли я дійсно повинен використовувати noexcept?


507

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

  1. Існує багато прикладів функцій, які я знаю, що ніколи не кинуть, але для яких компілятор не може визначити це самостійно. Чи слід додавати noexceptдо декларації функції у всіх таких випадках?

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

  2. Коли я реально розраховувати спостерігати покращення продуктивності після використання noexcept? Зокрема, наведіть приклад коду, для якого компілятор C ++ здатний генерувати кращий машинний код після додавання noexcept.

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


40
Код, який використовує move_if_nothrow(або щоchamacallit), побачить поліпшення продуктивності, якщо є неісключений ctor переміщення.
Р. Мартіньо Фернандес


5
Це move_if_noexcept.
Нікос

Відповіді:


180

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

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

Ну, тоді використовуйте його, коли очевидно, що функція ніколи не кине.

Коли я реально розраховувати спостерігати покращення продуктивності після використання noexcept? [...] Особисто я хвилююсь noexceptчерез збільшення свободи, наданої компілятору для безпечного застосування певних видів оптимізацій.

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

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


6
Я думаю, що std::terminateфокус все ще підкоряється моделі Zero-Cost. Тобто просто так трапляється, що діапазон інструкцій у межах noexceptфункцій відображається для виклику, std::terminateякщо throwвикористовується замість розмотувача стека. Тому я сумніваюся, що це більше витрат, ніж регулярне відстеження виключень.
Матьє М.

4
@Klaim Дивіться це: stackoverflow.com/a/10128180/964135 На насправді він просто повинен бути не метання, але noexceptгарантує це.
Паббі

3
"Якщо noexceptвикидається функція, то std::terminateвиклик, який здається, що це передбачає невелику кількість накладних витрат" ... Ні, це слід реалізувати, не створюючи таблиці винятків для такої функції, яку диспетчер винятків повинен виловлювати, а потім спасати.
Potatoswatter

8
Обробка винятків @Pubby C ++ зазвичай виконується без накладних витрат, за винятком таблиць стрибків, які відображають адреси потенційних сайтів викликів у точки входу обробника. Видалення цих таблиць наближається до повного видалення обробки винятків. Єдина відмінність - розмір виконавчого файлу. Напевно, нічого не варто згадувати.
Potatoswatter

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

133

Я продовжую повторювати ці дні: спочатку семантика .

Додавання noexcept, noexcept(true)і noexcept(false)в першу чергу про семантику. Це лише випадково обумовлює ряд можливих оптимізацій.

Як код читання програміста, присутність noexceptсхожа з такою const: вона допомагає мені краще зрозуміти, що може, а що не може статися. Тому варто витратити трохи часу на роздуми над тим, чи знаєте ви, чи не буде функція кинути. Для нагадування, будь-який тип динамічного розподілу пам’яті може кинути.


Гаразд, тепер про можливі оптимізації.

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

Компілятор може голити тільки трохи жиру (можливо) від обробки даних за винятком, тому що він повинен брати до уваги той факт , що ви можете збрехати. Якщо функція, позначена noexceptкидає, кидає, тоді std::terminateвикликається.

Ця семантика була обрана з двох причин:

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

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

8
Так, можна визначити noexcept таким чином, як ви пропонуєте, але це було б дійсно непридатною функцією. Багато функцій можуть кинутись, якщо певні умови не виконані, і ви не можете їх зателефонувати, навіть якщо знаєте, що умови виконані. Наприклад, будь-яка функція, яка може кидати std :: invalid_argument.
tr3w

3
@MatthieuM. Трохи запізнився на відповідь, але все-таки. Функції, позначені noexcept, можуть викликати інші функції, які можуть кинути, обіцянка полягає в тому, що ця функція не випромінює виняток, тобто вони повинні самі обробляти виняток!
Гонф

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

5
@Nemo: у бібліотеці Standard це, можливо, єдина, однак вона має принцип, який можна повторно використовувати в іншому місці. Операція переміщення - це операція, яка тимчасово ставить деякий стан у "кінцівку", і лише тоді, коли noexceptхтось може впевнено використовувати його на фрагменті даних, до якого можна було отримати доступ згодом. Я міг бачити, як ця ідея використовується в іншому місці, але бібліотека Standard досить тонка в C ++, і вона використовується лише для оптимізації копій елементів, які я думаю.
Матьє М.

77

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

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

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

void foo(int x) {
    try {
        bar();
        x = 5;
        // Other stuff which doesn't modify x, but might throw
    } catch(...) {
        // Don't modify x
    }

    baz(x); // Or other statement using x
}

Графік потоку для цієї функції відрізняється, якщо barвін позначений міткою noexcept(не існує способу виконання переходити між кінцем barі оператором вилову). Якщо позначено як noexcept, компілятор певний, значення x дорівнює 5 під час функції baz - сказано, що блок x = 5 "домінує" над блоком baz (x) без ребра bar()до заяви.

Потім він може зробити щось, що називається "постійне розповсюдження", щоб створити більш ефективний код. Тут, якщо baz вкладений, висловлювання, що використовують x, можуть також містити константи, і те, що раніше було оцінкою виконання, може бути перетворене на оцінку часу компіляції тощо.

У будь-якому випадку, коротка відповідь: noexceptдозволяє компілятору генерувати більш жорсткий графік потоку, а графік потоку використовується для міркування про всілякі поширені оптимізації компілятора. Для компілятора такі примітки користувачів є надзвичайними. Компілятор спробує розібратися з цим матеріалом, але зазвичай це не вдається (функція, про яку йде мова, може бути в іншому об'єктному файлі, який не видно компілятору, або транзитивно використовувати якусь функцію, яку не видно), або коли вона є, якийсь тривіальний виняток, який може бути викинутий, про який ви навіть не знаєте, тому він не може неявно позначати його як noexcept(наприклад, виділення пам'яті може кинути bad_alloc).


3
Чи насправді це має значення на практиці? Приклад надуманий, тому що нічого раніше не x = 5можна кинути. Якби ця частина tryблоку служила будь-якій меті, міркування не було б дотримано.
Potatoswatter

7
Я б сказав, що це дійсно має велике значення в оптимізації функцій, які містять блоки спробу / лову. Приклад, який я наводив, хоча і надуманий, не є вичерпним. Більш суть полягає в тому, що noexcept (на зразок оператора return () перед ним) допомагає компілятору генерувати менший графік потоку (менше ребер, менше блоків), який є основною частиною багатьох оптимізацій, які він робить.
Terry Mahaffey

Як компілятор може розпізнати, що код може кинути виняток? Чи вважається та доступ до масиву можливим винятком?
Томаш Кубес

3
@ qub1n Якщо компілятор може бачити тіло функції, він може шукати явні throwтвердження, або інші подібні речі newможуть кинути. Якщо компілятор не може бачити тіло, він повинен покладатися на наявність чи відсутність noexcept. Звичайний доступ до масиву зазвичай не генерує винятки (у C ++ немає перевірки меж), тому ні, доступ до масиву не поодинці змушує компілятора думати, що функція кидає винятки. (
Позашляховий

@cdhowie " вона повинна покладатися на присутність або відсутність noexcept " або присутність throw()у попередньому noexcept C ++
curiousguy

57

noexceptможе значно підвищити ефективність деяких операцій. Це відбувається не на рівні генерації машинного коду компілятором, а шляхом вибору найефективнішого алгоритму: як уже згадували інші, ви здійснюєте цей вибір за допомогою функції std::move_if_noexcept. Наприклад, зростання std::vector(наприклад, коли ми зателефонуємо reserve) повинно забезпечувати сувору гарантію виключення та безпеки. Якщо він знає, що Tконструктор переміщення не кидає, він може просто перемістити кожен елемент. В іншому випадку він повинен скопіювати всі Ts. Про це детально було описано у цьому дописі .


4
Додаток: Це означає, що якщо ви визначаєте конструктори переміщення або оператори присвоєння переміщення додають noexceptдо них (якщо застосовується)! Неявно визначені функції учасника переміщення noexceptдодаються до них автоматично (якщо це застосовується).
mucaho

33

Коли я реально, окрім як спостерігати покращення продуктивності після використання noexcept? Зокрема, наведіть приклад коду, для якого компілятор C ++ може генерувати кращий машинний код після додавання noexcept.

Гм, ніколи? Ніколи не час? Ніколи.

noexceptпризначено для оптимізації продуктивності компілятора так само, constяк для оптимізації продуктивності компілятора. Тобто майже ніколи.

noexceptв основному використовується, щоб дозволити "ви" виявляти під час компіляції, якщо функція може кинути виняток. Пам'ятайте: більшість компіляторів не виділяють спеціальний код для винятку, якщо він насправді щось не кидає. Тому noexceptсправа не в тому, щоб дати підказкам компілятора про те, як оптимізувати функцію настільки, як дати вам підказки про те, як використовувати функцію.

Шаблони на зразок move_if_noexceptвиявлять, чи визначається конструктор переміщення, noexceptі повернуть a, const&а не &&тип, якщо його немає. Це спосіб сказати, що рухатись, якщо це дуже безпечно зробити.

Взагалі, вам слід користуватися, noexceptколи ви думаєте, що це буде корисно для цього. Деякий код буде проходити різними шляхами, якщо is_nothrow_constructibleвірно для цього типу. Якщо ви використовуєте код, який буде це робити, не соромтеся до noexceptвідповідних конструкторів.

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


13
Строго, move_if_noexceptкопія не поверне, вона поверне const lvalue-посилання, а не rvalue-посилання. Загалом це призведе до того, що абонент зробить копію замість переміщення, але move_if_noexceptне робить її. Інакше чудове пояснення.
Джонатан Вейклі

12
+1 Джонатан. Наприклад, зміни розміру вектора переміщуватиме об'єкти, а не копіювати їх, якщо конструктор переміщення є noexcept. Так що "ніколи" неправда.
mfontanini

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

7
@mfontanini: Компілятор генерує лише кращий код, оскільки компілятор змушений скласти інший кодовий шлях . Це працює лише тому std::vector, що написано, щоб змусити компілятора зібрати інший код. Справа не в тому, що компілятор щось виявляє; йдеться про код користувача, який-небудь виявляє.
Нікол Болас

3
Справа в тому, що я, здається, не можу знайти "оптимізацію компілятора" у цитаті на початку вашої відповіді. Як сказав @ChristianRau, його компілятор генерує більш ефективний код, не важливо, в чому походження цієї оптимізації. В кінці кінців, компілятор буде генерувати більш ефективний код, чи не так? PS: Я ніколи не казав, що це оптимізація компілятора, я навіть сказав "Це не оптимізація компілятора".
mfontanini

22

У Бьярне «s слова ( C ++ Мова програмування 4 - е видання , стор 366):

Якщо припинення є прийнятною відповіддю, несподіваний виняток досягне цього, оскільки він перетворюється на виклик термінації () (§13.5.2.5). Також noexceptспецифікатор (§ 13.5.1.1) може зробити це бажання явним.

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

double compute(double x) noexcept;     {
    string s = "Courtney and Anya";
    vector<double> tmp(10);
    // ...
}

Конструктор векторів може не отримати пам'ять на свої десять пар і кинути a std::bad_alloc. У такому випадку програма припиняється. Він припиняється беззастережно шляхом виклику std::terminate()(§30.4.1.3). Він не викликає деструкторів від виклику функцій. Визначено реалізацією, чи викликаються деструктори з областей між throwі noexcept(наприклад, для s в обчисленні ()). Програма як раз завершиться, тому ми не повинні залежати від будь-якого об'єкта. Додаючи noexceptспецифікатор, ми вказуємо, що наш код не був написаний, щоб впоратися з кидком.


2
У вас є джерело для цієї цитати?
Антон Голов

5
@AntonGolov "Мова програмування на C ++, 4-е видання", стор. 366
Расті Шеклфорд

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

21
  1. Існує багато прикладів функцій, які я знаю, що ніколи не кинуть, але для яких компілятор не може визначити це самостійно. Чи слід додати ноекрім декларації функції у всіх таких випадках?

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

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

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

  1. У яких ситуаціях я повинен бути більш уважним щодо використання noexcept, і в яких ситуаціях я можу піти з мається на увазі noexcept (false)?

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

  1. операції переміщення (оператор присвоєння переміщення та конструктори переміщення)
  2. операції своп
  3. оператори пам'яті (оператор видалення, оператор видалення [])
  4. деструктори (хоча це неявно, noexcept(true)якщо ви їх не зробите noexcept(false))

Ці функції, як правило, повинні бути noexcept, і, швидше за все, реалізація бібліотеки може використовувати noexceptвластивість. Наприклад, std::vectorможна використовувати операції з переміщення, що не кидаються, без шкоди для строгих гарантій виключення. В іншому випадку доведеться повернутися до елементів копіювання (як це було в C ++ 98).

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

  1. Коли я реально розраховувати спостерігати покращення продуктивності після використання noexcept? Зокрема, наведіть приклад коду, для якого компілятор C ++ здатний генерувати кращий машинний код після додавання noexcept.

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

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

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

Я також рекомендую книгу Скотта Майєрса "Ефективні сучасні C ++", "Пункт 14: Декларуйте функції, за винятком випадків, якщо вони не випускають винятків" для подальшого читання.


І все-таки було б набагато більше сенсу, якби винятки були впроваджені на C ++, як у Java, де ви позначаєте метод, який може кидати throwsключове слово замість noexceptмінус. Я просто не можу отримати вибір дизайну на C ++ ...
doc

Вони назвали його noexceptтому, що throwвже було взято. Простіше кажучи, throwможна використовувати майже так, як ви згадуєте, за винятком того, що вони розробили його дизайн, так що він став майже марним - навіть згубним. Але ми зациклювалися на цьому зараз, оскільки його усунення було б суттєвою зміною з малою користю. Так noexceptв основному throw_v2.
AnorZaken

Як throwне корисно?
цікавогут

@curiousguy "кинути" сам (для викидання винятків) корисно, але "кинути" як специфікатор винятків було вимкнено, а в C ++ 17 навіть видалено. З причин , чому виключення специфікаторів не є корисними, побачити це питання: stackoverflow.com/questions/88573 / ...
Philipp Classen

1
@ PhilippClaßen Специфікатор throw()винятку не надав тієї ж гарантії, що і nothrow?
допитливий хлопець

17

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

Коли ви говорите "я знаю [вони] ніколи не кинуть", ви маєте на увазі, вивчаючи реалізацію функції, ви знаєте, що функція не кине. Я думаю, що такий підхід всередину.

Краще розглянути, чи може функція кидати винятки, щоб бути частиною конструкції функції: настільки ж важлива, як список аргументів і чи метод є мутатором (... const). Заявлення про те, що "ця функція ніколи не викидає винятків" є обмеженням для реалізації. Якщо відмінити це, це не означає, що функція може викидати винятки; це означає, що поточна версія функції та всі майбутні версії можуть кидати винятки. Це обмеження, яке ускладнює реалізацію. Але деякі методи повинні мати обмеження, щоб бути практично корисним; найголовніше, щоб їх можна було викликати від деструкторів, а також для впровадження «відкатного» коду в методах, що забезпечують чітку гарантію виключення.


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

Дивіться також пов'язане з Java питання .
Raedwald
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.