Clang / LLVM neden tüm numaralandırılmış durumların ele alındığı bir switch ifadesinde varsayılan kullanım konusunda beni uyarıyor?


34

Aşağıdaki enum ve switch deyimini göz önünde bulundurun:

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
    }
};

Ben bir Objective-C programcısıyım, ancak bunu daha geniş bir kitleye C saf yazdım.

-Weverything olan Clang / LLVM 4.1, beni varsayılan satırda uyarır:

Tüm numaralandırma değerlerini kapsayan anahtardaki varsayılan etiket

Şimdi, bunun neden orada olduğunu görebiliyorum: mükemmel bir dünyada, tartışmaya giren tek değerler enumda theMaskolacaktı, bu nedenle varsayılan gerekmiyor. Peki ya bazı kesmek ortaya çıkar ve güzel işlevime başlatılmamış bir int atarsa? İşlevim kütüphanede bir düşüş olarak sağlanacak ve orada neler olabileceği üzerinde hiçbir kontrolüm yok. Kullanmak default, bunu ele almanın çok temiz bir yoludur.

LLVM tanrıları neden bu davranışı infernal cihazlarının değersiz görmektedir? Argümanı kontrol etmek için bundan önce bir if ifadesiyle mi başlamalıyım?


1
Şunu söylemeliyim ki, her şeyin sebebi kendimi daha iyi bir programcı yapmak. As NSHipster diyor : "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
-Weverythingfaydalı olabilir, ancak kodunuzu değiştirmek için çok fazla mutasyona dikkat edin. Bu uyarıların bazıları sadece değersiz değil, aynı zamanda karşı-üretken ve en iyi şekilde kapatılmış. (Gerçekten de, bunun için kullanım örneği -Weverything: onunla başla ve mantıklı olmayan şeyleri kapat.)
Steven Fisher

Posterity için uyarı mesajı biraz genişletebilir misiniz? Onlardan genellikle daha fazlası vardır.
Steven Fisher

Cevapları okuduktan sonra hala ilk çözümünüzü tercih ediyorum. Varsayılan ifade IMHO kullanımı cevaplarda verilen alternatiflerden daha iyidir. Sadece küçük bir not: cevaplar gerçekten çok iyi ve bilgilendirici, sorunun nasıl çözüleceğini gösterecek güzel bir yol.
bbaja42

@StevenFisher Tüm uyarı buydu. Ve Killian'ın belirttiğim gibi, enumumu daha sonra değiştirirsem, geçerli değerlerin varsayılan uygulamaya geçme ihtimalini açar. İyi bir tasarım deseni gibi görünüyor (eğer böyle bir şeyse).
Swizzlr,

Yanıtlar:


31

İşte ne sorun clang'ın raporlamasından ne de koruduğunuzdan muzdarip olan bir versiyonu:

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

Killian zaten clang'ın neden uyarı verdiğini açıkladı: enum'u uzattıysanız, muhtemelen istediğiniz şey olmayan varsayılan duruma düşersiniz. Yapılacak doğru şey, varsayılan davayı kaldırmak ve işlenmeyen koşullar için uyarılar almaktır .

Artık birisinin işlevinizi numaralandırma dışında bir değerle çağırabileceğinden endişe ediyorsunuz. Bu, işlevin ön koşulunu yerine getirememe gibi görünüyor: testingMasknumaralandırmadan bir değer beklediği belgelenmiştir ancak programcı başka bir şeyden geçmiştir. Bu yüzden kullanarak bir programcı hatası yapınassert() (ya NSCAssert()da Objective-C kullanıyormuşsunuz gibi). Programlayıcınız yanlış yaparsa, programlayıcının yanlış yaptığını açıklayan bir mesajla programınızın kilitlenmesini sağlayın.


6
+1. Küçük bir değişiklik olarak, her bir vakayı almayı ve sonuna return;bir son assert(false);vermeyi tercih ederim (yasal numaraların başında assert()ve içinde listeleyerek kendimi tekrarlamak yerine switch).
Josh Kelley

1
Ya yanlış numaralandırma aptal bir programcı tarafından değil, hafıza bozulma hatası yüzünden geçerse? Sürücü Toyota'nın molasına bastığında ve bir hata enum'u bozduğunda, daha sonra işleme programı çökmeli ve yanmalı ve sürücüye görüntülenen bir metin olmalı: "programcı yanlış yapıyor, kötü programcı! ". Bir uçurumun kenarından geçmeden önce, bunun kullanıcıya nasıl yardımcı olacağını pek bilmiyorum.

2
@ Lundin, OP'nin yaptığı şey mi, yoksa yeni kurduğunuz anlamsız bir teorik kenar vakası mı? Ne olursa olsun, bir "bellek bozulması hatası" [i] programcı hatasıdır, [ii] anlamlı bir şekilde devam edebileceğiniz bir şey değildir (en azından soruda belirtilen şartlarla).

1
@GrahamLee Ben sadece "uzan ve öl" beklenmedik bir hata tespit ettiğinizde mutlaka en iyi seçenek olmadığını söylüyorum.

1
İddia ve davaların yanlışlıkla senkronize edilmesine izin verebileceğiniz yeni bir problem var.
kullanıcı253751,

9

Burada defaultetiket olması , beklediğiniz şey hakkında kafanızın karıştığının bir göstergesidir. Tüm olası enumdeğerleri açık bir şekilde tükettiğiniz için default, muhtemelen çalıştırılamaz ve gelecekteki değişikliklere karşı korunmalarına gerek yoktur, çünkü eğer genişletirseniz enum, o zaman yapı zaten bir uyarı verecektir .

Dolayısıyla, derleyici tüm üsleri kapladığınızı ancak henüz sahip olmadığınızı düşünüyor gibi göründüğünü fark eder ve bu her zaman kötü bir işarettir. switchBeklenen forma geçmek için en az çabayı göstererek, derleyiciye yaptığınız şeyin gerçekte ne yaptığınızı ve bunu bildiğinizi gösterirsiniz.


1
Ancak benim endişem kirli bir değişken ve buna karşı korunma (dediğim gibi, başlatılmamış bir int). Bana öyle geliyor ki, böyle bir durumda switch ifadesinin temerrüde düşmesi mümkün olacak.
Swizzlr

Objective-C tanımını tam anlamıyla bilmiyorum, ancak bir typdef'd enum'un derleyici tarafından uygulanacağını varsaydım. Diğer bir deyişle, program eğer bir "başlatılmamış" değeri yalnızca yöntemi girebilir zaten tanımlanmamış bir davranış sergiler ve bunu tamamen makul bir derleyici bu hesaba katmalı yok bulabilirsiniz.
Kilian Foth

3
@KilianHiçbiri hayır, değil. Objective-C kodları, Java kodları değil, C kodlarıdır. Altında yatan entegre tip herhangi bir değer olabilir işlev bağımsız değişkeni mevcut.

2
Ayrıca, sayılar çalışma zamanında tam sayılardan ayarlanabilir. Böylece akla gelebilecek herhangi bir değeri içerebilirler.

OP haklı. testMask m; myFunction (m); büyük olasılıkla varsayılan davaya çarpacaktı.
Matthew James Briggs,

4

Clang'ın kafası karışık, varsayılan bir ifadeye sahip olan mükemmel bir uygulama var, savunma programlaması olarak bilinir ve iyi programlama uygulaması olarak kabul edilir (1). Belki de masaüstü programlamasında kullanılmasa da, görev kritik sistemlerde bolca kullanılır.

Savunma programlamasının amacı, teoride asla yaşanmayacak olan beklenmeyen hataları yakalamaktır. Bu tür beklenmeyen bir hata, fonksiyonun yanlış bir girdi veya hatta bir "kötü saldırı" vermesi için programcının mutlaka olması gerekmez. Daha büyük olasılıkla, bozuk bir değişken olabilir: arabellek taşması, yığın taşması, kaçak kodu ve işlevinizle ilgili olmayan benzeri hatalar buna neden olabilir. Gömülü sistemler söz konusu olduğunda değişkenler EMI nedeniyle, özellikle de harici RAM devreleri kullanıyorsanız değişebilir.

Varsayılan ifadenin içine ne yazdığına gelince ... eğer programın oraya gittiğinde haywire gittiğinden şüpheleniyorsan, bir çeşit hata işlemine ihtiyacın var. Pek çok durumda, muhtemelen beklenmedik bir durumda olduğunu düşündüğünüzü göstermek için "beklenmedik ama önemli değil" şeklinde bir yorum içeren boş bir ifade ekleyebilirsiniz.


(1) MISRA-C: 2004 15.3.


1
Lütfen ortalama PC programlayıcısının tipik olarak tamamen savunma programlarını tamamen yabancı bulmaya başladığını unutmayın, çünkü program hakkındaki görüşleri teoride yanlış olabilecek hiçbir şeyin pratikte yanlış gidemeyeceği soyut bir ütopyadır. Bu nedenle, kime sorduğunuza bağlı olarak sorunuza oldukça çeşitli cevaplar alacaksınız.

3
Kafes karışmaz; özel olarak , uyarıcı uyarılar gibi her zaman kullanışlı olmayan uyarıları içeren tüm uyarıları sağlaması istenmiştir . (Kişisel olarak bu uyarıyı seçiyorum çünkü enum anahtarlarımda varsayılanlar istemiyorum; enum değeri eklersem ve bunu yapmazsam uyarı almamam anlamına geliyor. Bu nedenle, her zaman enum dışında kötü değer vakası.)
Jens Ayton

2
@JensAyton Kafası karışmış. Tüm profesyonel statik analizör araçları ve tüm MISRA uyumlu derleyiciler, bir switch ifadesi varsayılan bir ifadeden yoksunsa bir uyarı verecektir . Birini kendin dene.

4
MISRA-C'ye Kodlama yine, bir C derleyicisi için geçerli kullanım değildir ve -Weverythingyanar her uyarı değil, belirli kullanım alanına bir seçim uygun. Zevkinize uymamak karışıklık ile aynı şey değil.
Jens Ayton

2
@ Lundin: MISRA-C'ye bağlı kalmanın sihirli bir şekilde programları güvenli ve hatasız hale getirmediğinden eminim ... ve kesinlikle hiç kullanmayan insanlardan da pek çok güvenli, hatasız C kodu olduğundan eminim. MISRA'yı duydum . Aslında, birinin (popüler fakat tartışmasız) görüşünden biraz daha fazlası ve bazı kurallarını kendilerinin tasarladıkları bağlamın dışında keyfi ve hatta bazen zararlı bulduğu insanlar var.
cHao

4

Hala daha iyi:

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
    }
};

Bu, enuma öğe eklerken daha az hataya meyillidir. Enum değerlerinizi işaretsiz yaparsanız testi> = 0 için atlayabilirsiniz. Bu yöntem yalnızca enum değerlerinde boşluk yoksa, işe yarar, ancak bu genellikle böyle olur.


2
Bu, türü yalnızca içermesini istemediğiniz değerlerle kirletiyor. : Burada güzel eski WTF ile benzerlikler vardır thedailywtf.com/articles/What_Is_Truth_0x3f_
Martin Sugioarto

4

Peki ya bazı kesmek ortaya çıkar ve güzel işlevime başlatılmamış bir int atarsa?

O zaman Tanımsız Davranış elde edersiniz ve defaultanlamınız olmaz. Bunu daha iyi yapmak için yapabileceğin hiçbir şey yok.

Daha net olalım. Birisi intsizin fonksiyonunuza bir başlatılmamış geçerse , o Tanımsız Davranış. Göreviniz Halting Problemini çözebilir ve bunun önemi olmaz. Bu UB. UB çağrıldıktan sonra yapabileceğiniz hiçbir şey yoktur.


3
Sessizce yoksaymak yerine günlüğe kaydetmeye veya istisna atmaya ne dersiniz?
jgauffin

3
Gerçekten de, örneğin ... anahtara varsayılan bir ifade eklemek ve hatayı incelikle ele almak gibi yapılabilecek çok şey var. Bu savunma programlaması olarak bilinir . Yalnızca aptal programcıya, işlevi yanlış girdiler

1
Hayır, hiçbir şey halletmeyecek. Bu UB. Program, fonksiyona girildiği anda UB'ye sahiptir ve fonksiyon gövdesi bu konuda hiçbir şey yapamaz.
DeadMG

2
@DeadMG Bir enum, uygulamadaki karşılık gelen tamsayı türünün sahip olabileceği herhangi bir değere sahip olabilir. Bu konuda tanımsız bir şey yok. Başlatılmamış bir otomatik değişkenin içeriğine gerçekten erişmek UB'dir, ancak "kötü kesmek" (bu muhtemelen bir tür hata olabilir), değerlerden biri olmasa da, işleve iyi tanımlanmış bir değer atar. Enum bildirgesinde listelenmiştir.

2

Varsayılan ifade mutlaka yardımcı olmaz. Anahtar bir numaralamanın üzerindeyse, numarada tanımlanmayan herhangi bir değer tanımsız davranışı gerçekleştirir.

Bildiğiniz kadarıyla, derleyici bu anahtarı (varsayılan olarak) aşağıdaki gibi derleyebilir:

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

Tanımsız davranışı tetikledikten sonra geri dönüş olmaz.


2
İlk olarak, tanımlanmamış bir davranış değil, tanımlanmamış bir değer . Bu, derleyicinin daha uygun başka bir değer alabileceği, ancak ayı yeşil peynire, vb. Getiremeyeceği anlamına gelir. İkincisi, söyleyebileceğim kadarıyla, bu sadece C ++ için geçerlidir. C'de, bir enumtürün değer aralığı, altında yatan tamsayı tipindeki değerlerin aralığı ile aynıdır (uygulama tanımlıdır, bu nedenle kendi kendine tutarlı olmalıdır).
Jens Ayton

1
Bununla birlikte, bir enum değişkeninin ve bir numaralandırma sabitinin bir örneği mutlaka aynı genişliğe ve imzaya sahip olmak zorunda değildir, her ikisi de tanımlanmıştır. Ancak, C'deki tüm değerlerin tam olarak karşılık gelen tamsayı türü gibi değerlendirildiğinden eminim, bu nedenle tanımlanmamış veya belirtilmemiş bir davranış yok.

2

Ayrıca default:her durumda bir tane almayı tercih ederim . Her zamanki gibi partiye geç kaldım, ama ... yukarıda görmediğim başka düşünceler:

  • Özel uyarı (ya da fırlatıldığında da hata -Werror) gelen -Wcovered-switch-default(gelen -Weverythingama olmayan -Wall) geliyor. Ahlaki esnekliğiniz, bazı uyarıları kapatmanıza izin veriyorsa (yani, bazı şeyleri -Wallveya bunlardan çıkarmayı-Weverything ), atmayı düşünün -Wno-covered-switch-default(veya -Wno-error=covered-switch-defaultkullanırken -Werror) ve genel -Wno-...olarak uygunsuz bulduğunuz diğer uyarılar için.
  • İçin gcc(ve daha genel davranış clang), danışmak gcciçin manpage -Wswitch, -Wswitch-enum, -Wswitch-defaultswitch ifadeleri sayılan türde benzer durumlarda için (farklı) davranışının.
  • Konseptdeki bu uyarıdan da hoşlanmıyorum ve ifadelerini de sevmiyorum; bana göre, uyarıdaki kelimeler ("varsayılan etiket ... tüm değerleri kapsar ... değerleri"), default:durumun her zaman yürütüleceğini, örneğin

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

    İlk okuduğumda, bununla karşılaştığınızı düşündüğüm şeydi - default:davanızın her zaman idam edileceği, çünkü hiçbir şey break;ya return;da benzeri bir şey yoktu . Bu kavram (kulağımla), diğerlerinden dağıttığı diğer dadı tarzı (bazen yararlı olsa da) yorumuna benzer clang. Eğer foo == 1iki fonksiyon da yerine getirilirse; Yukarıdaki kodunuzda bu davranış var. Yani, yalnızca sonraki vakalardan kod çalıştırmaya devam etmek istemeniz durumunda, ayrılma konusunda başarısız olun! Ancak bu, sizin probleminiz değil gibi görünüyor.

Pedantik olma riski altında olması için başka bazı düşünceler:

  • Bununla birlikte, bu davranışın (daha fazla) diğer dillerde veya derleyicilerde agresif tür denetimi ile tutarlı olduğunu düşünüyorum. Eğer hipotez gibi bazı günahkar, varsa yok bir pas girişiminde intaçıkça kendi özgül türünü tüketmek niyetinde olan bu fonksiyonun, veya bir şey, sizin derleyici saldırgan bir uyarı veya hata ile bu durumda eşit derecede iyi korumak gerekir. AMA değil! (Yani, o en azından görünüyor gccve clangyapmayın enumtip denetimi, ama ben duymak iccyapar ). Eğer almıyorsanız beri tip -Emniyet, sen alabilir değeri yukarıda tartışıldığı gibi -Güvenlik. Aksi takdirde, TFA’da önerildiği gibi, structtip güvenliği sağlayabilecek bir şey veya bir şey düşünün .
  • Başka bir geçici çözüm sizin de yeni bir "değer" oluşturarak olabilir enumgibi MaskValueIllegalve bu desteklemeyen caseGözlerinde farklı switch. Bu default:(başka bir tuhaf değere ek olarak) tarafından yemiş

Yaşasın savunma kodlaması!


1

İşte size alternatif bir öneri:
OP, bir kişinin enum'un beklendiği intyerde geçtiği davaya karşı korumaya çalışıyor . Veya daha büyük olasılıkla, birisinin daha fazla vaka içeren daha yeni bir başlık kullanarak eski bir kütüphaneyi daha yeni bir programla ilişkilendirdiği durumlarda.

intDavayı ele almak için neden anahtarı değiştirmiyorsunuz ? Anahtardaki değerin önüne bir döküm eklemek, uyarıyı ortadan kaldırır ve hatta varsayılanın neden olduğu hakkında bir ipucu sağlar.

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
    }
}

Bunu , olası değerlerin her birinin test edilmesinden ya da enum değer aralığının iyi sıralandığı ve daha basit bir testin işe yaradığı varsayımıyla yapılmasından daha az sakıncalı buluyorum . Bu, varsayılanın tam ve güzelce yaptığı şeyi yapmanın çirkin bir yoludur.assert()


1
Bu yazı okumak oldukça zordur (metin duvarı). Sakıncası var düzenleyebilir daha iyi bir şekle ing?
gnat
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.