C ++ 'tanımsız davranış' (UB) ve C # veya Java gibi diğer diller neden yok?


50

Bu Yığın Taşması yayını , C / C ++ dil belirtiminin 'tanımsız davranış' olarak ilan ettiği durumların oldukça kapsamlı bir listesini listeler. Ancak, neden C # veya Java gibi diğer modern dillerin 'tanımsız davranış' kavramına sahip olmadığını anlamak istiyorum. Derleyici tasarımcı tüm olası senaryoları (C # ve Java) kontrol edebilir mi, yoksa (C ve C ++) kontrol edebilir mi?




3
ve yine de bu SO yazısı, Java özelliğinde bile tanımsız davranışa işaret ediyor!
gbjbaanb

“Neden C ++ 'Tanımsız Davranışa Sahiptir?” ” Maalesef, bu, X, Y ve / veya Z (tümü olabilir nullptr) nedenlerinden ötürü, nesnel olarak ifadenin ötesinde yanıtlaması zor olan sorulardan biri gibi görünüyor. biri, davranışı önerilen bir şartname yazarak ve / veya benimseyerek tanımlamaktan rahatsız oldu ". : c
code_dredd

Ben öncül meydan okurdum. En azından C # "güvensiz" kod içeriyor. Microsoft, "Bir anlamda, güvenli olmayan kod yazmak, bir C # programına C kodu yazmaktan çok daha fazlası" yazıyor ve neden böyle bir şey yapmak istediğine örnek sebepler veriyor: donanıma veya işletim sistemine erişmek ve hız için. C bunun için icat edildi (cehennem, işletim sistemini C'ye yazdılar !), İşte orada.
Peter - Monica

Yanıtlar:


72

Tanımlanmamış davranış, yalnızca geçmişe bakıldığında çok kötü bir fikir olarak kabul edilen şeylerden biridir.

İlk derleyiciler büyük başarılar elde etti ve alternatif - makine dili ya da meclis dili programlamasına nazaran gelişmeleri memnuniyetle karşıladı. Bununla ilgili sorunlar iyi biliniyordu ve üst düzey diller bilinen sorunları çözmek için özel olarak icat edildi. (O zamanki coşku o kadar büyüktü ki, HLL'ler “programlamanın sonu” olarak selamlanan somtimlerdi - sanki bundan sonra sadece istediklerimizi önemsiz bir şekilde yazmak zorunda kalacağız ve derleyici tüm gerçek işleri yapacaktı.)

Daha yeni bir yaklaşımla ortaya çıkan yeni sorunları fark edemedik. Kodun çalıştığı gerçek makineden uzak olmak, sessizce, yapmasını beklediğimiz şeyi yapmayan şeylerin daha fazla olabileceği anlamına gelir. Örneğin, bir değişken tahsis etmek tipik olarak başlangıç ​​değerini tanımsız bırakır; bu bir problem olarak görülmedi, çünkü eğer bir değer tutmak istemeseydiniz bir değişken tahsis edemezdiniz, değil mi? Profesyonel programcıların başlangıç ​​değerini vermeyi unutmamalarını beklemek elbette çok fazla değildi, değil mi?

Daha güçlü kodlama sistemleri ve daha güçlü programlama sistemleriyle mümkün olan daha karmaşık yapılarla, evet, pek çok programcının zaman zaman bu denemeleri gerçekten yerine getireceği ve sonuçta tanımlanamayan davranışların büyük bir sorun haline geldiği ortaya çıktı. Bugün bile, güvenlik sızıntılarının küçüğünden korkunçlara kaçması, bir biçimde veya bir diğerinde tanımsız davranışların sonucudur. (Bunun nedeni, genellikle tanımsız davranışın aslında hesaplamada bir sonraki daha düşük seviyedeki şeyler tarafından çok tanımlanmış olmasıdır ve bu seviyeyi anlayan saldırganlar, bir program yapmak için bu kıkırdama odasını yalnızca istenmeyen şeyleri değil, tam olarak yapılan şeyleri kullanabilir. onlar niyetinde.)

Bunu fark ettiğimizden beri, tanımsız davranışı yüksek seviyeli dillerden kovma konusunda genel bir itici güç vardı ve Java bu konuda özellikle kapsamlıydı (ki zaten kendi tasarımlı sanal makinesinde çalışacak şekilde tasarlandığından beri nispeten kolaydı). C gibi daha eski diller, varolan büyük miktarda kodla uyumluluğunu yitirmeden kolayca bu şekilde yeniden yapılandırılamaz.

Düzenleme: Belirtildiği gibi, verimlilik başka bir nedendir. Tanımlanmamış davranış, derleyici yazarların, hedef mimariden faydalanma konusunda çok fazla çaba harcadığı anlamına gelir; Bu, dünkü güçlendirilmiş makinelerde, programcı maaşının genellikle yazılım geliştirme için bir darboğaz olması durumunda, bugün olduğundan daha önemliydi.


56
Birçok C topluluğunun bu ifadeye katılacağını sanmıyorum. C'ye sonradan uyarlama yapıp tanımsız davranışı tanımlarsanız (örneğin, her şeyi varsayılan olarak başlat, işlev parametresi, vb. İçin bir değerlendirme sırası seçtiyseniz), iyi işlenmiş kodun büyük tabanı mükemmel bir şekilde çalışmaya devam eder. Yalnızca bugün iyi tanımlanmayan kodlar bozuluyor. Diğer taraftan, bugün olduğu gibi tanımsız bırakırsanız, derleyiciler CPU mimarilerinde ve kod optimizasyonunda yeni gelişmelerden yararlanma konusunda özgür olmaya devam edecektir.
Christophe

13
Cevabın ana kısmı benim için gerçekten inandırıcı gelmiyor. Demek istediğim, int32_t add(int32_t x, int32_t y)C ++ 'da iki sayı (içinde olduğu gibi ) güvenle ekleyen bir işlev yazmak imkansız . Bunun etrafındaki olağan argümanlar verimlilikle ilgilidir, ancak genellikle bazı taşınabilirlik argümanları ile serpiştirilir ("Bir kez yaz, yazdığın platformda koş ... ... başka hiçbir yerde ;-)"). Kabaca, bu nedenle bir argüman şu şekilde olabilir: Bazı şeyler tanımsızdır, çünkü 16bit mikrokontrolcüde mi yoksa 64bit sunucuda mı olduğunuzu bilmiyorsunuz (zayıf biri, ancak hala bir argüman)
Marco13

12
@ Marco13 Anlaşıldı - ve "tanımsız davranış" sorunundan bir şey tanımlayarak "tanımlanmış davranış" yaparak kurtulmak, ancak kullanıcının istediği ve "tanımsız davranış" yerine "gerçekleştiği zaman hiçbir uyarı olmadan" IMO sadece kod avukatı oyunları oynamak değil .
alephzero

9
"Bugün bile, güvenlik sızıntılarının küçüğünden korkunçlara sızması, bir şekilde veya başka bir şekilde tanımlanmamış davranışların sonucudur." Kaynak belirtilmeli. Şimdi çoğunun XYZ enjeksiyonu olduğunu sanıyordum.
Joshua,

34
"Tanımlanmamış davranış, yalnızca geçmişe bakıldığında çok kötü bir fikir olarak kabul edilen şeylerden biridir." Bu senin görüşün. Birçokları (kendim dahil) paylaşmıyoruz.
Monica ile Hafiflik Yarışları

103

Temel olarak, Java ve benzer dil tasarımcılarının dillerinde tanımsız davranış istememeleri nedeniyle. Bu bir takas oldu - tanımsız davranışa izin vermek performansı arttırma potansiyeline sahip, ancak dil tasarımcıları güvenlik ve öngörülebilirliği daha yüksek önceliklendirdi.

Örneğin, C'de bir dizi tahsis ederseniz, veriler tanımsızdır. Java'da, tüm baytların 0 (veya başka bir belirli değer) olarak başlatılması gerekir. Bu, çalışma zamanının dizi üzerinde geçmesi gerektiği (C (n) işlemi) anlamına gelirken, C anında tahsisat yapabilir. C bu tür işlemler için her zaman daha hızlı olacaktır.

Diziyi kullanan kod okumadan önce onu yerleştirecekse, bu temelde Java için harcanan çabadır. Ancak, ilk önce kodun okunması durumunda, Java’da öngörülebilir sonuçlar alırsınız ancak C’de öngörülemeyen sonuçlar elde edersiniz.


19
HLL ikileminin mükemmel sunumu: güvenlik ve kullanım kolaylığı ile performans Gümüş mermi yoktur: Her iki taraf için de kullanım alanları vardır.
Christophe

5
@Christophe Adil olmak gerekirse, bir soruna UB'nin C ve C ++ gibi tamamen tartışmasız gitmesine izin vermekten daha iyi yaklaşımlar vardır. Yararlı olan yerlerde uygulamanız için, emniyetli ve yönetimli bir dilde, kaçış kapaklarını güvenli olmayan bölgelere koyabilirsiniz. TBH, C / C ++ programımı sadece "ihtiyacınız olan pahalı çalışma makinelerini takın, umurumda değil, ama bana meydana gelen TÜM'lerden bahsedin" yazan bir bayrakla derlemek gerçekten güzel olurdu ."
Alexander,

4
Başlatılmamış konumlarını kasıtlı olarak okuyan bir veri yapısına güzel bir örnek Briggs ve Torczon'un seyrek küme gösterimidir (örneğin, bkz. Codingplayground.blogspot.com/2009/03/… ). n) Java'nın zorla başlatılması ile.
Arch D. Robison

9
Verilerin başlatılmasının zorlanmasının, kırık programları daha öngörülebilir hale getirdiği doğru olsa da, amaçlanan davranışı garanti etmez: Algoritma, hatalı bir şekilde başlatılmış sıfırı yanlış okurken, anlamlı verileri okumayı beklerse, bu, sanki bir hatadır. biraz çöp oku. Bir C / C ++ programında, böyle bir hata valgrind, başlatılmamış değerin tam olarak nerede kullanıldığını gösterecek olan işlemi çalıştırarak görülebilir . valgrindJava kodunu kullanamazsınız , çünkü çalışma zamanı başlatma işlemini başlatır ve valgrindkontrolleri gereksiz kılar.
cmaster

5
@cmaster C # derleyicisinin başlatılmamış yerlerden okuma yapmanıza izin vermemesinin nedeni budur. Çalışma zamanı kontrollerine gerek yok, başlatma gerekli değil, sadece derleme zamanı analizi. Yine de bir takas, ancak potansiyel olarak atanmamış yerel halk arasında dallanma ile başa çıkmanın iyi bir yolunun olmadığı bazı durumlar var. Uygulamada, bunun başlangıçta kötü bir tasarım olmadığı ve karmaşık dallanmayı önlemek için (insanların ayrıştırması zor olan) kodu yeniden düşünerek daha iyi çözdüğü bir durum bulamadım, ama en azından mümkün.
Luaan

42

Tanımsız davranış, derleyiciye belirli sınırlarda veya başka koşullarda tuhaf veya beklenmedik (veya normal) bir şey yapma enlemini vererek önemli optimizasyon sağlar.

Bkz http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html

Başlatılmamış bir değişkenin kullanımı: Bu genellikle C programlarındaki sorunların kaynağı olarak bilinir ve bunları yakalamak için birçok araç vardır: derleyici uyarılarından statik ve dinamik analizörlere kadar. Bu, (Java'nın yaptığı gibi) kapsam dahilinde tüm değişkenlerin sıfır olarak başlatılmasını gerektirmeyerek performansı artırır. Çoğu skaler değişken için bu, fazla ek yüke neden olur, ancak yığın dizileri ve malloc'd belleği, özellikle de depolama genellikle tamamen üzerine yazıldığından, oldukça pahalı olabilecek bir depolama belleğine neden olur.


İşaretli tamsayı taşması: 'int' türündeki (örneğin) aritmetik taşarsa, sonuç tanımlanmaz. Bir örnek "INT_MAX + 1" in INT_MIN olması garanti edilmez. Bu davranış, bazı kodlar için önemli olan belirli optimizasyon sınıflarını etkinleştirir. Örneğin, INT_MAX + 1'in tanımsız olduğunu bilmek "X + 1> X" in "true" olarak optimize edilmesini sağlar. Çarpmayı bilmek "taşamaz" dır (çünkü bunu yapmak tanımsızdır), "X * 2/2" nin "X" e optimize edilmesini sağlar. Bunlar önemsiz gibi görünse de, bu tür şeyler genellikle iç içe ve makro genişleme ile ortaya çıkar. Bunun izin verdiği daha önemli bir optimizasyon, "<=" gibi döngüler içindir:

for (i = 0; i <= N; ++i) { ... }

Bu döngüde, derleyici, "i" nin taşma üzerinde tanımsız olması durumunda döngünün tam olarak N + 1 kez tekrarlanacağını varsayabilir, bu da geniş bir döngü optimizasyonunun başlamasını sağlar. Diğer taraftan, değişken tanımlanmışsa taşma üzerine sarılırsa, derleyici, döngünün muhtemelen sonsuz olduğunu varsaymalıdır (N, INT_MAX ise) - bu önemli döngü optimizasyonlarını devre dışı bırakır. Bu özellikle 64-bit platformları etkiler çünkü kodun çoğu indüksiyon değişkenleri olarak "int" kullanır.


27
Tabii ki, işaretli tamsayı taşmasının tanımlanmamasının gerçek nedeni, C geliştirildiğinde, kullanımda işaretli tamsayıların en az üç farklı temsilinin bulunmasıydı (birinin tamamlayıcısı, ikisinin tamamlayıcısı, işaret büyüklüğü ve belki de ofset ikili). ve her biri INT_MAX + 1 için farklı bir sonuç verir. Taşma tanımsız olarak tanımlanması , bir derleyicinin imzalı tamsayı aritmetiğinin başka bir biçimini simüle etmesini istemekten ziyade, her durumda a + byerel add b atalimatla derlenmesine izin verir .
Mark

2
Tamsayı taşmalarının gevşek bir şekilde tanımlanmış bir şekilde davranmasına izin vermek , tüm olası davranışların uygulama gereksinimlerini karşıladığı durumlarda önemli optimizasyonlara izin verir . Ancak, bu optimizasyonların çoğu, programcıların her ne pahasına olursa olsun tamsayı taşmalarını önlemek için gerekliyse kaybedilecektir.
supercat

5
@supercat Tanımlanmamış davranışlardan kaçınmanın daha yeni dillerde daha yaygın olmasının bir nedeni de budur - programcı zamanı CPU zamanından çok daha fazla değerlidir. UB sayesinde C'nin yapması gereken optimizasyon türleri, modern masaüstü bilgisayarlarda esasen anlamsızdır ve kodu zorlaştırır (güvenlik uygulamalarından bahsetmeden). Performans kritik kodlarında bile, C'de yapmak için biraz daha zor (ya da daha da zor) olacak yüksek seviyeli optimizasyonlardan faydalanabilirsiniz. C # yazılımında 3D oluşturucum var ve kullanabilmek HashSetharika bir şey.
Luaan

2
@supercat: Wrt_loosely tanım_, tamsayı taşması için mantıksal seçenek Uygulama Tanımlı Davranış gerektirmesiydi . Bu mevcut bir kavram ve uygulamalara aşırı bir yük getirmiyor. Birçoğu "2'nin etrafını sarmasıyla tamamlıyor" diyerek kaçardı. <<zor durum olabilir.
MSalters

@ MSalters Tanımlanmamış davranış ya da uygulama tanımlanmış davranış olmayan basit ve iyi çalışılmış bir çözüm vardır: özgün olmayan davranış. Yani, " x << ytürün geçerli bir değerine göre değerlendirir int32_tancak hangisini söyleyemeyiz" diyebilirsiniz. Bu, uygulayıcıların hızlı çözümü kullanmalarına izin verir, ancak zaman yolculuğu tarzı optimizasyonlarına izin veren yanlış bir önkoşul olarak davranmaz, çünkü geri dönüşümsüzlük bu tek işlemin çıktısıyla sınırlıdır - bu özellik, hafızanın, uçucu değişkenlerin, vb. Gözle görülür şekilde etkilenmemesini garanti eder ifade değerlendirmesi ile. ...
Mario Carneiro

20

C'nin ilk günlerinde, çok fazla kaos vardı. Farklı derleyiciler diline farklı davrandılar. Dil için bir spesifikasyon yazmaya ilgi olduğunda, bu şartnamenin programcıların derleyicileri ile güvendikleri C ile oldukça geriye dönük olması gerekir. Ancak, bu ayrıntıların bazıları taşınabilir değildir ve genel olarak anlamsızdır, örneğin belirli bir endianess veya veri düzenini varsaymak gibi. Bu nedenle C standardı, tanımlanmamış veya uygulama tarafından belirlenmiş davranış olarak birçok detayı saklar; bu, derleyici yazarlarına çok fazla esneklik bırakır. C ++ C üzerine kuruludur ve aynı zamanda tanımsız davranış sergiler.

Java, C ++ 'dan daha güvenli ve daha basit bir dil olmaya çalıştı. Java, dil semantiğini kapsamlı bir sanal makine açısından tanımlar. Bu, tanımlanmamış davranış için çok az yer bırakır, diğer yandan bir Java uygulamasının yapmasını zorlaştırmak için gereken gereksinimleri yapar (örneğin, bu referans atamaları atomik olmalı veya tam sayıların nasıl çalışması gerekir). Java'nın potansiyel olarak güvenli olmayan işlemleri desteklediği yerlerde, bunlar çalışma zamanında genellikle sanal makine tarafından kontrol edilir (örneğin, bazı yayınlar).


Yani, C ve C ++ 'nın tanımsız davranışlardan kurtulmamasının tek nedeni geriye dönük uyumluluk mu diyorsunuz?
Sisir

3
Kesinlikle en büyüklerinden biri, @Sisir. Hatta tecrübeli programcılar arasında, sen şaşıracaksın ne kadar kırmak olmamalıdır şeyler gelmez bir derleyici tanımsız davranışını nasıl işleyeceğini değiştirdiğinde bölünürler. ( "Dır GCC dışarı optimize başladığı Vaka noktasında, kaos biraz vardı this? Null" gerekçesiyle, bir süre önce denetler thisvarlık nullptraslında asla olamaz böylece UB, ve.)
Justin Zaman

9
@Sisir, başka bir büyük olan hızdır. C'nin ilk günlerinde, donanım bugün olduğundan çok daha heterojendi. INT_MAX'a 1 eklediğinizde ne olacağını belirtmemekle, derleyicinin mimarisi için en hızlı olanı yapmasına izin verebilirsiniz (örneğin, birinin tamamlayıcı sistemi -INT_MAX üretecek, ikisinin tamamlayıcı sistemi INT_MIN üretecektir). Benzer şekilde, bir dizinin sonunu okuduğunuzda ne olacağını belirtmeyerek, bellek korumalı bir sistemi programı sonlandırırken, pahalı olmayan çalışma zamanı sınırlarını denetlemenize gerek kalmayacak şekilde birini sonlandırabilirsiniz.
Mark

14

JVM ve .NET dilleri kolaylaşıyor:

  1. Doğrudan donanımla çalışmak zorunda değiller.
  2. Yalnızca modern masaüstü ve sunucu sistemleriyle veya makul şekilde benzer cihazlarla veya en azından onlar için tasarlanmış cihazlarla çalışmak zorundadırlar.
  3. Tüm bellekler için çöp toplama ve zorla başlatma işlemlerini uygulayarak göstergenin güvenliğini sağlayabilirler.
  4. Tek bir tanımlayıcı uygulamayı da sağlayan tek bir oyuncu tarafından belirlendiler.
  5. Performans üzerinden güvenliği seçiyorlar.

Seçimler için iyi noktalar olsa da:

  1. Sistem programlama tamamen farklı bir programdır ve bunun yerine uygulama programlaması için ödünsüz bir şekilde optimize edilmesi makul olur.
  2. Kuşkusuz, her zaman daha az egzotik donanım var, ancak küçük gömülü sistemler burada kalıyor.
  3. GC, fungus olmayan kaynaklar için uygun değildir ve iyi performans için çok daha fazla yer kaplar. Ve çoğu (ancak hemen hemen hepsi değil) zorunlu başlatmalar en iyi duruma getirilebilir.
  4. Daha fazla rekabet avantajı var, ancak komiteler uzlaşma demek.
  5. Tüm bu sınırlarla kontroller yapmak en uzağa optimize edilebilir olsa bile, kadar ekleyin. Null işaretçi kontrolleri çoğunlukla sanal adres alanı sayesinde sıfır genel gider için erişim yakalanarak gerçekleştirilebilir, ancak optimizasyon hala engellenmiştir.

Kaçış kapakları temin edildiğinde, bunlar tam gelişmiş tanımsız davranışı tekrar devreye sokarlar. Ancak, en azından, sadece çok kısa bir genişlikte kullanılırlar, bu nedenle manuel olarak doğrulanması daha kolaydır.


3
Aslında. İşim için C # programlarım. Her seferinde bir süre içerisinde güvensiz çekiçlerden birine ulaşıyorum ( unsafeanahtar kelime veya nitelikler System.Runtime.InteropServices). Bu şeyleri, idare edilemeyen şeylerin nasıl hata ayıklanacağını bilen birkaç programcıya bırakarak ve yine de pratikte olduğu kadar az tutarız. Performansla ilgili en son güvensiz çekiciden bu yana 10 yıldan fazla zaman geçti, ancak bazen yapmalısınız çünkü tam anlamıyla başka bir çözüm yok.
Joshua,

19
Sizeof (char) == sizeof (kısa) == sizeof (int) == sizeof (şamandıra) == 1. analog cihazlardan sıkça faydalanıyorum. ve C hakkındaki güzel şey, makul kod üreten uygun bir derleyiciye sahip olabilmem. Eğer zorunlu olan dil ikişer sargılarla tamamlanırsa, her ekleme bir test ve dalla sonuçlanır, DSP odaklı bir kısımda başlangıç ​​olmayan bir şey. Bu güncel bir üretim parçasıdır.
Dan Mills,

5
@BenVoigt Bazılarımız, küçük bir bilgisayarın 4k kod alanı, sabit bir 8 seviye çağrı / iade yığını, 64 byte RAM, 1MHz saat ve 1000 $ 'ın <0.20 $' lık olduğu bir dünyada yaşıyoruz. Modern bir cep telefonu, tüm amaçlar ve amaçlar için hemen hemen sınırsız depolamaya sahip küçük bir PC'dir ve hemen hemen bir PC olarak kabul edilebilir. Tüm dünya çok çekirdekli değildir ve zor gerçek zamanlı kısıtlamalardan yoksundur.
Dan Mills,

2
@DanMills: Kol Cortex ile modern cep telefonlarından bahsetmiyor. 2002’de "özellik telefonları" hakkında konuşabilen bir işlemci. Evet, SRAM 192kB’de 64 byte’dan çok daha fazla. ("Küçük" ama "küçük" değil) 192kB ayrıca 30 yıldır doğru bir şekilde "modern" masaüstü veya sunucu olarak da adlandırılmamıştır. Ayrıca bu günlerde 20 sent, 64 byte SRAM'den daha fazla olan bir MSP430 alacak.
Ben Voigt

2
@BenVoigt 192kB, son 30 yılda bir masaüstü olmayabilir, ancak kelimenin tam anlamıyla böyle bir şey yaptığımı iddia edeceğim web sayfalarını sunmanın tamamen yeterli olduğunu temin ederim. Gerçek şu ki, genellikle yapılandırma web sunucularını içeren bir LOT gömülü uygulama için tamamen makul (cömert, hatta) bir miktar ram. Tabii, muhtemelen üzerinde amazon kullanmıyorum, ama sadece böyle bir çekirdekte IOT crapware ile tamamlanmış bir buzdolabı çalıştırıyor olabilirim (Boş zaman ve alanla). Bunun için hiç kimsenin tercüme veya JIT dillerine ihtiyacı yok!
Dan Mills

8

Java ve C #, en azından geliştirilmelerinin başında, hakim bir satıcı tarafından tanımlanır. (Sırasıyla Sun ve Microsoft). C ve C ++ farklıdır; en başından beri çok sayıda rakip uygulamaları vardı. C özellikle egzotik donanım platformlarında da çalıştı. Sonuç olarak, uygulamalar arasında farklılıklar vardı. C ve C ++ 'yı standart hale getiren ISO komiteleri büyük bir ortak payda üzerinde anlaşabilir, ancak uygulamaların uygulama için odadan ayrılan standartları değiştirdiği kenarlarda.

Bu aynı zamanda, bir davranışı seçmek, başka bir seçime eğilimli donanım mimarileri için pahalı olabileceğinden - endianness açık bir seçimdir.


“Büyük ortak bir payda” nın kelimenin tam anlamıyla anlamı nedir? Alt kümelerden veya üst kümelerden mi bahsediyorsunuz? Gerçekten yeterince ortak faktörleri kastediyor musunuz? Bu en az kullanılan çoklu mi yoksa en büyük ortak faktör mü? Sokak dilini konuşamayan robotlar için bu çok kafa karıştırıcı, sadece matematik. :)
tchrist

@tchrist: Ortak davranış bir altkümedir, fakat bu altküme oldukça soyuttur. Ortak standart tarafından belirlenmemiş birçok alanda, gerçek uygulamalar bir seçim yapmalıdır. Şimdi bu seçimlerden bazıları oldukça açık ve bu nedenle uygulama tarafından tanımlanmış, ancak diğerleri daha belirsiz. Çalışma zamanında hafıza düzeni bir örnektir: bir seçim yapılması gerekir , ancak nasıl belgeleyeceğiniz belli değildir.
MSalters

2
Orijinal C bir adam tarafından yapıldı. Tasarım gereği zaten çok sayıda UB vardı. C popülerleştikçe işler daha da kötüye gitti, ancak UB en başından beri oradaydı. Pascal ve Smalltalk çok daha az UB'ye sahipti ve hemen hemen aynı zamanda geliştirildiler. C'nin ana avantajı, taşınması son derece kolaydı - tüm taşınabilirlik sorunları uygulama programcısına devredilmişti: P (sanal) işlemcime basit bir C derleyicisi bile taşıdım; LISP veya Smalltalk gibi bir şey yapmak çok daha fazla çaba harcardı (gerçi bir .NET çalışma zamanı için sınırlı bir prototip olmasına rağmen :).
Luaan

@Luaan: Bu Kernighan mı, Ritchie mi? Ve hayır, Tanımsız Davranış değildi. Biliyorum, masamda orijinal AT&T şablon derleyici belgelerine sahip olduğumu biliyorum. Uygulama yaptığını yaptı. Belirsiz ve tanımsız davranış arasında bir fark yoktu.
MSalters

4
@ MSalters Ritchie ilk insandı. Kernighan daha sonra sadece çok katıldı. Eh, "Tanımsız Davranış" yoktu, çünkü bu terim henüz yoktu. Ancak, bugün tanımsız olarak adlandırılacak olan aynı davranışa sahipti. C'nin bir özelliği olmadığı için, “belirtilmemiş” bile bir gerginliktir :) Bu sadece derleyicinin umursamadığı bir şeydi ve detaylar uygulama programcılarına kalmış. Taşınabilir uygulamalar üretmek için tasarlanmadı , sadece derleyicinin taşınması kolay olacaktı.
Luaan

6

Asıl sebep, bir yandan C ve C ++ ile diğer yandan Java ve C # (sadece birkaç örnek için) arasındaki niyet açısından temel bir farktan kaynaklanıyor. Tarihsel nedenlerden dolayı, buradaki tartışmaların çoğu C ++ 'dan ziyade C hakkında konuşur, fakat (muhtemelen zaten bildiğiniz gibi) C ++ C'nin oldukça doğrudan bir soyundandır, bu yüzden C hakkında söyledikleri eşit olarak C ++' a uygulanır.

Büyük ölçüde unutulmuş olmalarına rağmen (ve varlıkları bazen reddedilmiş olsalar da), UNIX'in ilk sürümleri assembly dilinde yazılmıştı. (Yalnızca değilse) C'nin asıl amacının çoğu, montaj dilinden daha yüksek bir dile kadar UNIX bağlantı noktasıydı. Niyetin bir kısmı, işletim sisteminde mümkün olan en yüksek düzeyde bir dilde yazmaktı - ya da diğer yönden bakmak, derleme dilinde yazılması gereken miktarı en aza indirmekti.

Bunu gerçekleştirmek için, C'nin donanıma derleme diliyle neredeyse aynı düzeyde erişim sağlaması gerekiyordu . PDP-11 (bir örnek için) G / Ç kayıtlarını belirli adreslere eşledi. Örneğin, sistem konsolunda bir tuşa basılıp basılmadığını kontrol etmek için bir hafıza konumunu okudunuz. Okunmayı bekleyen veriler olduğunda bu yere bir bit ayarlandı. Daha sonra, basılan tuşun ASCII kodunu almak için belirtilen başka bir konumdan bir bayt okudunuz.

Aynı şekilde, bazı veriler yazdırmak isterseniz, belirtilen başka bir yeri kontrol eder ve çıkış cihazı hazır olduğunda verilerinizi belirtilen başka bir yere yazarsınız.

Bu tür aygıtlar için yazma sürücülerini desteklemek için C, bazı tam sayı türlerini kullanarak rasgele bir konum belirtmenize, bir işaretçiye dönüştürmenize ve bu konumu bellekte okumanıza veya yazmanıza izin verdi.

Elbette, bunun oldukça ciddi bir sorunu var: dünyadaki her makine 1970'lerin başlarından itibaren bir PDP-11 ile aynı şekilde düzenlenmiş bir hatıraya sahip değil. Böylece, bu tamsayıyı aldığınızda, bir işaretçiye dönüştürdüğünüzde ve o işaretçiyi kullanarak okuduğunuzda veya yazdığınızda, ne alacağınız konusunda hiç kimse makul bir garanti veremez. Sadece bariz bir örnek için, okuma ve yazma, donanımdaki kayıtları ayırmak için haritalandırabilir, bu nedenle bir şey yazarsanız (normal belleğe aykırı), sonra tekrar okumaya çalışın, okuduklarınız yazdıklarınızla eşleşmeyebilir.

Bırakan birkaç olasılık görebiliyorum:

  1. Tüm olası donanıma bir arabirim tanımlayın - donanımla etkileşim kurmak için herhangi bir şekilde okumak veya yazmak isteyebileceğiniz tüm konumların mutlak adreslerini belirtin.
  2. Bu erişim seviyesini yasaklayın ve böyle şeyler yapmak isteyen herkesin assembly dilini kullanması gerektiğine dair karar verin.
  3. İnsanların bunu yapmasına izin verin, ancak hedefledikleri donanımın kılavuzlarını okumalarını (örneğin) kullanmalarını sağlayın ve kullandıkları donanıma uygun kodu yazın.

Bunlardan 1, yeterince tartışmaya değmeyecek kadar akıllıca görünüyor. 2 temel olarak dilin temel amacını ortadan kaldırıyor. Bu, üçüncü seçeneği, aslında makul bir şekilde düşünebilecekleri tek seçenek olarak bırakır.

Oldukça sık görülen bir diğer nokta ise tamsayı tiplerinin boyutlarıdır. C int, mimarlık tarafından önerilen doğal boyutta olması gereken "konumu" alır . Öyleyse, eğer 32 bit bir VAX programlıyorsam, intmuhtemelen 32 bit olmalı, ancak 36 bit bir Univac programlıyorsam, intmuhtemelen 36 bit olmalıdır (vb.). Muhtemelen, 8 bit'in katları olarak garanti edilen türleri kullanarak 36 bitlik bir bilgisayar için bir işletim sistemi yazmak makul değildir (ve mümkün olmayabilir). Belki de sadece yüzeysel oluyorum, ama bana öyle geliyor ki 36 bitlik bir makine için bir işletim sistemi yazıyorsam, muhtemelen 36 bitlik bir türü destekleyen bir dil kullanmak istiyorum.

Dil açısından bakıldığında, bu hala daha tanımsız davranışlara yol açmaktadır. 32 bite uyacak en büyük değeri alırsam, 1 eklediğimde ne olacak? Tipik 32 bit donanımda, devrilecek (veya muhtemelen bir tür donanım arızası atacak). Öte yandan, 36 bit donanım üzerinde çalışıyorsa, sadece bir tane ekleyeceğim. Dil yazma işletim sistemlerini destekleyecekse, her iki davranışı da garanti edemezsiniz - hem tür boyutlarının hem de taşma davranışının birinden diğerine değişmesine izin vermek zorundasınız.

Java ve C # bunların hepsini görmezden gelebilir. Yazma işletim sistemlerini desteklemeyi amaçlamıyorlar. Onlarla birlikte birkaç seçeneğiniz var. Birincisi, donanımın istediklerini desteklemektir - 8, 16, 32 ve 64 bit türlerini talep ettikleri için, sadece bu boyutları destekleyen donanımlar oluştururlar. Diğer bir belirgin olasılık, dilin sadece istediği ortamı sağlayan, diğer donanımın ne istediğinden bağımsız olarak çalışabilmesidir.

Çoğu durumda, bu gerçekten bir seçenek veya seçenek değildir. Aksine, birçok uygulama ikisinden de biraz fazlasını yapar. Java'yı normalde bir işletim sisteminde çalışan bir JVM'de çalıştırın. Çoğu zaman, işletim sistemi C ile yazılmamıştır ve JVM C ++ ile yazılmıştır. Eğer JVM bir ARM CPU üzerinde çalışıyorsa, donanımı Java'nın ihtiyaçlarına daha yakından uyarlamak için CPU'nun ARM'in Jazelle uzantılarını içermesi ihtimali oldukça iyidir, bu yüzden yazılımda daha az yapılması gereken ve Java kodunun daha hızlı çalışması (ya da daha az olması) neyse yavaşça).

özet

C ve C ++ tanımsız davranışa sahiptir, çünkü kimse yapmayı amaçladıklarını yapmalarına izin veren kabul edilebilir bir alternatif tanımlamamıştır. C # ve Java farklı bir yaklaşım benimsiyor, ancak bu yaklaşım C ve C ++ hedeflerine (hiç değilse) çok yakışmıyor. Özellikle, hiçbiri en çok seçilen donanım üzerine sistem yazılımı (işletim sistemi gibi) yazmak için makul bir yol sağlamaz. Her ikisi de, işlerini yapmak için mevcut sistem yazılımı tarafından sağlanan tesislere (genellikle C veya C ++ ile yazılmış) bağlıdır.


4

C Standardının yazarları, okurlarının, açık bir şekilde düşündükleri bir şeyi tanımalarını beklemiş ve yayınlanan gerekçelerinde açıkça belirtmişlerdir, ancak açıkça söylemediler: Komite, derleyici yazarlarının, müşterilerinin ihtiyaçlarını karşılamak için sipariş vermesi gerekmemelidir, Çünkü müşteriler Komite'den daha iyi bir şey bilmelidirler. Belli tür plaformlar için derleyicilerin bir yapıyı belirli bir şekilde işlemesi beklendiği açıksa, Standardın bu tanımsız davranışa yol açtığını söyleyip söylemediğini kimse umursamalıdır. Standardın, uygun derleyicilere uygun bir şekilde bir kod parçasını işlemesini zorunlu kılmaması, programcıların almayan derleyiciler almaya istekli olması gerektiği anlamına gelmez.

Dil tasarımına yönelik bu yaklaşım, derleyici yazarların mallarını ödeme yapan müşterilere satmaları gereken bir dünyada çok iyi çalışıyor. Derleyici yazarların pazarın etkilerinden izole edildiği bir dünyada tamamen parçalanır. 1990'larda popüler olan bir dili yönlendirmek için uygun pazar koşullarının mevcut olacağı şüpheli ve aklı başında bir dil tasarımcısının bu tür pazar koşullarına güvenmek isteyeceği konusunda daha da şüpheli.


Burada önemli bir şey tanımladığınızı hissediyorum, ama benden kaçıyor. Cevabınızı netleştirir misiniz? Özellikle ikinci paragraf: Şimdi şartlar ve daha önceki şartlar farklı diyor, ama anlamadım; tam olarak ne değişti? Ayrıca, "yol" şimdi öncekinden farklıdır; belki bunu da açıklayabilirsin?
anatolyg

4
Tanımlanmamış tüm davranışları tanımlanmamış davranışlarla değiştirmek için kampanyanızın görünüyor veya daha kısıtlı olan şeyler hala güçleniyor.
Deduplicator

1
@anatolyg: Henüz yapmadıysanız, yayınlanan C Rationale belgesini okuyun (Google'da C99 Rationale yazın). Sayfa 11 23-29 satırları “pazar yeri” hakkında ve sayfa 13 satır 5-8 portatifliği ile ilgili olarak neyin anlatıldığından bahseder. Bir derleyici yazarın, optimize edicinin, Standart tarafından tanımlanmayan eylemleri gerçekleştirdiği için kodlarının "kırıldığını" faydalı bir şekilde ele aldığını söyleyen programcılara, bir derleyici yazarındaki bir patronun nasıl tepki vereceğini düşünüyorsunuz? bunu desteklemeyi reddetti, çünkü bu devam edecekti ...
supercat

1
... bu tür yapıların kullanımı? Böyle bir bakış açısı, clang ve gcc'nin destek panolarında açıkça görülmekte ve kırılgan dil ve clang'ın desteklemesinden çok daha kolay ve güvenli bir şekilde optimizasyonu kolaylaştıracak içsel gelişimin engellenmesine hizmet etmiştir.
supercat

1
@supercat: Derleyici satıcılarına şikayet ederek nefesini boşa harcıyorsun. Endişelerinizi neden dil komitelerine yönlendirmiyorsunuz? Sizinle aynı fikirde oldukları takdirde, derleyici ekiplerini baştan sona yenmek için kullanabileceğiniz bir hata verilir. Ve bu süreç, dilin yeni versiyonunun geliştirilmesinden çok daha hızlı. Ama eğer aynı fikirde olmazlarsa, en azından gerçek nedenleri elde edeceksin, oysa derleyici yazarları sadece tekrar edecekler (tekrar tekrar) “Bu kodun kırılmasını belirlemedik, bu karar dil komitesi tarafından verilmiş ve biz kararlarını takip et. "
Ben Voigt

3

C ++ ve c her ikisi de tanımlayıcı standartlara sahiptir (yine de ISO sürümleri).

Hangi sadece dillerin nasıl çalıştığını açıklamak ve dilin ne olduğu hakkında tek bir referans sağlamak için var. Tipik olarak, derleyici satıcıları ve kütüphane yazarları ana ISO standardına dahil olan bazı önerilerde bulunur.

Java ve C # (veya demek istediğinizi varsaydığım Visual C #) kuralcı standartlara sahiptir. Önceden dilde kesin olarak ne olduğunu, nasıl çalıştığını ve izin verilen davranış olarak kabul ettiklerini söylerler.

Bundan daha önemlisi, Java aslında Open-JDK'da bir "referans uygulaması" na sahiptir. ( Roslyn , Visual C # başvuru uygulaması olarak sayılıyor, ancak bunun için bir kaynak bulamadığını düşünüyorum.)

Java'nın durumunda, standartta herhangi bir belirsizlik varsa ve Open-JDK bunu kesin olarak yapar. Open-JDK bunu standart şekilde yapar.


Durum bundan daha kötü: Komitenin tanımlayıcı mı yoksa tanımlayıcı mı olduğu konusunda bir fikir birliği sağladığını sanmıyorum.
Supercat

1

Tanımsız davranış derleyicinin çeşitli mimarlar üzerinde çok verimli kodlar üretmesini sağlar. Erik'in cevabı optimizasyondan bahseder, ancak bunun ötesine geçer.

Örneğin, imzalanmış taşmalar C'de tanımsız davranışlardır. Uygulamada, derleyiciden CPU'nun yürütmesi için basit bir imzalı ek opcode üretmesi beklenirdi ve bu davranış bu CPU'nun yaptığı her şey olurdu.

Bu, C'nin çok iyi performans göstermesine ve çoğu mimaride çok kompakt kod üretmesine izin verdi. Eğer standart imzalı tamsayıların belirli bir şekilde taşması gerektiğini belirtmiş olsaydı, farklı şekilde davranan CPU'ların basit imzalı bir ekleme için daha fazla kod üretmesi gerekirdi.

Bu, C'deki tanımsız davranışların çoğunun sebebi ve intsistemler arasındaki büyüklük gibi şeylerin neden değiştiğinin nedenidir . Intmimariye bağımlıdır ve genellikle a'dan büyük olan en hızlı, en verimli veri türü olarak seçilir char.

C yeniyken, bu düşünceler önemliydi. Bilgisayarlar daha az güçlüydü, çoğu zaman işlem hızı ve hafızası sınırlıydı. Performansın gerçekten önemli olduğu yerlerde C kullanılıyordu ve geliştiricilerin bu tanımsız davranışların gerçekte kendi sistemlerinde ne olacağını bilecek şekilde bilgisayarların nasıl iyi çalıştığını anlamaları bekleniyordu.

Java ve C # gibi diller daha sonra ham performans üzerindeki tanımsız davranışların ortadan kaldırılmasını tercih etti.


-5

Bir anlamda, Java da var. Diyelim ki, Arrays.sort ile yanlış karşılaştırıcı verdiniz. Onu algılaması dışında istisna atabilir. Aksi takdirde, bir diziyi herhangi bir özel olacağı garanti edilmeyen bir şekilde sıralar.

Benzer şekilde, birkaç iş parçacığından değişkeni değiştirirseniz, sonuçlar da tahmin edilemez.

C ++ tanımsız daha fazla durum (veya daha çok işlem tanımlamaya karar verdikten sonra) yapmak ve bunun için bir isme sahip olmak için daha ileri gitti.


4
Bu, burada bahsettiğimiz türdeki tanımsız davranış değil. "Yanlış karşılaştırıcılar" iki tipte gelir: toplam siparişi tanımlayanlar ve olmayanlar. Maddelerin göreceli sırasını sürekli olarak tanımlayan bir karşılaştırıcı sağlarsanız, davranış iyi tanımlanır, programcının istediği davranış değildir. Göreceli sıralamayla tutarlı olmayan bir karşılaştırıcı sağlarsanız, davranış hala iyi tanımlanmıştır: sort işlevi bir istisna atar (bu da muhtemelen programcının istediği davranış değildir).
Mark

2
Değişkenleri değiştirme gelince, yarış koşulları genellikle tanımsız davranış olarak kabul edilmez. Java'nın paylaşılan verilere verilen ödevleri nasıl işlediğinin ayrıntılarını bilmiyorum, ama dilin genel felsefesini bilerek, atomik olmak gerektiğinden eminim. Aynı anda 53 ve 71'i atamak, a51 veya 73'ü çıkarabilirseniz tanımsız bir davranış olacaktır, ancak yalnızca 53 veya 71'i alabiliyorsanız, iyi tanımlanmıştır.
Mark

@Mark Sistemin yerel sözcük boyutundan daha büyük veri parçalarıyla (örneğin, 16 bitlik bir kelime boyutu sisteminde 32 bitlik bir değişken), her 16 bitlik kısmı ayrı ayrı depolamayı gerektiren bir mimariye sahip olmak mümkündür. (SIMD böyle bir başka potansiyel durumdur.) Bu durumda, basit bir kaynak kod seviyesi ataması bile, derleyici tarafından atomik olarak yürütülmesini sağlamak için özel bir özen gösterilmediği sürece mutlaka atomik değildir.
CVn
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.