Чому Clang / LLVM попереджає мене про використання за замовчуванням у операторі комутатора, де охоплені всі перелічені випадки?


34

Розглянемо наступне твердження enum and switch:

typedef enum {
    MaskValueUno,
    MaskValueDos
} testingMask;

void myFunction(testingMask theMask) {
    switch (theMask) {
        case MaskValueUno: {}// deal with it
        case MaskValueDos: {}// deal with it
        default: {} //deal with an unexpected or uninitialized value
    }
};

Я програміст Objective-C, але це я написав у чистому С для широкої аудиторії.

Clang / LLVM 4.1 з -Weverything попереджає мене у рядку за замовчуванням:

Мітка за замовчуванням у перемикачі, яка охоплює всі значення перерахування

Тепер я можу розібратися, чому це саме так: в ідеальному світі єдині значення, що вводяться в аргумент, theMaskбули б у перерахунку, тому за замовчуванням не потрібно. Але що робити, якщо якийсь хак прийде разом і кине неініціалізований інт у мою прекрасну функцію? Моя функція буде забезпечуватися як падіння бібліотеки, і я не маю ніякого контролю над тим, що могло б туди потрапити. Використання default- це дуже акуратний спосіб вирішення цього питання.

Чому боги LLVM вважають цю поведінку недостойною свого пекельного пристрою? Чи повинен я передувати цьому твердженням if, щоб перевірити аргумент?


1
Я мушу сказати, моя причина - Все є в тому, щоб зробити себе кращим програмістом. Як NSHipster каже : "Pro tip: Try setting the -Weverything flag and checking the “Treat Warnings as Errors” box your build settings. This turns on Hard Mode in Xcode.".
Swizzlr

2
-Weverythingможе бути корисним, але будьте обережні щодо того, щоб занадто сильно змінити код, щоб з ним боротися. Деякі з цих попереджень не тільки непридатні, але контрпродуктивні, і найкраще їх вимкнути. (Дійсно, саме такий випадок використання -Weverything: почніть з нього і вимкніть те, що не має сенсу.)
Стівен Фішер

Чи можете ви трохи розширити попередження про попередження? Зазвичай їх більше, ніж це.
Стівен Фішер

Прочитавши відповіді, я все ще віддаю перевагу вашому початковому рішенню. Використання твердження IMHO за замовчуванням краще, ніж альтернативи, наведені у відповідях. Лише невелика примітка: відповіді дійсно хороші та інформативні, вони показують класний спосіб вирішити проблему.
bbaja42

@StevenFisher Це було все попередження. І як зазначив Кілліан, якщо я змінити перерахунок пізніше, це відкриє можливість для дійсних значень перейти до реалізації за замовчуванням. Здається, це гарний шаблон дизайну (якщо це така штука).
Swizzlr

Відповіді:


31

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

void myFunction(testingMask theMask) {
    assert(theMask == MaskValueUno || theMask == MaskValueDos);
    switch (theMask) {
        case MaskValueUno: {}// deal with it
        case MaskValueDos: {}// deal with it
    }
}

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

Тепер ви стурбовані тим, що хтось може викликати вашу функцію зі значенням, яке знаходиться поза перерахуванням. Це звучить як невиконання обов'язкової функції функції: документально очікувати значення від testingMaskперерахування, але програміст передав щось інше. Тому зробіть помилку програміста, використовуючи assert()(або, NSCAssert()як ви сказали, ви використовуєте Objective-C). Зробіть збиток програми повідомленням, в якому пояснюється, що програміст робить це неправильно, якщо програміст робить це неправильно.


6
+1. Як невелика модифікація, я вважаю за краще кожен випадок return;і додавати його assert(false);до кінця (замість того, щоб повторювати себе, перераховуючи юридичні переліки в початковому assert()та в останньому switch).
Джош Келлі

1
Що робити, якщо неправильну перерахунок не передає німий програміст, а скоріше помилка пошкодження пам’яті? Коли водій натискає на розрив своєї Toyota і помилка зіпсувала перешкоду, тоді програма злому повинна вийти з ладу та згоріти, а водієві повинен бути текст: "програміст робить це неправильно, поганий програміст! ". Я не зовсім бачу, як це допоможе користувачеві, перш ніж вони проїхатимуть через край скелі.

2
@Lundin - це те, що робить ОП, чи це безглуздий теоретичний крайній випадок, який ви тільки що сконструювали? Незалежно від того, що "помилка пошкодження пам'яті" [i] є помилкою програміста, [ii] - це не те, з чого можна продовжувати змістовно (принаймні, не з вимогами, зазначеними в питанні).

1
@GrahamLee Я просто кажу, що "лягай і помирай" - це не обов'язково найкращий варіант, коли ви помічаєте несподівану помилку.

1
У ньому є нова проблема: ви можете дозволити твердженню і випадки випадково вийти з синхронізації.
користувач253751

9

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

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


1
Але моя стурбованість - це брудна змінна, і захист від цього (наприклад, як я вже сказав, неініціалізований int). Мені здається, що в такому випадку оператор перемикання міг би досягти дефолту.
Swizzlr

Я не знаю на увазі визначення для Objective-C, але я припускав, що компілятор буде примусово вводити typedef'd enum. Іншими словами, "неініціалізоване" значення може ввести метод лише в тому випадку, якщо у вашій програмі вже є невизначена поведінка, і я вважаю цілком розумним, що компілятор не враховує це.
Кіліан Фот

3
@KilianFoth ні, вони ні. Перерахунки Objective-C - це перерахунки на C, а не переліки Java. Будь-яке значення базового інтегрального типу може бути присутнім в аргументі функції.

2
Також перерахунки можуть встановлюватися під час виконання з цілих чисел. Таким чином, вони можуть містити будь-яке значення, яке можна уявити.

ОП правильно. тестуванняМаска m; myFunction (м); швидше за все, потрапить у випадку за замовчуванням.
Меттью Джеймса Бріггса

4

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

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

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


(1) MISRA-C: 2004 р. 15.3.


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

3
Кланг не плутається; явно було запропоновано надати всі попередження, включаючи не завжди корисні, наприклад, стилістичні застереження. (Я особисто вмикаюсь у це попередження, тому що я не хочу за замовчуванням у своїх перемикачах enum; наявність їх означає, що я не отримую попередження, якщо я додаю значення enum і не оброблю його. З цієї причини я завжди обробляю випадок поганої вартості поза перерахуванням.)
Йенс Айтон

2
@JensAyton Це плутати. Усі професійні інструменти для статичного аналізатора, а також усі компілятори, сумісні з MISRA, подадуть попередження, якщо у викладенні перемикання не вистачає оператора за замовчуванням. Спробуйте самі.

4
Кодування до MISRA-C - не єдине дійсне використання компілятора C, і, знову ж таки, -Weverythingвмикає кожне попередження, а не вибір, відповідний конкретному випадку використання. Не відповідає вашому смаку - це не те саме, що плутанина.
Йенс Айтон

2
@Lundin: Я впевнений, що дотримання MISRA-C не магічно робить програми безпечними та без помилок ... і я однаково впевнений, що є багато безпечного, без помилок коду С від людей, які ніколи навіть не чула про MISRA. Насправді це трохи більше, ніж чиясь (популярна, але не безперечна) думка, і є люди, які вважають деякі її правила довільними, а іноді навіть шкідливими поза контекстом, для якого вони були розроблені.
cHao

4

Ще краще:

typedef enum {
    MaskValueUno,
    MaskValueDos,

    MaskValue_count
} testingMask;

void myFunction(testingMask theMask) {
    assert(theMask >= 0 && theMask<MaskValue_count);
    switch theMask {
        case MaskValueUno: {}// deal with it
        case MaskValueDos: {}// deal with it
    }
};

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


2
Це забруднює тип значеннями, які ви просто не хочете, щоб тип містив. Тут є схожість зі старим хорошим WTF тут: thedailywtf.com/articles/What_Is_Truth_0x3f_
Мартін Сухіарто

4

Але що робити, якщо якийсь хак прийде разом і кине неініціалізований інт у мою прекрасну функцію?

Тоді ви отримуєте не визначену поведінку , і ваша defaultбуде безглуздою. Ви нічого не можете зробити, щоб зробити це краще.

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


3
Як щодо того, щоб записати його або замість цього викинути виняток, якщо мовчки проігнорувати його?
jgauffin

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

1
Ні, він нічого не впорається. Це UB. У програмі є UB в момент введення функції, і орган функції нічого не може з цим робити.
DeadMG

2
@DeadMG Перерахунок може мати будь-яке значення, яке може мати відповідний цілий тип у реалізації. У цьому немає нічого невизначеного. Доступ до вмісту неініціалізованої автоматичної змінної - це справді UB, але "злий хак" (який, швидше за все, якась помилка) може також кинути чітко визначене значення функції, хоча це не одне із значень зазначені в декларації про перерахування.

2

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

Усім, що ви знаєте, компілятор може компілювати цей комутатор (за замовчуванням) як:

if (theMask == MaskValueUno)
  // Execute something MaskValueUno code
else // theMask == MaskValueDos
  // Execute MaskValueDos code

Після того, як ви спровокуєте невизначену поведінку, назад не буде.


2
По-перше, це не визначене значення , а не визначена поведінка. Це означає, що компілятор може приймати якесь інше значення, яке є більш зручним, але він може не перетворити Місяць на зелений сир і т. Д. По-друге, наскільки я можу сказати, це стосується лише C ++. У C діапазон значень enumтипу такий самий, як діапазон значень його цілого цілого типу (який визначається реалізацією, отже, повинен бути самостійним).
Єнс Айтон

1
Однак екземпляр змінної перерахунку та константа перерахування не обов'язково повинні мати однакову ширину та підпис, вони обидві визначені impl.defined. Але я майже впевнений, що всі перерахунки в C оцінюються точно так само, як їх відповідний цілий тип, тому там немає невизначеної або навіть не визначеної поведінки.

2

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

  • Конкретне попередження (або помилка, якщо також кидання -Werror) походить -Wcovered-switch-default(із, -Weverythingале ні -Wall). Якщо ваша моральна гнучкість дозволяє вам вимкнути певні попередження (наприклад, викреслити деякі речі з -Wallабо -Weverything), розгляньте можливість кидання -Wno-covered-switch-default(або -Wno-error=covered-switch-defaultпід час використання -Werror), і взагалі -Wno-...щодо інших попереджень ви вважаєте незгодними.
  • Для gcc(і більш загальної поведінки clang), зверніться до gccсторінці Довідника -Wswitch, -Wswitch-enum, -Wswitch-defaultдля (різні) поведінки в подібних ситуаціях , перерахованих типів в операторах перемикання.
  • Мені також не подобається це попередження в концепції, і мені не подобається його формулювання; для мене слова з попередження ("мітка за замовчуванням ... охоплює всі ... значення") говорять про те, що default:справа завжди буде виконуватися, наприклад

    switch (foo) {
      case 1:
        do_something(); 
        //note the lack of break (etc.) here!
      default:
        do_default();
    }

    У першому читанні, це те , що я думав , що ви працюєте в , - що ваш default:випадок не завжди буде виконано , так як там немає break;або return;або подібне. Ця концепція схожа (на моє вухо) на інші коментарі в стилі няні (хоч і час від часу корисні) коментарі, які випливають з clang. Якщо foo == 1обидві функції будуть виконані; ваш код вище має таку поведінку. Тобто, не вдалося пробитись лише в тому випадку, якщо ви хочете, можливо, зберегти виконання коду з наступних випадків! Однак це не є вашою проблемою.

Загрожуючи педантичністю, деякі інші думки про повноту:

  • Однак я вважаю, що така поведінка (більше) відповідає агресивній перевірці типу інших мов чи компіляторів. Якщо, як ви припускаєте, деякий негідник робить спробу передати intабо що - то в цю функцію, яка явно яка має намір споживати свій власний тип специфічного, ваш компілятор повинен захищати вас однаково добре в цій ситуації з агресивним попередженням або помилкою. Але це не так! (Тобто, здається, що принаймні gccі clangне робіть enumперевірки типу, але я чую, що iccце робиться ). Оскільки ви не отримуєте безпеку типу , ви можете отримати цінність- безпеку, як обговорювалося вище. В іншому випадку, як це пропонується в TFA, розгляньте structабо щось, що може забезпечити безпеку типу.
  • Іншим способом вирішення може бути створення нового "значення" у вашому enum, як MaskValueIllegal, а не підтримка цього caseу вашому switch. Це було б з'їдене default:(крім будь-якого іншого дурного значення)

Хай живе захисне кодування!


1

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

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

void myFunction(testingMask theMask) {
    int maskValue = int(theMask);
    switch(maskValue) {
        case MaskValueUno: {} // deal with it
        case MaskValueDos: {}// deal with it
        default: {} //deal with an unexpected or uninitialized value
    }
}

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


1
цю публікацію досить важко читати (стіна тексту). Ви б не хотіли відредагувати його на кращу форму?
гнат
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.