C'de dizi indekslerinin (ifadeye karşı) değerlendirme sırası


47

Bu koda bakarak:

static int global_var = 0;

int update_three(int val)
{
    global_var = val;
    return 3;
}

int main()
{
    int arr[5];
    arr[global_var] = update_three(2);
}

Hangi dizi girişi güncellenir? 0 veya 2?

C spesifikasyonunda, bu özel durumda operasyonun önceliğini gösteren bir parçası var mı?


21
Bu tanımsız davranış kokuyor. Kesinlikle bilerek kodlanmaması gereken bir şey.
Fiddling Bits

1
Kötü kodlama örneği olduğuna katılıyorum.
Jiminion

4
Bazı fıkra sonuçları: godbolt.org/z/hM2Jo2
Bob__

15
Bunun dizi indeksleri veya işlem sırası ile ilgisi yoktur. C spec'in "dizi noktaları" olarak adlandırdığı şeyle ve özellikle de atama ifadelerinin sol ve sağ el ifadesi arasında bir dizi noktası OLUŞTURMAMASI ile ilgilidir, bu nedenle derleyici bunu yapmakta serbesttir seçer.
Lee Daniel Crocker

4
Bu clangkod parçasının bir uyarı IMHO'sunu tetiklemesi için bir özellik isteği bildirmelisiniz .
malat

Yanıtlar:


51

Sol ve Sağ İşlenenlerin Sırası

Atamayı gerçekleştirmek için arr[global_var] = update_three(2), C uygulaması işlenenleri değerlendirmeli ve bir yan etki olarak sol işlenenin depolanan değerini güncellemelidir. C 2018 6.5.16 (ödevlerle ilgilidir) paragraf 3, sol ve sağ işlenenlerde sıralama olmadığını gösterir:

İşlenenlerin değerlendirmeleri sıralanmamıştır.

Bu araçlar C uygulaması hesaplamak için serbesttir lvalue arr[global_var] , daha sonra değerlendirmek için (bu ifade belirtir ne bulmaktan ortalama lvalue hesaplayarak) ilk update_three(2)ve son olarak ikinci önceki için değeri atamak; veya önce değerlendirmek update_three(2), sonra değeri hesaplamak, sonra ilkini ikincisine atamak; veya değeri değerlendirmek ve update_three(2)karışık bir şekilde değerlendirmek ve daha sonra sol değeri doğru değere atamaktır.

Her durumda, değerin lvalue'ya atanması en son gelmelidir, çünkü 6.5.16 3 ayrıca şunları söylüyor:

… Sol işlenenin depolanan değerini güncellemenin yan etkisi, sol ve sağ işlenenlerin değer hesaplamalarından sonra sıralanır…

Sıralama İhlali

Bazıları global_var, 6.5 2 ihlali ile hem kullanımı hem de ayrı olarak güncellenmesi nedeniyle tanımlanmamış davranış hakkında düşünebilir ,

Bir skaler nesne üzerindeki bir yan etki, aynı skaler nesne üzerindeki farklı bir yan etkiye veya aynı skaler nesnenin değerini kullanarak bir değer hesaplamasına göre sıralanmamışsa, davranış tanımsızdır…

Birçok C uygulayıcısı x + x++tarafından, C standardı tarafından tanımlanmadığı gibi ifadelerin davranışının, her ikisi de xdizileme olmadan aynı ifadede değerini kullandıkları ve ayrı ayrı değiştirdikleri oldukça tanıdıktır . Bununla birlikte, bu durumda, bazı sıralama sağlayan bir işlev çağrımız var. işlev çağrısında global_varkullanılır arr[global_var]ve işlev çağrısında güncellenir update_three(2).

6.5.2.2 10, fonksiyon çağrılmadan önce bir dizi noktası olduğunu söyler:

Fonksiyon göstergesinin ve gerçek argümanların değerlendirilmesinden sonra ancak gerçek çağrıdan önce bir sıralama noktası vardır ...

Fonksiyonun içerisinde global_var = val;bir olduğunu tam ifadesi ve böyledir 3içinde return 3;6.8 4 başına:

Bir tam ifadesi başka ifade parçasıdır, ne de bir Bildiricisi veya soyut Bildiricisi bir parçası olmayan bir ifadedir ...

Daha sonra bu iki ifade arasında, yine 6.8 4 başına bir sıralama noktası vardır:

… Tam ifadenin değerlendirilmesi ile değerlendirilecek bir sonraki ifadenin değerlendirilmesi arasında bir sıralama noktası vardır.

Böylece, C uygulaması arr[global_var]önce değerlendirebilir ve daha sonra işlev çağrısını yapabilir, bu durumda aralarında bir dizi noktası vardır, çünkü işlev çağrısından önce bir tane vardır veya işlev çağrısında değerlendirebilir global_var = val;ve sonra arr[global_var]bu durumda tam ifade sonra bir tane var çünkü aralarında bir dizi noktası. Dolayısıyla, davranış tanımlanmamıştır - bu iki şeyden herhangi biri önce değerlendirilebilir - ancak tanımlanmamıştır.


24

Buradaki sonuç belirtilmedi .

Alt ifadelerin nasıl gruplandığını belirten bir ifadedeki işlemlerin sırası iyi tanımlanmış olsa da, değerlendirme sırası belirtilmemiştir. Bu durumda, ya global_varönce okunabilir ya da önce çağrı update_threeyapılabilir, ancak hangisinin olduğunu bilmenin bir yolu yoktur.

Orada değil bir işlev çağrısı bir dizi noktası tanıtır çünkü tanımsız davranış olarak değiştirmektedir o da dahil olmak üzere işlevinde her ifadeyi yapar burada global_var.

Açıklığa kavuşturmak için, C standardı bölüm 3.4.3'teki tanımlanmamış davranışı şu şekilde tanımlar :

tanımlanmamış davranış

taşınabilir olmayan veya hatalı bir program yapısının veya bu Uluslararası Standardın hiçbir şart getirmediği hatalı verilerin kullanılması üzerine davranış

ve bölüm 3.4.4'teki belirtilmemiş davranışı şu şekilde tanımlar :

belirtilmemiş davranış

belirtilmemiş bir değerin veya bu Uluslararası Standardın iki veya daha fazla olasılık sağladığı ve herhangi bir durumda seçilen başka bir şart getirmediği başka bir davranışın kullanılması

Standart, işlev bağımsız değişkenlerinin değerlendirme sırasının belirtilmediğini belirtir; bu durumda, ya arr[0]3'e ayarlandığı ya da 3'e ayarlandığı anlamına gelir arr[2].


“Bir işlev çağrısı bir dizi noktası getiriyor” yetersiz. Önce sol işlenen değerlendirilirse, yeterlidir, o zamandan beri sıra noktası sol işleneni işlevdeki değerlendirmelerden ayırır. Ancak, sol işlenen işlev çağrısından sonra değerlendirilirse, işlevin çağrılmasından kaynaklanan sıralama noktası, işlevdeki değerlendirmelerle sol işlenenin değerlendirmesi arasında değildir. Tam ifadeleri ayıran sıra noktasına da ihtiyacınız vardır.
Eric Postpischil

2
@EricPostpischil C11 öncesi terminolojisinde, bir işlevin giriş ve çıkışında bir sıra noktası vardır. C11 terminolojisinde, tüm fonksiyon gövdesi çağıran bağlama göre belirsiz bir şekilde dizilir. Bunların her ikisi de sadece farklı terimler kullanarak aynı şeyi
MM

Bu kesinlikle yanlış. Ödev argümanlarının değerlendirme sırası belirtilmemiştir. Bu özel atamanın sonucuna gelince, hem taşınamaz hem de özünde yanlış olan güvenilir olmayan bir içeriğe sahip bir dizi oluşturulmasıdır (anlambilim veya amaçlanan sonuçlardan herhangi biri ile tutarsız). Tanımsız davranış için mükemmel bir durum.
kuroi neko

1
@kuroineko Çıktının değişebilmesi otomatik olarak tanımsız davranış yapmaz. Standart, tanımlanmamış ve tanımlanmamış davranış için farklı tanımlara sahiptir ve bu durumda ikincisidir.
dbush

@EricPostpischil Burada sıralama noktalarınız var (C11 bilgilendirici Ek C'den): "Bir fonksiyon çağrısında fonksiyon belirleyicisinin değerlendirmeleri ile gerçek çağrıdaki gerçek argümanlar arasında. (6.5.2.2)", "Tam ifadenin değerlendirilmesi arasında ve bir sonraki tam ifade değerlendirilecek ... / - / ... dönüş ifadesinde (isteğe bağlı) ifade (6.8.6.4) ". Ve her noktalı virgülde de, çünkü bu tam bir ifade.
Lundin

1

Denedim ve 0 girdisini güncelledim.

Ancak bu soruya göre: bir ifadenin sağ tarafı daima önce değerlendirilir mi

Değerlendirme sırası belirtilmemiş ve sıralanmamıştır. Bu yüzden böyle bir kod kaçınılması gerektiğini düşünüyorum.


Güncellemeyi 0 girişinde de aldım.
Jiminion

1
Davranış tanımsız değil, belirtilmemiş. Doğal olarak her ikisine de bağlı olarak kaçınılmalıdır.
Antti Haapala

@AnttiHaapala Düzenledim
Mickael B.

1
Hmm ah ve sıralanmamış ama sıralı olarak sıralı değil ... Sırada rastgele duran 2 kişi belirsiz sıralı. Ajan Smith'in içindeki Neo emsalsiz ve tanımsız davranışlar olacak.
Antti Haapala

0

Atamanız gereken bir değere sahip olmadan bir ödev için kod yayınlamak pek mantıklı olmadığından, çoğu C derleyicisi önce işlevi çağıran kodu yayar ve sonucu bir yere kaydeder (kayıt, yığın vb.), bu değeri son hedefine yazar ve bu nedenle değiştirildikten sonra global değişkeni okuyacaklardır. Buna herhangi bir standartla değil, saf mantıkla tanımlanan "doğal düzen" diyelim.

Yine de optimizasyon sürecinde, derleyiciler değeri bir yerde geçici olarak saklamanın ara adımını ortadan kaldırmaya çalışacak ve fonksiyon sonucunu mümkün olduğunca doğrudan nihai hedefe yazmaya çalışacak ve bu durumda, önce dizini okumak zorunda kalacaklar. işlev sonucunu doğrudan diziye taşıyabilmek için, örneğin bir kayıt defterine. Bu, genel değişkenin değiştirilmeden önce okunmasına neden olabilir.

Dolayısıyla bu, optimizasyonun yapılıp yapılmadığına ve bu optimizasyonun ne kadar agresif olduğuna bağlı olarak, sonucun farklı olma olasılığı oldukça yüksek olan kötü özellik ile tanımlanmamış bir davranıştır. Bu sorunu kodlama ile çözmek bir geliştirici olarak görevinizdir:

int idx = global_var;
arr[idx] = update_three(2);

veya kodlama:

int temp = update_three(2);
arr[global_var] = temp;

Başparmak için iyi bir kural olarak: Global değişkenler olmadıkça const(veya değişmezler ancak hiçbir kodun bunları yan etki olarak değiştirmeyeceğini bilmiyorsanız), bunları asla çok iş parçacıklı bir ortamda olduğu gibi doğrudan kodda kullanmamalısınız, bu bile tanımsız olabilir:

int result = global_var + (2 * global_var);
// Is not guaranteed to be equal to `3 * global_var`!

Derleyici bunu iki kez okuyabildiğinden ve başka bir iş parçacığı iki okuma arasındaki değeri değiştirebildiğinden. Yine de, optimizasyon kesinlikle kodun sadece bir kez okunmasına neden olur, bu yüzden şimdi başka bir iş parçacığının zamanlamasına da bağlı olan farklı sonuçlara sahip olabilirsiniz. Böylece, global değişkenleri kullanımdan önce geçici bir yığın değişkenine depolarsanız çok daha az baş ağrınız olur. Derleyici bunun güvenli olduğunu düşünüyorsa, büyük olasılıkla bunu bile optimize edecek ve bunun yerine genel değişkeni doğrudan kullanacağından, sonuçta performans veya bellek kullanımında hiçbir fark yaratmayabileceğini unutmayın.

(Her ihtimale karşı herkes yapardı neden birisi sorar x + 2 * xyerine 3 * xbazı CPU'lar ilavesi ultra hızlı ve derleyici (bit vardiya halinde bunlar dönecek şekilde bir güç iki tarafından çarpma açık - 2 * x == x << 1), henüz keyfi sayılarla çarpma çok yavaş olabilir Böylece, 3 ile çarpmak yerine, x'i 1'e biraz kaydırarak ve sonuca x ekleyerek çok daha hızlı kod elde edersiniz ve hatta bu numara, 3 ile çarptığınızda ve modern bir hedef olmadıkça agresif optimizasyonu açtığınızda modern derleyiciler tarafından gerçekleştirilir. O zamandan beri çarpmanın toplama kadar eşit olduğu CPU, hesaplamayı yavaşlatacaktır.)


2
Tanımlanmamış bir davranış değildir - standart olasılıkları listeler ve bunlardan biri her durumda seçilir
Antti Haapala

Derleyici 3 * xiki x okumaya dönüşmez . X'i bir kez okuyabilir ve sonra x'e okuduğu kayıtta x + 2 * x yöntemini yapabilir
MM

6
@Mecki "Sadece koda bakarak sonucun ne olduğunu söyleyemezseniz, sonuç tanımsızdır" - tanımsız davranışın C / C ++ 'da çok belirgin bir anlamı vardır ve bu değildir. Diğer answerers bu özel örneğidir neden açıkladık belirtilmemiş , ancak tanımsız .
marcelm

3
Orijinal sorunun kapsamı dışına çıksa bile, bilgisayarın içine biraz ışık atma niyetini takdir ediyorum. Bununla birlikte, UB çok hassas C / C ++ jargonudur ve özellikle soru bir dil teknikliğiyle ilgili olduğunda dikkatli kullanılmalıdır. Bunun yerine, cevabı önemli ölçüde artıracak uygun "belirtilmemiş davranış" terimini kullanmayı düşünebilirsiniz.
kuroi neko

2
@Mecki " Tanımsız, İngilizce dilinde çok özel bir anlama sahiptir " ... ancak söz language-lawyerkonusu dilin tanımsız için kendi "çok özel anlamı" olduğu etiketli bir soruda , yalnızca kullanmadan karışıklığa neden olacaksınız. dilin tanımı.
TripeHound

-1

Global edit: Üzgünüm arkadaşlar, kovuldum ve saçmalık yazdım. Sadece eski bir moruk sırası.

C'nin kurtulduğuna inanmak istedim, ama ne yazık ki C11'den beri C ++ ile eşit hale getirildi. Görünüşe göre, derleyicinin ifadelerde yan etkilerle ne yapacağını bilmek, şimdi bir "senkronizasyon noktasının önünde yer alıyor" temelinde kod dizilerinin kısmi bir sırasını içeren küçük bir matematik bilmecesini çözmeyi gerektirir.

K&R günlerinde birkaç kritik gerçek zamanlı gömülü sistemi tasarladım ve uyguladım (motor kontrol altında tutulmazsa insanları en yakın duvara çökebilecek bir elektrikli otomobilin kontrolörü dahil), 10 tonluk bir endüstriyel düzgün bir şekilde komut verilmezse insanları bir hamur haline getirebilen robot ve zararsız olsa da birkaç düzine işlemcinin veri yolunu% 1'den daha az sistem yükü ile kurutan bir sistem katmanı).

Tanımlanmamış ve belirtilmemiş arasındaki farkı elde etmek için çok yaşlı veya aptal olabilirim, ancak aynı anda yürütmenin ve veri erişiminin ne anlama geldiğine dair hala iyi bir fikrim var. Tartışmalı olarak bilgilendirilmiş düşünceme göre, C ++ ve şimdi C adamlarının evcil hayvan dilleri ile senkronizasyon sorunlarını ele geçirmesinin bu saplantısı pahalı bir boru rüyası. Ya eşzamanlı yürütmenin ne olduğunu biliyorsunuz ve bu gizmoslardan herhangi birine ihtiyacınız yok ya da ihtiyacınız yok ve dünyayı onunla uğraşmaya çalışmayan bir iyilik yapardınız.

Tüm bu göz doldurmalı bellek bariyeri soyutlamalarının yükü, çoklu CPU önbellek sistemlerinin geçici bir dizi kısıtlamasından kaynaklanmaktadır, bunların hepsi, örneğin muteksler ve durum değişkenleri C ++ gibi ortak işletim sistemi senkronizasyon nesnelerinde güvenli bir şekilde kapsüllenebilir. teklifler.
Bu kapsüllemenin maliyeti, ince taneli özel CPU komutlarının kullanımının elde edebileceği şeylerle karşılaştırıldığında performanslarda bir dakika düşüştür. Anahtar kelime (ya da
volatile#pragma dont-mess-with-that-variableherkes için, bir sistem programcısı olarak, bakım) derleyiciye bellek erişimini yeniden düzenlemeyi durdurmasını söylemek için yeterli olurdu. Optimal kod, doğrudan CPU'ya özgü talimatlar ile düşük seviye sürücüyü ve işletim sistemi kodunu serpmek için doğrudan asm direktifleriyle kolayca üretilebilir. Altta yatan donanımın (önbellek sistemi veya veri yolu arabirimi) nasıl çalıştığına dair samimi bir bilgi olmadan, yine de işe yaramaz, verimsiz veya hatalı kod yazmanız gerekir.

volatileAnahtar kelime ve Bob'un bir dakikalık ayarlaması , en katı düşük seviyeli programcıların amcası dışında herkes olurdu. Bunun yerine, C ++ matematik düşkünlerinin olağan çetesi, mevcut olmayan problemleri arayan ve bir programlama dilinin tanımını bir derleyicinin özellikleriyle yanlış yönlendiren tipik tasarım eğilimlerine yol açan, başka bir anlaşılmaz soyutlama tasarlayan bir saha gününe sahipti.

Sadece bu kez C'nin temel bir yönünü tanımlamak için gereken değişiklik, çünkü bu "engeller" düzgün çalışması için düşük seviyeli C kodunda bile oluşturulmak zorundaydı. Bu, diğer şeylerin yanı sıra, herhangi bir açıklama ya da gerekçe olmaksızın, ifadelerin tanımına zarar verdi.

Sonuç olarak, bir derleyicinin bu saçma C parçasından tutarlı bir makine kodu üretebilmesi, C ++ adamlarının 2000'li yılların sonlarındaki önbellek sistemlerindeki potansiyel tutarsızlıklarla baş etmesinin sadece uzak bir sonucudur.
C'nin (ifade tanımı) temel bir yönünün korkunç bir karmaşasını yarattı, böylece önbellek sistemleri hakkında bir lanet vermeyen ve haklı olarak - C programcılarının büyük çoğunluğu, şimdi açıklamak için gurulara güvenmeye zorlanıyor. a = b() + c()ve arasındaki fark a = b + c.

Bu talihsiz dizide ne olacağını tahmin etmeye çalışmak zaten net bir zaman ve çaba kaybıdır. Derleyicinin bundan ne yapacağına bakılmaksızın, bu kod patolojik olarak yanlıştır. Bununla ilgili tek sorumlu şey, çöp kutusuna göndermek.
Kavramsal olarak, yan etkiler, değişikliğin değerlendirmeden önce veya sonra açık bir şekilde ayrı bir ifadede yapılmasına izin verme önemsiz çabasıyla her zaman ifadelerden çıkarılabilir.
Bu tür boktan kod, bir derleyicinin hiçbir şeyi optimize etmesini bekleyemeyeceğiniz 80'lerde haklı olabilir. Ancak derleyiciler uzun zamandır çoğu programcıdan daha akıllı hale geldi, geriye kalan tek şey boktan bir kod.

Bu tanımlanmamış / tanımlanmamış tartışmanın önemini de anlayamıyorum. Ya tutarlı bir davranışla kod oluşturmak için derleyiciye güvenebilirsiniz ya da edemezsiniz. Bunu tanımsız ya da tanımlanmamış olarak adlandırmak, tartışmalı bir nokta gibi görünüyor.

Tartışmalı olarak bilgilendirilmiş düşünceme göre, C zaten K&R durumunda yeterince tehlikelidir. Yararlı bir evrim sağduyulu güvenlik önlemleri eklemek olacaktır. Örneğin, bu gelişmiş kod analiz aracını kullanarak, teknik özellikler derleyiciyi uç noktalara potansiyel olarak güvenilmez bir kod üretmek yerine en azından bonkers kodu hakkında uyarılar üretmeye zorlar.
Ancak bunun yerine çocuklar C ++ 17'de sabit bir değerlendirme sırası belirlemeye karar verdiler. Artık her yazılım embesili, yeni derleyicilerin şaşkınlığı belirleyici bir şekilde ele alacağından emin olarak, kodunda yan etkileri bilerek aktif olarak teşvik edilmektedir.

K&R bilgi işlem dünyasının gerçek mucizelerinden biriydi. Yirmi dolar için dilin kapsamlı bir belirtimini aldınız (tek tek kişilerin sadece bu kitabı kullanarak tam derleyiciler yazdığını gördüm), mükemmel bir referans el kitabı (içindekiler genellikle sizi yanıtınızın birkaç sayfası içinde gösterecektir) soru) ve dili mantıklı bir şekilde kullanmayı öğreten bir ders kitabı. Çok, çok aptalca şeyler yapmak için dili kötüye kullanabileceğiniz çeşitli yollar hakkında rasyonel, örnek ve akıllıca uyarı sözleriyle tamamlayın.

Bu mirası çok az kazanç için yok etmek benim için acımasız bir atık gibi görünüyor. Ama yine de bu noktayı tamamen göremeyebilirim. Belki bir tür ruh beni bu yan etkilerden önemli ölçüde yararlanan yeni C kodu örneği yönünde gösterebilir?


Aynı ifadede aynı nesne üzerinde yan etkiler varsa tanımlanmamış bir davranıştır, C17 6.5 / 2. Bunlar C17 6.5.18 / 3'e göre sıralanmamıştır. Ancak 6.5 / 2'den gelen metin "Bir skaler nesne üzerindeki bir yan etki, aynı skaler nesne üzerindeki farklı bir yan etkiye veya aynı skaler nesnenin değerini kullanan bir değer hesaplamasına göre sıralanmamışsa, davranış tanımsızdır." işlev içindeki değer hesaplaması, dizinde dizin erişiminden önce veya sonra sıralanır; çünkü atama işlecinin kendisinde sıralanmamış işlenenler olmasına bakılmaksızın sıralanır.
Lundin

İşlev çağrısı, isterseniz "sıralanmamış erişime karşı bir muteks" gibi davranır. Karanlık virgül operatörü gibi bok gibi 0,expr,0.
Lundin

"Tanımsız davranış, tanılama zorluğu olan belirli program hatalarını yakalamaması için uygulayıcı lisansını verir. Ayrıca, uygun dil uzantısının uygun olduğu alanları da tanımlar: Uygulayıcı, bir dil sağlayarak dili artırabilir. resmi olarak tanımlanmamış davranışın tanımı . ve Standardın tam olarak uymayan yararlı programları küçük düşürmesi gerektiğini söyledi. Bence Standart yazarlarının çoğu kaliteli derleyiciler yazmak isteyen insanların açık olduğunu düşünürdü ...
supercat

... derleyicilerini müşterileri için olabildiğince faydalı hale getirmek için bir fırsat olarak UB kullanmaya çalışmalıdır. Herhangi bir derleyici yazarlar "derleyiciniz bu kodu herkesin bu kodu daha az yararlı bir şekilde" ile "şikayetlerini yanıtlamak için bir bahane olarak kullanacağını hayal olabilirdi çünkü bu Standart yararlı bir şekilde işlemek için gerekmiyor ve uygulamalar Davranışları Standart tarafından zorunlu kılınmayan programları yararlı bir şekilde işleyen sadece kırık programların yazılmasını teşvik eder ".
supercat

Yorumunuzdaki noktayı göremiyorum. Derleyiciye özgü davranışlara güvenmek, taşınabilir olmama garantisidir. Aynı zamanda, bu "ekstra tanımlardan" herhangi birini her an durdurabilen derleyici üreticisine büyük bir inanç gerektirir. Bir derleyicinin yapabileceği tek şey, akıllı ve bilgili bir programcının benzer hataları ele almaya karar verebileceği uyarılar üretmektir. Bu ISO canavarı ile gördüğüm sorun, OP'nin örnek yasallığı gibi korkunç bir kod oluşturmasıdır (bir ifadenin K&R tanımına kıyasla son derece net olmayan nedenlerle).
kuroi neko
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.