Döngünün hangi noktasında tamsayı taşması tanımsız davranış haline gelir?


86

Bu, burada gönderemeyeceğim çok daha karmaşık bir kod içeren sorumu açıklamak için bir örnek.

#include <stdio.h>
int main()
{
    int a = 0;
    for (int i = 0; i < 3; i++)
    {
        printf("Hello\n");
        a = a + 1000000000;
    }
}

Bu program benim platformumda tanımsız davranış içeriyor çünkü a3. döngüde taşacak.

Bu, tüm programın tanımsız davranışa sahip olmasını mı yoksa yalnızca taşma gerçekten olduktan sonra mı? Derleyici potansiyel olduğunu işe yarayabilir a olacak o tanımsız bütün döngü beyan ve hepsi taşma önce gerçekleşmesi rağmen printfs çalıştırmak için rahatsız böylece taşması?

(Farklı olsalar da C ve C ++ etiketli çünkü farklılarsa her iki dil için de cevaplarla ilgilenirim.)


7
Derleyicinin kullanılmayan bir şeyi çalışıp çalışmadığını merak edin a(kendini hesaplamak dışında) ve kaldırına
4386427

12
Sen hoşunuza gidebilecek My Little Doktoru: Tanımsız Davranış Sihirli olduğunu , bu yıl CppCon dan. Her şey, derleyicilerin tanımsız davranışa göre hangi optimizasyonları gerçekleştirebileceğiyle ilgilidir.
TartanLlama



Yanıtlar:


108

Tamamen teorik bir cevapla ilgileniyorsanız, C ++ standardı tanımlanmamış davranışların "zamanda yolculuk" yapmasına izin verir:

[intro.execution]/5: İyi biçimlendirilmiş bir programı yürüten uyumlu bir uygulama, aynı program ve aynı girdiyle soyut makinenin karşılık gelen örneğinin olası yürütmelerinden biri ile aynı gözlemlenebilir davranışı üretecektir. Bununla birlikte, bu tür bir yürütme tanımlanmamış bir işlem içeriyorsa, bu Uluslararası Standart, bu programı bu girdi ile yürüten uygulamaya herhangi bir gereklilik getirmez (ilk tanımsız işlemden önceki işlemlerle ilgili olarak bile)

Bu nedenle, programınız tanımlanmamış davranış içeriyorsa, tüm programınızın davranışı tanımsızdır.


4
@KeithThompson: Ama o zaman, sneeze()işlevin kendisi sınıfın herhangi bir şeyinde tanımsızdır Demon(ki burun çeşidi bir alt sınıftır ), her şeyi yine de dairesel hale getirir.
Sebastian Lenartowicz

1
Ancak printf geri dönmeyebilir, bu nedenle ilk iki tur tanımlanır, çünkü tamamlanana kadar UB'nin olacağı net değildir. Bkz stackoverflow.com/questions/23153445/...
usr

1
(Önyükleme kodu tanımsız davranış dayanır çünkü) bir derleyici Linux çekirdeği için "nop" yayacak şekilde kendi hakları içerisinde teknik olmasının nedeni budur: blog.regehr.org/archives/761
Crashworks

3
@Crashworks İşte bu yüzden Linux taşınamaz C olarak yazılır ve derlenir (yani -fno-sıkı-aliasing gibi belirli bir derleyici gerektiren belirli bir derleyici gerektiren bir C üst kümesi)
user253751

3
@usr Geri dönmüyorsa tanımlı bekliyorum printfama printfdönecekse tanımsız davranış daha önce sorunlara neden olabilir printf. Dolayısıyla zamanda yolculuk. printf("Hello\n");ve sonra sonraki satır şu şekilde derlenirundoPrintf(); launchNuclearMissiles();
user253751

31

Öncelikle bu sorunun başlığını düzeltmeme izin verin:

Tanımsız Davranış, (özellikle) yürütme alanı değildir.

Tanımsız Davranış tüm adımları etkiler: derleme, bağlama, yükleme ve çalıştırma.

Bunu pekiştirmek için bazı örnekler, hiçbir bölümün ayrıntılı olmadığını unutmayın:

  • derleyici, Tanımlanmamış Davranış içeren kod bölümlerinin hiçbir zaman yürütülmediğini varsayabilir ve bu nedenle bunlara yol açacak yürütme yollarının ölü kod olduğunu varsayabilir. Her C programcısının tanımlanmamış davranışlar hakkında bilmesi gerekenleri görün Chris Lattner başkası.
  • bağlayıcı, zayıf bir sembolün (adıyla tanınan) birden fazla tanımının varlığında, Tek Tanım Kuralı sayesinde tüm tanımların aynı olduğunu varsayabilir
  • yükleyici (dinamik kitaplıklar kullanmanız durumunda) aynı şeyi varsayabilir, böylece bulduğu ilk sembolü seçer; bu genellikle (ab) LD_PRELOADUnixlerde hileler kullanarak çağrıları yakalamak için kullanılır
  • sarkan işaretçiler kullanırsanız yürütme başarısız olabilir (SIGSEV)

Tanımlanmamış Davranış hakkında bu kadar korkutucu olan budur : Vaktinden önce tam olarak hangi davranışın meydana geleceğini tahmin etmek neredeyse imkansızdır ve bu öngörü, araç zincirinin her güncellemesinde, altta yatan işletim sistemi, ...


Michael Spencer'ın (LLVM Developer) hazırladığı bu videoyu izlemenizi tavsiye ederim: CppCon 2016: My Little Optimizer: Undefined Behavior is Magic .


3
Beni endişelendiren bu. Gerçek kodumda karmaşıktır ama her zaman taşacağı bir durumum olabilir . Ve bunu gerçekten umursamıyorum, ancak "doğru" kodun da bundan etkileneceğinden endişeleniyorum. Açıkçası düzeltmem gerekiyor, ancak düzeltmek için anlayış gerekiyor :)
jcoder

8
@jcoder: Burada önemli bir kaçış var. Derleyicinin giriş verilerini tahmin etmesine izin verilmez. Tanımsız Davranışın gerçekleşmediği en az bir girdi olduğu sürece, derleyici bu belirli girdinin hala doğru çıktıyı ürettiğinden emin olmalıdır. Tehlikeli optimizasyonlarla ilgili tüm korkutucu sözler yalnızca kaçınılmaz UB için geçerlidir . Pratik olarak konuşursak, argcdöngü sayısı olarak kullanmış olsaydınız , vaka argc=1UB üretmez ve derleyici bunu halletmeye zorlanır.
MSalters

@jcoder: Bu durumda, bu ölü kod değildir. Ancak derleyici, bunun defalarca iartırılamayacağını Nve dolayısıyla değerinin sınırlı olduğunu anlayacak kadar akıllı olabilir .
Matthieu M.

4
@jcoder: f(good);X ile ilgili bir şey yapıp f(bad);tanımlanmamış bir davranışı çağırırsa, o zaman sadece çağıran bir programın f(good);X yapması garanti edilir, ancak f(good); f(bad);X'i yapması garanti edilmez.

4
@Hurkyl daha da ilginci, eğer kodunuz ise if(foo) f(good); else f(bad);, akıllı bir derleyici karşılaştırmayı bir kenara atacak ve koşulsuz bir üretecektir foo(good).
John Dvorak

28

Bir agresif C optimize veya C ++ derleyici 16 bit hedefleyen intolacak biliyorum eklemeye davranış olduğunu 1000000000bir etmek inttürü olduğunu tanımsız .

Her iki standart tarafından da tüm programın silinmesini ve ayrılmasını da içerebilecek her şeyi yapma izni vardır int main(){}.

Peki ya daha büyük inte'ler? Henüz bunu yapan bir derleyici bilmiyorum (ve hiçbir şekilde C ve C ++ derleyici tasarımında uzman değilim), ancak bazen 32 bit intveya daha yüksek bir sürümü hedefleyen bir derleyicinin döngünün sonsuzdur ( ideğişmez) ve bu yüzden asonunda taşar. Yani bir kez daha çıktıyı optimize edebilir int main(){}. Burada belirtmeye çalıştığım nokta, derleyici optimizasyonları giderek daha agresif hale geldikçe, giderek daha fazla tanımlanmamış davranış yapılarının kendilerini beklenmedik şekillerde göstermeleridir.

Döngünüzün sonsuz olduğu gerçeği, döngü gövdesinde standart çıktıya yazdığınız için kendi içinde tanımsız değildir.


3
Standart, tanımsız davranış ortaya çıkmadan önce istediği her şeyi yapmasına izin veriyor mu? Bu nerede belirtilir?
jimifiki

4
neden 16 bit? Sanırım OP, 32 bitlik işaretli bir taşma arıyor.
4386427

8
@jimifiki Standartta. C ++ 14 (N4140) 1.3.24 "Tanımlanmamış davranış = bu Uluslararası Standardın hiçbir gereklilik getirmediği davranış." Artı detaylandıran uzun bir not. Ama asıl mesele, tanımlanmamış olan bir "ifadenin" davranışı değil, programın davranışıdır . Bu, UB'nin standarttaki bir kural tarafından (veya bir kuralın olmamasıyla) tetiklendiği sürece, standardın bir bütün olarak program için geçerli olmayacağı anlamına gelir . Böylece programın herhangi bir parçası istediği gibi davranabilir.
Angew,

5
İlk ifade yanlış. Eğer int16-bit, ilave gerçekleşecek long(literal işlenen türüne sahip olduğundan long) o iyi tanımlanmış nereye, daha sonra bir uygulama tanımlı dönüşüm arkasına tarafından dönüştürülebilir int.
R .. GitHub BUZA YARDIM ETMEYİ DURDUR

2
@usr davranışı printfstandart tarafından her zaman geri dönecek şekilde tanımlanır
MM

11

Teknik olarak, C ++ standardına göre, bir program tanımlanmamış davranış içeriyorsa , derleme zamanında bile (program çalıştırılmadan önce) tüm programın davranışı tanımsızdır.

Uygulamada, derleyici (optimizasyonun bir parçası olarak) taşmanın meydana gelmeyeceğini varsayabileceğinden, en azından programın döngünün üçüncü yinelemesindeki davranışı (32 bitlik bir makine varsayılarak) tanımsız olacaktır. üçüncü yinelemeden önce doğru sonuçları almanız olasıdır. Bununla birlikte, tüm programın davranışı teknik olarak tanımlanmadığı için, programın tamamen yanlış çıktı üretmesini (çıktı olmaması dahil), yürütme sırasında herhangi bir noktada çalışma zamanında çökmesini veya hatta tamamen derleyememesini (tanımsız davranış, Derleme zamanı).

Tanımlanmamış davranış, derleyiciye optimize etmek için daha fazla alan sağlar çünkü kodun ne yapması gerektiğine dair belirli varsayımları ortadan kaldırır. Bunu yaparken, tanımlanmamış davranışları içeren varsayımlara dayanan programların beklendiği gibi çalışması garanti edilmez. Bu nedenle, C ++ standardına göre tanımsız olarak kabul edilen belirli bir davranışa güvenmemelisiniz.


Ya UB kısmı bir if(false) {}kapsam dahilindeyse ? Derleyicinin tüm dalların ~ iyi tanımlanmış mantık bölümleri içerdiğini varsayması ve dolayısıyla yanlış varsayımlar üzerinde çalışması nedeniyle bu, tüm programı zehirler mi?
mlvljr

1
Standart, tanımlanmamış davranışa hiçbir koşul getirmez, bu nedenle teoride , evet, tüm programı zehirler. Bununla birlikte, pratikte , herhangi bir iyileştirici derleyici muhtemelen ölü kodu kaldıracaktır, bu nedenle muhtemelen yürütme üzerinde hiçbir etkisi olmayacaktır. Yine de bu davranışa güvenmemelisin.
bwDraco

Bilmekte
fayda var

9

@TartanLlama'nın yeterince ifade ettiği gibi tanımlanmamış bir davranışın neden 'zamanda yolculuk' yapabildiğini anlamak için, 'sanki' kuralına bir göz atalım:

1.9 Program yürütme

1 Bu Uluslararası Standarttaki anlamsal açıklamalar, parametreleştirilmiş belirleyici olmayan soyut bir makineyi tanımlar. Bu Uluslararası Standart, uyumlu uygulamaların yapısına herhangi bir gereklilik getirmez. Özellikle, soyut makinenin yapısını kopyalamaları veya taklit etmeleri gerekmez. Daha ziyade, uyumlu uygulamaların aşağıda açıklandığı gibi soyut makinenin gözlemlenebilir davranışını taklit etmesi (sadece) gereklidir.

Bununla, programı bir girdi ve çıktı içeren bir 'kara kutu' olarak görebiliriz. Giriş, kullanıcı girişi, dosyalar ve diğer birçok şey olabilir. Çıktı, standartta belirtilen 'gözlemlenebilir davranış'tır.

Standart yalnızca girdi ve çıktı arasında bir eşleme tanımlar, başka hiçbir şey yoktur. Bunu bir 'örnek kara kutuyu' tanımlayarak yapar, ancak aynı eşlemeye sahip diğer kara kutuların da eşit derecede geçerli olduğunu açıkça söyler. Bu, kara kutunun içeriğinin alakasız olduğu anlamına gelir.

Bunu akılda tutarak, tanımsız davranışın belirli bir anda meydana geldiğini söylemek mantıklı olmaz. Gelen numune kara kutu uygulanması nerede ve ne zaman olur, biz söyleyebiliriz ama gerçek artık olduğunda biz nerede ve diyemeyiz böylece kara kutu, tamamen farklı bir şey olabilir. Teorik olarak, bir derleyici örneğin tüm olası girdileri numaralandırmaya ve ortaya çıkan çıktıları önceden hesaplamaya karar verebilir. O zaman tanımlanmamış davranış, derleme sırasında meydana gelirdi.

Tanımsız davranış, girdi ve çıktı arasında bir eşlemenin olmamasıdır. Bir programın bazı girdiler için tanımlanmamış davranışları olabilir, ancak diğerleri için tanımlanmış davranışları olabilir. O zaman girdi ve çıktı arasındaki eşleştirme tamamlanmamış; Çıktıya eşlemenin olmadığı bir girdi var.
Söz konusu programın herhangi bir girdi için tanımsız davranışı vardır, bu nedenle eşleme boştur.


6

int32-bit olduğu varsayılırsa , üçüncü yinelemede tanımsız davranış gerçekleşir. Bu nedenle, örneğin, döngü yalnızca koşullu olarak erişilebilirse veya üçüncü yinelemeden önce koşullu olarak sonlandırılabiliyorsa, üçüncü yinelemeye gerçekten ulaşılmadıkça tanımsız bir davranış olmayacaktır. Bununla birlikte, tanımsız davranış durumunda, tanımsız davranışın başlatılmasına göre "geçmişte" olan çıktı da dahil olmak üzere programın tüm çıktıları tanımsızdır. Örneğin, sizin durumunuzda bu, çıktıda 3 "Merhaba" mesajı görme garantisi olmadığı anlamına gelir.


6

TartanLlama'nın cevabı doğru. Tanımlanmamış davranış, derleme sırasında bile herhangi bir zamanda gerçekleşebilir. Bu saçma görünebilir, ancak derleyicilerin yapmaları gereken şeyi yapmalarına izin veren önemli bir özelliktir. Derleyici olmak her zaman kolay değildir. Her seferinde şartname ne diyorsa onu yapmalısınız. Bununla birlikte, bazen belirli bir davranışın meydana geldiğini kanıtlamak korkunç derecede zor olabilir. Durma sorununu hatırlarsanız, belirli bir girdi beslendiğinde sonsuz bir döngüyü tamamlayıp tamamlamadığını kanıtlayamayacağınız bir yazılım geliştirmek oldukça önemsizdir.

Derleyicileri kötümser yapabilir ve bir sonraki talimatın sorunlar gibi bu durdurma sorunlarından biri olabileceğinden korkarak sürekli derleyebiliriz, ancak bu mantıklı değil. Bunun yerine derleyiciye bir geçiş izni veriyoruz: bu "tanımlanmamış davranış" konularında, herhangi bir sorumluluktan kurtulmuşlardır. Tanımlanmamış davranış, onları gerçekten kötü-hain durma problemlerinden ayırmakta güçlük çekecek kadar ince bir şekilde alçakça olan tüm davranışlardan oluşur.

Göndermeyi sevdiğim bir örnek var, ancak kaynağını kaybettiğimi kabul etsem de, bu yüzden başka kelimelerle ifade etmem gerekiyor. MySQL'in belirli bir sürümündendi. MySQL'de, kullanıcı tarafından sağlanan verilerle doldurulmuş dairesel bir tamponu vardı. Tabii ki, verilerin arabelleği aşmadığından emin olmak istediler, bu yüzden bir kontrolleri vardı:

if (currentPtr + numberOfNewChars > endOfBufferPtr) { doOverflowLogic(); }

Yeterince mantıklı görünüyor. Ancak, ya numberOfNewChars gerçekten büyükse ve taşıyorsa? Sonra etrafını sarar ve bundan daha küçük bir işaretçi haline gelir endOfBufferPtr, böylece taşma mantığı asla çağrılmaz. Böylece ondan önce ikinci bir çek eklediler:

if (currentPtr + numberOfNewChars < currentPtr) { detectWrapAround(); }

Arabellek taşma hatasını halletmişsiniz gibi görünüyor, değil mi? Ancak, bu tamponun Debian'ın belirli bir sürümünde taştığını belirten bir hata gönderildi! Dikkatli bir araştırma, Debian'ın bu sürümünün gcc'nin özellikle yeni bir sürümünü kullanan ilk sürüm olduğunu gösterdi. Gcc'nin bu sürümünde, derleyici currentPtr + numberOfNewChars öğesinin hiçbir zaman currentPtr'den daha küçük bir işaretçi olamayacağını fark etti çünkü işaretçiler için taşma tanımsız bir davranıştır! Bu, gcc'nin tüm denetimi optimize etmesi için yeterliydi ve kontrol etmek için kodu yazmış olsanız bile birdenbire arabellek taşmalarına karşı korunmadınız !

Bu özel bir davranıştı. Her şey yasaldı (duyduğuma göre gcc bu değişikliği bir sonraki sürümde geri aldı). Sezgisel davranış olarak düşündüğüm şey bu değil, ama hayal gücünüzü biraz uzatırsanız, bu durumun hafif bir varyasyonunun derleyici için nasıl bir durma sorunu haline gelebileceğini görmek kolaydır. Bu nedenle, spec yazarları onu "Tanımsız Davranış" yaptı ve derleyicinin kesinlikle istediği her şeyi yapabileceğini belirtti.


Aralıkları "int" nin ötesine geçen türlerde işaretli aritmetik yapılıyormuş gibi davranan özellikle şaşırtıcı derleyicileri düşünmüyorum, özellikle x86'da basit kod üretme yaparken bile bunu yapmanın ara düzeyi kesmekten daha verimli olduğu düşünüldüğünde. Sonuçlar. Daha da şaşırtıcı olan, taşmanın diğer hesaplamaları etkilediği zamandır; bu, kod iki uint16_t değerinin ürününü bir uint32_t'ye depolasa bile gcc'de gerçekleşebilir - sterilize edici olmayan bir yapıda şaşırtıcı davranmak için makul bir nedeni olmayan bir işlem.
supercat

Elbette, doğru kontrol if(numberOfNewChars > endOfBufferPtr - currentPtr), numberOfNewChars hiçbir zaman negatif olamaz ve currentPtr her zaman arabellek içinde bir yere işaret ederse, saçma "sarmalama" kontrolüne bile ihtiyacınız yoktur. (
Sağladığınız kodun

@ Random832 Bir ton dışarıda bıraktım. Daha geniş bağlamdan alıntı yapmaya çalıştım, ancak kaynağımı kaybettiğimden beri, bağlamın başka kelimelerle ifade edilmesinin beni daha fazla belaya soktuğunu fark ettim, bu yüzden onu dışarıda bıraktım. Doğru şekilde alıntı yapabilmem için o lanet hata raporunu gerçekten bulmam gerekiyor. Bu, kodu tek bir şekilde yazdığınızı ve onu tamamen farklı bir şekilde derlediğinizi nasıl düşünebileceğinizin gerçekten güçlü bir örneğidir.
Cort Ammon

Bu, tanımlanmamış davranışla ilgili en büyük sorunum. Bazen doğru kod yazmayı imkansız kılar ve derleyici bunu algıladığında, varsayılan olarak bunun tanımsız davranışı tetiklediğini size söylemez. Bu durumda, kullanıcı basitçe aritmetik yapmak ister - işaretçi olsun ya da olmasın - ve güvenli kod yazmak için tüm sıkı çalışmaları geri alındı. En azından kodun bir bölümüne açıklama eklemenin bir yolu olmalı - burada süslü optimizasyonlar yok. C / C ++, bu tehlikeli durumun optimizasyon lehine devam etmesine izin vermek için çok fazla kritik alanda kullanılıyor
John McGrath

4

Teorik cevapların ötesinde, pratik bir gözlem, derleyicilerin uzun zamandır döngülerde içlerinde yapılan iş miktarını azaltmak için çeşitli dönüşümler uyguladıklarıdır. Örneğin, verilen:

for (int i=0; i<n; i++)
  foo[i] = i*scale;

bir derleyici bunu şuna dönüştürebilir:

int temp = 0;
for (int i=0; i<n; i++)
{
  foo[i] = temp;
  temp+=scale;
}

Böylece her döngü yinelemesinde bir çarpma tasarrufu sağlanır. Değişik derecelerde saldırganlıkla uyarlanan derleyicilerin ek bir optimizasyon biçimi, bunu şuna dönüştürecektir:

if (n > 0)
{
  int temp1 = n*scale;
  int *temp2 = foo;
  do
  {
    temp1 -= scale;
    *temp2++ = temp1;
  } while(temp1);
}

Taşma üzerine sessiz sarmalayan makinelerde bile, n'den küçük bir sayı varsa bu arızalanabilir ve ölçekle çarpıldığında 0 sonucunu verir. değerini beklenmedik bir şekilde değiştirdi ("ölçek" in UB'yi çağırmadan orta döngüyü değiştirebileceği her durumda, derleyicinin optimizasyonu gerçekleştirmesine izin verilmezdi).

INT_MAX + 1 ve UINT_MAX arasında bir değer elde etmek için iki kısa işaretsiz türün çarpıldığı durumlarda bu tür optimizasyonların çoğu herhangi bir sorun yaşamazken, gcc, bir döngü içinde böyle bir çarpmanın döngünün erken çıkmasına neden olabileceği bazı durumlara sahiptir. . Oluşturulan koddaki karşılaştırma talimatlarından kaynaklanan bu tür davranışları fark etmedim, ancak derleyicinin bir döngünün en fazla 4 veya daha az kez çalıştırılabileceği sonucuna varmak için taşmayı kullandığı durumlarda gözlemlenebilir; çıkarımları döngünün üst sınırının göz ardı edilmesine neden olsa bile, bazı girdilerin UB'ye neden olacağı ve diğerlerinin oluşmayacağı durumlarda varsayılan olarak uyarılar oluşturmaz.


4

Tanımlanmamış davranış, tanımı gereği gri bir alandır. Ne "tanımsız davranış" olduğunu - Sadece o ya yapmayacağım ne olacağını tahmin edemez araçları .

Çok eski zamanlardan beri, programcılar her zaman tanımsız bir durumdan tanımlılık kalıntılarını kurtarmaya çalıştılar. Onlar gerçekten kullanımına isteyen bazı kod var, ama onlar iddia deneyin böylece, tanımlanmamış çıkıyor ki ettik: "Ben tanımsız biliyorum ama mutlaka o, en kötü ihtimalle, şu ya da bu yapacağım; bunu yapmak asla o . " Ve bazen bu argümanlar aşağı yukarı doğrudur - ama çoğu zaman yanlıştır. Ve derleyiciler daha akıllı hale geldikçe (veya bazı insanlar daha sinsice ve daha sinsice diyebilir), sorunun sınırları değişmeye devam ediyor.

Yani gerçekten, çalışması garantili ve uzun süre çalışmaya devam edecek bir kod yazmak istiyorsanız, tek bir seçeneğiniz var: ne pahasına olursa olsun tanımlanmamış davranıştan kaçının. Gerçekten, eğer onunla ilgilenirseniz, sizi rahatsız etmek için geri gelecektir.


ve yine de, işte şu ... derleyiciler optimize etmek için tanımlanmamış davranışları kullanabilir, ancak GENEL OLARAK SİZE SÖYLEMEZLER. Öyleyse, ne pahasına olursa olsun X yapmaktan kaçınmanız gereken bu harika araca sahipsek, derleyici neden düzeltebilmeniz için size bir uyarı veremiyor?
Jason S

1

Örneğinizin dikkate almadığı bir şey optimizasyondur. adöngüde ayarlanır, ancak hiç kullanılmaz ve bir iyileştirici bunu çözebilir. Bu nedenle, optimizasyon uzmanının atması meşrudura uzmanının tamamen vazgeçmesi meşrudur ve bu durumda tüm tanımlanmamış davranışlar, boojum'un kurbanı gibi ortadan kaybolur.

Ancak elbette bunun kendisi tanımsızdır, çünkü optimizasyon tanımsızdır. :)


1
Davranışın tanımsız olup olmadığını belirlerken optimizasyonu düşünmek için hiçbir neden yoktur.
Keith Thompson

2
Programın varsayılabileceği gibi davranması, tanımsız davranışın "ortadan kalktığı" anlamına gelmez. Davranış hala tanımlanmadı ve sadece şansa güveniyorsun. Programın davranışının derleyici seçeneklerine göre değişebileceği gerçeği, davranışın tanımsız olduğunun güçlü bir göstergesidir.
Jordan Melo

@JordanMelo Önceki cevapların çoğu optimizasyonu tartıştığından (ve OP özellikle bunu sorduğundan), daha önce hiçbir cevabın kapsamadığı bir optimizasyon özelliğinden bahsetmiştim. Ayrıca, optimizasyon onu ortadan kaldırsa bile, belirli bir şekilde çalışmak için optimizasyona güvenmenin yine tanımsız olduğuna dikkat çektim . Kesinlikle tavsiye etmiyorum! :)
Graham

@KeithThompson Elbette, ancak OP özellikle optimizasyonu ve platformunda göreceği tanımlanmamış davranış üzerindeki etkisini sordu. Optimizasyona bağlı olarak bu belirli davranış ortadan kalkabilir. Yine de cevabımda söylediğim gibi, tanımsızlık olmazdı.
Graham

0

Bu soru çift etiketli C ve C ++ olduğundan ikisini de deneyeceğim ve ele alacağım. C ve C ++ burada farklı yaklaşımlar alır.

C'de uygulama, tüm programı tanımlanmamış davranışa sahipmiş gibi ele almak için tanımlanmamış davranışın çağrılacağını kanıtlayabilmelidir. OPs örneğinde, derleyicinin bunu kanıtlaması önemsiz görünebilir ve bu nedenle tüm program tanımsızmış gibi.

Bunu, özünde şu soruyu soran Kusur Raporu 109'dan görebiliriz :

Bununla birlikte, C Standardı "tanımlanmamış değerlerin" ayrı varlığını kabul ederse (yalnızca yaratılışı tamamen "tanımlanmamış davranış" içermeyen), derleyici testi yapan bir kişi aşağıdaki gibi bir test senaryosu yazabilir ve aynı zamanda (veya muhtemelen talep) uygun bir uygulamanın, en azından bu kodu "hata" olmadan derlemesi (ve muhtemelen yürütülmesine de izin vermesi).

int array1[5];
int array2[5];
int *p1 = &array1[0];
int *p2 = &array2[0];

int foo()
{
int i;
i = (p1 > p2); /* Must this be "successfully translated"? */
1/0; /* Must this be "successfully translated"? */
return 0;
}

Yani asıl soru şudur: Yukarıdaki kod "başarılı bir şekilde tercüme edilmeli" mi (bu ne anlama geliyorsa)? (5.1.1.3 alt maddesine ekli dipnota bakın.)

ve yanıt şuydu:

C Standardı, "tanımlanmamış değer" değil, "belirsiz olarak değerli" terimini kullanır. Belirsiz değerli bir nesnenin kullanılması, tanımsız davranışa neden olur. 5.1.1.3 alt maddesinin dipnotu, geçerli bir program hala doğru bir şekilde çevrildiği sürece, bir uygulamanın herhangi bir sayıda tanılama üretmekte özgür olduğuna işaret etmektedir. Değerlendirilmesi tanımlanmamış davranışla sonuçlanacak bir ifade, sabit bir ifadenin gerekli olduğu bir bağlamda ortaya çıkarsa, içeren program tam olarak uyumlu değildir. Ayrıca, belirli bir programın olası her yürütülmesi, tanımsız davranışla sonuçlanacaksa, verilen program kesinlikle uyumlu değildir. Uygun bir uygulama, tam olarak uyumlu bir programı çevirmekte başarısız olmamalıdır, çünkü o programın olası bir şekilde yürütülmesi tanımlanmamış davranışa neden olabilir. Foo hiçbir zaman çağrılamayabileceğinden, verilen örnek uygun bir uygulama ile başarıyla çevrilmelidir.

C ++ 'da yaklaşım daha rahat görünür ve uygulamanın statik olarak kanıtlayıp ispatlayamadığına bakılmaksızın bir programın tanımsız davranışa sahip olduğunu gösterir.

Elimizde [intro.abstrac] p5 var:

İyi biçimlendirilmiş bir programı yürüten uyumlu bir uygulama, aynı program ve aynı girdiyle soyut makinenin karşılık gelen örneğinin olası yürütmelerinden biri ile aynı gözlemlenebilir davranışı üretecektir. Bununla birlikte, bu tür bir yürütme tanımlanmamış bir işlem içeriyorsa, bu belge, bu programın bu girdi ile çalıştırılmasına ilişkin herhangi bir gereksinim getirmez (ilk tanımsız işlemden önceki işlemlerle ilgili olarak bile).


Bir işlevin yürütülmesinin UB'yi çağıracağı gerçeği, yalnızca belirli bir girdi verildiğinde bir programın davranış şeklini etkileyebilir, eğer girdi verildiğinde programın en az bir olası yürütülmesi UB'yi çağırırsa. Bir işlevi çağırmanın UB'yi çağıracağı gerçeği, bir programın işlevin başlatılmasına izin vermeyen girdi beslendiğinde tanımlanmış davranışa sahip olmasını engellemez.
supercat

@supercat En azından benim cevabımın C için söylediğine inanıyorum.
Shafik Yaghmour

Aynısının, alıntılanan metin re C ++ için de geçerli olduğunu düşünüyorum, çünkü "Böyle bir yürütme" ifadesi, programın belirli bir girdi ile çalıştırılabileceği yolları ifade eder. Belirli bir girdi bir işlevin yürütülmesine neden olamazsa, alıntılanan metinde böyle bir işlevdeki herhangi bir şeyin UB ile sonuçlanacağını söyleyen hiçbir şey görmüyorum.
supercat

-2

En iyi cevap yanlış (ancak yaygın) bir yanlış anlamadır:

Tanımsız davranış, bir çalışma zamanı özelliğidir *. Bu DEĞİL CAN "zaman yolculuğu"!

Bazı operasyonlar (standart olarak) yan etkilere sahip olacak şekilde tanımlanır ve optimize edilemez. G / Ç yapan veya volatiledeğişkenlere erişen işlemler bu kategoriye girer.

Ancak , bir uyarı var: UB, önceki işlemleri geri alan davranış da dahil olmak üzere herhangi bir davranış olabilir . Bu, bazı durumlarda, önceki kodu optimize etmeye benzer sonuçlar doğurabilir.

Aslında, bu en üstteki cevapla tutarlıdır (vurgu benim):

İyi biçimlendirilmiş bir programı yürüten uygun bir uygulama, aynı program ve aynı girdiyle soyut makinenin karşılık gelen örneğinin olası yürütmelerinden biri ile aynı gözlemlenebilir davranışı üretecektir.
Bununla birlikte, bu tür bir yürütme tanımlanmamış bir işlem içeriyorsa, bu Uluslararası Standart, bu programı bu girdi ile yürüten uygulamaya herhangi bir gereklilik getirmez (ilk tanımsız işlemden önceki işlemlerle ilgili olarak bile).

Evet, bu alıntı yapar demek "bile ilk tanımsız operasyonundan önceki operasyonlar kapsamında" bu özellikle ediliyor yaklaşık kod olduğunu, ancak haber yürütülen , sadece derlenmiş değil.
Sonuçta, gerçekte ulaşılmayan tanımsız davranış hiçbir şey yapmaz ve UB içeren satıra gerçekten ulaşılması için, ondan önce gelen kod önce yürütülmelidir!

Yani evet, UB yürütüldüğünde , önceki işlemlerin herhangi bir etkisi tanımsız hale gelir. Ancak bu gerçekleşene kadar, programın yürütülmesi iyi tanımlanmıştır.

Bununla birlikte, programın bunun gerçekleşmesine neden olan tüm yürütmelerinin, önceki işlemleri gerçekleştiren ancak daha sonra etkilerini kaldıranlar da dahil olmak üzere eşdeğer programlara optimize edilebileceğini unutmayın . Sonuç olarak, önceki kod, etkilerinin geri alınmasına eşdeğer olduğu her durumda optimize edilebilir ; aksi takdirde olamaz. Örnek için aşağıya bakın.

* Not: Bu, derleme zamanında meydana gelen UB ile tutarsız değildir . Derleyici aslında UB kodu kanıtlayabilirsek olacak her zaman tüm girişler için yürütülecek, daha sonra UB derleme zaman uzatabilirsiniz. Bununla birlikte, bu, önceki tüm kodun sonunda geri döneceğini bilmeyi gerektirir , bu da güçlü bir gerekliliktir. Yine, bir örnek / açıklama için aşağıya bakın.


Bunu somut hale getirmek için, aşağıdaki kodun onu izleyen tanımsız davranışlardan bağımsız olarak yazdırıp girdinizi beklemesi gerektiğini unutmayın foo:

printf("foo");
getchar();
*(char*)1 = 1;

Bununla birlikte, fooUB oluştuktan sonra ekranda kalacağına veya yazdığınız karakterin artık giriş arabelleğinde olmayacağına dair bir garanti olmadığını da unutmayın ; bu işlemlerin her ikisi de "geri alınabilir", bu da UB "zaman yolculuğuna" benzer bir etkiye sahiptir.

Eğer getchar()çizgi yoktu, o olur hatları uzakta optimize edilmesi için yasal yalnızca ve eğer olurdu farksız çıkışının fooardından ve onu "un-yapıyor".

İkisinin ayırt edilemez olup olmayacağı tamamen uygulamaya bağlıdır (yani derleyicinize ve standart kitaplığınıza). Örneğin, başka bir programın çıktıyı okumasını beklerken iş parçacığınızı burada printf bloke edebilir mi? Yoksa hemen geri mi dönecek?

  • Burada engelleyebiliyorsa, başka bir program tam çıktısını okumayı reddedebilir ve asla geri dönmeyebilir ve sonuç olarak UB asla gerçekleşmeyebilir.

  • Hemen buraya dönebilirse, o zaman geri dönmesi gerektiğini biliyoruz ve bu nedenle onu optimize etmek, onu çalıştırmaktan ve sonra etkilerini yapmaktan tamamen ayırt edilemez.

Elbette, derleyici kendi özel sürümü için hangi davranışa izin verildiğini bildiğinden printf, buna göre optimize edebilir ve sonuç printfolarak bazı durumlarda optimize edilirken bazılarında optimize edilemez. Ancak, yine, gerekçe, bunun UB'nin önceki işlemlerini yapmamasından ayırt edilemez olması, önceki kodun UB nedeniyle "zehirlenmiş" olması değil.


1
Standardı tamamen yanlış yorumluyorsunuz. Programı çalıştırırkenki davranışın tanımsız olduğunu söylüyor . Dönem. Bu cevap% 100 yanlış. Standart çok açıktır - saf yürütme akışının herhangi bir noktasında UB üreten girdiye sahip bir program çalıştırmak tanımsızdır.
David Schwartz

@DavidSchwartz: Yorumunuzu mantıksal sonuçlarına kadar takip ederseniz, bunun mantıklı olmadığını anlamalısınız. Girdi, program başladığında tam olarak belirlenen bir şey değildir . Programın herhangi bir satırdaki girdisinin (sadece varlığı bile ) o satıra kadar programın tüm yan etkilerine bağlı olmasına izin verilir . Bu nedenle, program olamaz o çevresiyle etkileşimi gerektirir ve bu nedenle UB çizgi ya da değil ilk etapta ulaşılacaktır olmadığını etkilediğinden, UB satırdan önce gelip yan etkileri üreten kaçının.
user541686

3
Bu önemli değil. Gerçekten mi. Yine, sadece hayal gücünüz yok. Örneğin, derleyici hiçbir uyumlu kodun farkı söyleyemeyeceğini söylerse, UB olan kodu, safça "önceki" olmasını beklediğiniz çıktılardan önce yürüteceği UB olan kodu taşıyabilir.
David Schwartz

2
@Mehrdad: Belki de bir şeyler söylemenin daha iyi bir yolu, UB'nin gerçek dünyada davranışı tanımlayabilecek bir şeyin olabileceği son noktayı geçmişe yolculuk yapamayacağını söylemek olabilir. Bir uygulama, girdi arabelleklerini inceleyerek getchar () 'a yapılacak sonraki 1000 çağrının engelleyemeyeceğini belirleyebilseydi ve UB'nin 1000. çağrıdan sonra oluşacağını da belirleyebilseydi, herhangi birini gerçekleştirmesi gerekmeyecekti. aramalar. Bununla birlikte, bir uygulama, önceki tüm çıktıya sahip olana kadar yürütmenin bir getchar ()
geçirmeyeceğini belirtecekse

2
... 300 baud uçbirimine teslim edildi ve bundan önce meydana gelen herhangi bir kontrol-C, önündeki tamponda başka karakterler olsa bile getchar () 'ın bir sinyal yükseltmesine neden olacaksa, böyle bir uygulama herhangi bir UB'yi getchar () 'dan önceki son çıktıyı geçecek şekilde taşıyın. Zor olan, hangi durumda bir derleyicinin programcıdan geçmesinin beklenmesi gerektiğini bilmektir, herhangi bir davranışsal, bir kütüphane uygulamasının Standart tarafından zorunlu kılınanların ötesinde sunabileceği garantilerdir.
supercat
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.