Pratikte, farklı derleyiciler neden farklı int x = ++ i + ++ i ;? değerlerini hesaplasınlar?


164

Bu kodu düşünün:

int i = 1;
int x = ++i + ++i;

Derleyeceğini varsayarak, bir derleyicinin bu kod için ne yapabileceğine dair bazı tahminlerimiz var.

  1. her ikisi de ++igeri döner 2, sonuçlanır x=4.
  2. biri ++idöner 2ve diğeri geri döner ve 3sonuçlanır x=5.
  3. her ikisi de ++igeri döner 3, sonuçlanır x=6.

Bana göre ikincisi büyük olasılıkla görünüyor. İki biri ++operatörleri ile yürütüldüğünde i = 1, iartırılır ve sonuç 2döndürülür. Sonra ikinci ++operatör ile yürütülür i = 2, iartırılır ve sonuç 3döndürülür. Sonra 2ve 3vermek için bir araya eklenir 5.

Ancak bu kodu Visual Studio'da çalıştırdım ve sonuç 6. Derleyicileri daha iyi anlamaya çalışıyorum ve bir sonuca neyin yol açabileceğini merak ediyorum 6. Tek tahminim, kodun bazı "yerleşik" eşzamanlılıkla çalıştırılabileceğidir. İki ++operatör çağrıldı, her biri idiğeri dönmeden önce artırıldı ve sonra ikisi de geri döndü 3. Bu, çağrı yığınına ilişkin anlayışımla çelişir ve açıklanması gerekir.

Bir C++derleyici, bir sonuca 4veya sonuca yol açacak hangi (makul) şeyleri yapabilir 6?

Not

Bu örnek, Bjarne Stroustrup'un Programlama: C ++ (C ++ 14) kullanarak İlkeler ve Uygulama kitabında tanımlanmamış davranışa bir örnek olarak ortaya çıktı.

Cinnamon'un yorumuna bakın .


5
C spesifikasyonu, yalnızca solda olmak üzere, artırma öncesi / artırma işlemlerine kıyasla = 'nin sağ tarafındaki işlemlerin veya değerlendirmelerin sırasını aslında kapsamaz.
Cristobol Polychronopolis

2
Bu örneği Stroustrup'un bir kitabından aldıysanız (cevaplardan birinin yorumunda belirtildiği gibi) soruda alıntı yapmanızı tavsiye ederiz.
Daniel R. Collins

4
@philipxy Önerdiğiniz kopya, bu sorunun kopyası değil. Sorular farklı. Önerilen mektubunuzdaki cevaplar bu soruyu cevaplamıyor. Önerilen mektubunuzdaki cevaplar, bu soruya verilen kabul edilmiş (veya yüksek oylu) cevapların kopyaları değil. Sanırım sorumu yanlış anladınız. Tekrar okumanızı ve kapatmak için oylamayı yeniden değerlendirmenizi öneririm.
tarçın

3
@philipxy "Cevaplar derleyicinin her şeyi yapabileceğini söylüyor ..." Bu benim soruma cevap vermiyor. "Sorunuzun farklı olduğunu düşünseniz bile, bunun sadece bunun bir varyasyonu olduğunu gösteriyorlar" Ne? "C ++ sürümünüzü vermeseniz de" Benim C ++ sürümüm sorumla alakasız. "bu nedenle, ifadenin bulunduğu tüm program her şeyi yapabilir" Biliyorum, ama sorum belirli davranışlarla ilgiliydi. "Yorumunuz oradaki cevapların içeriğini yansıtmıyor." Yorumum, yeniden okumanız gereken sorumun içeriğini yansıtıyor.
tarçın

2
Başlığa cevap vermek için; çünkü UB, bahaviyanın tanımsız olduğu anlamına gelir. Tarih boyunca farklı zamanlarda, farklı kişiler tarafından farklı mimariler için yapılmış birden çok derleyici, satırların dışını renklendirmeleri ve gerçek bir dünya uygulaması yapmaları istendiğinde , bu kısma şartnamenin dışında bir şey koymak zorunda kaldılar , böylece insanlar tam olarak bunu yaptı ve her biri bunlardan farklı boya kalemleri kullanıldı. Bu nedenle, bayağı özdeyiş, UB'ye güvenmeyin
Toby

Yanıtlar:


199

Derleyici kodunuzu alır, çok basit talimatlara böler ve ardından bunları en uygun olduğunu düşündüğü şekilde yeniden birleştirir ve düzenler.

Kod

int i = 1;
int x = ++i + ++i;

aşağıdaki talimatlardan oluşur:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
5. read i as tmp2
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

Ama benim yazdığım gibi numaralı bir liste olmasına rağmen, burada yalnızca birkaç sıralama bağımlılığı var: 1-> 2-> 3-> 4-> 5-> 10-> 11 ve 1-> 6-> 7- > 8-> 9-> 10-> 11 göreceli sıralarında kalmalıdır. Bunun dışında, derleyici özgürce yeniden sıralayabilir ve belki de fazlalığı ortadan kaldırabilir.

Örneğin, listeyi şu şekilde sipariş edebilirsiniz:

1. store 1 in i
2. read i as tmp1
6. read i as tmp3
3. add 1 to tmp1
7. add 1 to tmp3
4. store tmp1 in i
8. store tmp3 in i
5. read i as tmp2
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

Derleyici bunu neden yapabilir? Çünkü artımın yan etkilerinin sıralaması yoktur. Ama şimdi derleyici basitleştirebilir: örneğin, 4'te ölü bir depo var: değerin üzerine hemen yazılır. Ayrıca, tmp2 ve tmp4 gerçekten aynı şeydir.

1. store 1 in i
2. read i as tmp1
6. read i as tmp3
3. add 1 to tmp1
7. add 1 to tmp3
8. store tmp3 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

Ve şimdi tmp1 ile yapılacak her şey ölü koddur: asla kullanılmaz. Ve i'nin yeniden okunması da ortadan kaldırılabilir:

1. store 1 in i
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
10. add tmp3 and tmp3, as tmp5
11. store tmp5 in x

Bak, bu kod çok daha kısa. İyileştirici mutlu. Programcı değil, çünkü sadece bir kez artırıldım. Oops.

Bunun yerine derleyicinin yapabileceği başka bir şeye bakalım: orijinal sürüme geri dönelim.

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
5. read i as tmp2
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

Derleyici bunu şu şekilde yeniden sıralayabilir:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
5. read i as tmp2
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

ve sonra iki kez okunduğumu tekrar fark edin, bu yüzden bunlardan birini kaldırın:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

Bu güzel, ancak daha da ileri gidebilir: tmp1'i yeniden kullanabilir:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
6. read i as tmp1
7. add 1 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

Daha sonra 6'da i'nin yeniden okunmasını ortadan kaldırabilir:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
7. add 1 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

Şimdi 4 ölü bir depo:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
7. add 1 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

ve şimdi 3 ve 7 tek bir talimatta birleştirilebilir:

1. store 1 in i
2. read i as tmp1
3+7. add 2 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

Son geçici olanı ortadan kaldırın:

1. store 1 in i
2. read i as tmp1
3+7. add 2 to tmp1
8. store tmp1 in i
10. add tmp1 and tmp1, as tmp5
11. store tmp5 in x

Ve şimdi Visual C ++ 'nın size verdiği sonucu elde edersiniz.

Hiçbir şey yapmamak için talimatlar kaldırılmadığı sürece, her iki optimizasyon yolunda da önemli sipariş bağımlılıklarının korunduğunu unutmayın.


36
Şu anda, sıralamadan bahseden tek cevap budur .
PM 2Ring

3
-1 Bu cevabın aydınlatıcı olduğunu düşünmüyorum. Gözlemlenen sonuçlar hiçbir derleyici optimizasyonuna bağlı değildir (cevabıma bakın).
Daniel R. Collins

3
Bu okuma-değiştirme-yazma işlemlerini varsayar. Her yerde bulunan x86 gibi bazı CPU'larda, duruma daha da karmaşıklık katan atomik bir artış işlemi vardır.
Mark

6
@philipxy "Standardın nesne kodu hakkında söyleyecek hiçbir şeyi yoktur." Standardın da bu pasajın davranışı hakkında söyleyecek hiçbir şeyi yoktur - UB'dir. Bu sorunun bir öncülü. OP, pratikte derleyicilerin neden farklı ve tuhaf sonuçlara ulaşabileceğini bilmek istedi. Ayrıca, cevabım nesne kodu hakkında hiçbir şey söylemiyor.
Sebastian Redl

5
@philipxy İtirazınızı anlamıyorum. Belirtildiği gibi, soru, C ++ standardı hakkında değil, bir derleyicinin UB varlığında ne yapabileceği ile ilgilidir. Varsayımsal bir derleyicinin kodu nasıl dönüştürdüğünü keşfederken nesne kodunu kullanmak neden uygunsuz olsun? Aslında, nesne kodu dışında herhangi bir şey nasıl alakalı olabilir?
Konrad Rudolph

58

Bu UB iken (OP'nin ima ettiği gibi), aşağıda bir derleyicinin 3 sonucu alabileceği varsayımsal yollar verilmiştir. Bir ve aynı yerine xfarklı int i = 1, j = 1;değişkenlerle kullanıldığında üçü de aynı doğru sonucu verecektir i.

  1. her ikisi de ++ i 2 döndürür, x = 4 ile sonuçlanır.
int i = 1;
int i1 = i, i2 = i;   // i1 = i2 = 1
++i1;                 // i1 = 2
++i2;                 // i2 = 2
int x = i1 + i2;      // x = 4
  1. bir ++ i, 2'yi ve diğeri 3'ü döndürerek x = 5 sonucunu verir.
int i = 1;
int i1 = ++i;           // i1 = 2
int i2 = ++i;           // i2 = 3
int x = i1 + i2;        // x = 5
  1. her ikisi de ++ i 3 döndürür, sonuçta x = 6 olur.
int i = 1;
int &i1 = i, &i2 = i;
++i1;                   // i = 2
++i2;                   // i = 3
int x = i1 + i2;        // x = 6

2
bu umduğumdan daha iyi bir yanıt, teşekkür ederim.
tarçın

1
1. seçenek için, derleyici ön artırmaya bir not vermiş olabilir i. Sadece bir kez olabileceğini bilmek, onu yalnızca bir kez yayar. Seçenek 2 için kod, bir üniversite derleyici sınıfı projesinin yapabileceği gibi, tam anlamıyla makine koduna çevrilir. 3. seçenek için, 1. seçenek gibi, ancak ön artırmanın iki kopyasını oluşturdu. Küme değil, vektör kullanmış olmalı. :-)
Zan Lynx

@dxiv üzgünüm, benim hatam, gönderileri karıştırdım
muru

22

Bana göre ikincisi büyük olasılıkla görünüyor.

4 numaralı seçeneği tercih ediyorum: Her ikisi de ++iaynı anda gerçekleşir.

Daha yeni işlemciler, bazı ilginç optimizasyonlara ve paralel kod değerlendirmesine doğru ilerliyor, burada izin verildiği yerlerde, derleyicilerin daha hızlı kod üretmeye devam etmelerinin bir başka yolu. Pratik bir uygulama olarak görüyorum , derleyiciler paralelliğe doğru ilerlemektedir.

Belirleyici olmayan davranışa veya aynı bellek çekişmesinden dolayı veri yolu hatasına neden olan bir yarış durumunu kolayca görebiliyordum - kodlayıcı C ++ sözleşmesini ihlal ettiği için hepsine izin verildi - dolayısıyla UB.

Sorum şu: Bir C ++ derleyicisi, 4'ün veya bir sonucun veya 6'nın sonucuna yol açacak (makul) şeyler ne yapabilir?

Bu olabilir , ama içinde sayılmaz.

Kullanmayın ++i + ++ive mantıklı sonuçlar beklemeyin.


Hem bu yanıtı hem de @ dxiv'i kabul edebilseydim kabul ederdim. Cevap için teşekkürler.
tarçın

4
@UriRaz: İşlemci, derleyicinin seçimine bağlı olarak bir veri tehlikesi olduğunu bile fark etmeyebilir. Örneğin, derleyici iiki yazmaç atayabilir , her iki kaydı artırabilir ve her ikisini de geri yazabilir. İşlemcinin bunu çözme yolu yoktur. Temel sorun, ne C ++ ne de modern CPU'ların kesinlikle sıralı olmamasıdır. C ++, varsayılan olarak aynı anda gerçekleşmesine izin vermek için, sıralamadan önce ve sonra olur.
MSalters

1
Ancak Visual Studio kullanan OP için durumun bu olmadığını biliyoruz; x86 ve ARM dahil olmak üzere çoğu genel ISA, bir makine talimatının bir sonraki başlamadan önce tamamen bittiği, tamamen sıralı yürütme modeli açısından tanımlanır. Süper skalar düzensizlik, bu yanılsamayı tek bir iş parçacığı için sürdürmelidir. (Paylaşılan hafızayı okuyan diğer iş parçacıkları şeyleri program sırasına göre göremez, ancak OoO yürütmenin temel kuralı tek iş parçacıklı yürütmeyi bozmamaktır.)
Peter Cordes

1
Bu benim favori cevabım, çünkü CPU seviyesinde paralel komut yürütmeden bahseden tek cevap bu. Btw, ya yarış koşulları nedeniyle cpu iş parçacığının aynı bellek konumunda bir muteks kilidi açılmasını bekleyerek durdurulacağını belirtmek güzel olur, bu nedenle bu eşzamanlılık modelinde çok uygun değildir. İkincisi - aynı yarış durumu nedeniyle gerçek cevap 4veya 5, - cpu iş parçacığı yürütme modeline / hızına bağlı olarak olabilir , bu nedenle bu özünde UB'dir.
Agnius Vasiliauskas

1
@AgniusVasiliauskas Belki de, yine de "Pratikte, farklı derleyiciler neden farklı değerleri hesaplasın", günümüz işlemcilerinin basit bir görünümü göz önüne alındığında, anlaşılması kolay bir şey arıyor. Yine de, belirtilen birkaç cevaptan çok daha büyükse, derleyicilerin / işlemcilerin senaryoları aralığı. Yararlı görüşünüz bir başka. IMO, paralellik geleceğidir ve bu nedenle bu cevap, soyut bir şekilde de olsa - gelecek hala gelişirken bunlara odaklanmıştır. IAC, gönderi popüler hale geldi ve anlaşılması kolay cevaplar en iyi şekilde ödüllendirildi.
chux - Monica'yı eski durumuna getirin

17

Bence basit ve anlaşılır bir yorum (derleyici optimizasyonları veya çoklu okuma için herhangi bir teklif olmaksızın) sadece:

  1. Artış i
  2. Artış i
  3. ekle i+i

İle iiki kere arttırılır, değeri 3 olan ve birlikte ilave edildiği zaman, toplam 6'dır.

İnceleme için bunu bir C ++ işlevi olarak düşünün:

int dblInc ()
{
    int i = 1;
    int x = ++i + ++i;
    return x;   
}

Şimdi, GNU C ++ derleyicisinin eski bir sürümünü (win32, gcc sürüm 3.4.2 (mingw-özel)) kullanarak bu işlevi derlemekten elde ettiğim derleme kodu burada. Burada süslü optimizasyonlar veya çoklu okuma yoktur:

__Z6dblIncv:
    push    ebp
    mov ebp, esp
    sub esp, 8
    mov DWORD PTR [ebp-4], 1
    lea eax, [ebp-4]
    inc DWORD PTR [eax]
    lea eax, [ebp-4]
    inc DWORD PTR [eax]
    mov eax, DWORD PTR [ebp-4]
    add eax, DWORD PTR [ebp-4]
    mov DWORD PTR [ebp-8], eax
    mov eax, DWORD PTR [ebp-8]
    leave
    ret

Yerel değişkenin iyığın üzerinde tek bir yerde bulunduğunu unutmayın: adres [ebp-4]. Bu konum iki kez artırılır (montaj işlevinin 5. ve 8. satırlarında; bu adresin görünüşte fazlalık yükleri dahil eax). Daha sonra 9.-10. satırlarda, bu değer içine yüklenir eaxve sonra eklenir eax(yani akımı hesaplar i + i). Ardından, yığına fazladan kopyalanır ve eaxdönüş değeri olarak geri alınır (ki bu kesinlikle 6 olacaktır).

İfadeler için, bölüm 5.4:

Belirtilenler dışında, tek tek operatörlerin işlenenlerinin ve tek tek ifadelerin alt ifadelerinin değerlendirme sırası ve yan etkilerin meydana gelme sırası belirtilmemiştir.

Dipnot ile:

Operatörlerin önceliği doğrudan belirtilmez, ancak sözdiziminden türetilebilir.

Bu noktada, her ikisi de artırma operatörünü içeren (biri:) belirtilmemiş iki davranış örneği verilmiştir i = ++i + 1.

Şimdi, eğer bir kişi isterse şunları yapabilirdi: Bir tamsayı sarmalayıcı sınıfı (Java Tamsayı gibi) yapabilir; aşırı yük fonksiyonları operator+ve operator++ara değer nesnelerini döndürmeleri; ve böylece yazıp ++iObj + ++iObj5'i tutan bir nesneyi döndürmesini sağlayın (kısalık uğruna buraya tam kodu eklemedim.)

Kişisel olarak, işi yukarıda görülen sekanstan farklı bir şekilde yapan tanınmış bir derleyici örneği olup olmadığını merak etmiştim. Bana öyle geliyor ki, en basit uygulama inc, toplama işlemi gerçekleştirilmeden önce ilkel tipte sadece iki montaj kodu yapmaktır.


2
Artış operatörü gerçekten çok iyi tanımlanmış bir "dönüş" değerine sahip
edc65

@philipxy: İtiraz ettiğiniz pasajları çıkarmak için cevabı düzenledim. Bu noktada yanıta daha çok katılabilir veya katılmayabilirsiniz.
Daniel R. Collins

2
Bunlar "Belirtilmemiş iki davranış örneği" değildir, bunlar tanımlanmamış davranışların iki örneğidir , standarttaki farklı bir pasajdan kaynaklanan çok farklı bir canavar. C ++ 98'in dipnot örneğinin metninde normatif metinle çelişen "belirtilmemiş" dediğini görüyorum, ancak bu daha sonra düzeltildi.
Cubbi

@Cubbi: Burada alıntılanan standarttaki hem metin hem de dipnot "belirtilmemiş", "doğrudan belirtilmemiş" ifadesini kullanıyor ve 1.3.13 bölümündeki tanımlarla eşleşiyor gibi görünüyor.
Daniel R. Collins

1
@philipxy: Görüyorum ki buradaki cevapların çoğuna aynı yorumu tekrarlamışsınız. Görünüşe göre ana eleştiriniz, kapsamı sadece soyut standartla ilgili olmayan OP'nin sorusuyla ilgili.
Daniel R. Collins

7

Bir derleyicinin yapabileceği makul şey, Ortak Alt İfade Eleme'dir. Bu, derleyicilerde zaten yaygın bir optimizasyondur: gibi bir alt (x+1)ifade, daha büyük bir ifadede birden çok kez ortaya çıkarsa, yalnızca bir kez hesaplanması gerekir. İçinde Örneğin alt ifadesi kez hesaplanabilir.a/(x+1) + b*(x+1)x+1

Elbette, derleyicinin bu şekilde hangi alt ifadelerin optimize edilebileceğini bilmesi gerekir. rand()İki kez aramak rastgele iki numara vermelidir. Satır içi olmayan işlev çağrıları bu nedenle CSE'den muaf tutulmalıdır. Dikkat edeceğiniz gibi, iki oluşumun nasıl i++ele alınması gerektiğini söyleyen bir kural yoktur , bu nedenle onları CSE'den muaf tutmak için bir neden yoktur.

Sonuç, gerçekten int x = ++i + ++i;optimize edilmiş olabilir int __cse = i++; int x = __cse << 1. (CSE, ardından tekrarlanan güç azalması)


Standardın nesne kodu hakkında söyleyecek hiçbir şeyi yoktur. Bu, dil tanımıyla gerekçelendirilmiyor veya onunla ilgili değil.
philipxy

1
@philipxy: Standardın, Tanımlanmamış Davranışların herhangi bir biçimi hakkında söyleyecek hiçbir şeyi yoktur. Sorunun temeli bu.
MSalters

7

Pratikte, tanımlanmamış davranışa başvuruyorsunuz. Her şey "makul" düşünün ve genellikle şeyler değil, sadece şeyler olabilir mi makul düşünmüyoruz gerçekleşmesi. Her şey tanımı gereği "makul".

Çok makul bir derleme, derleyicinin bir ifadenin yürütülmesinin tanımsız davranışa yol açacağını gözlemlemesidir, bu nedenle ifade çalıştırılamaz, bu nedenle uygulamanızı kasıtlı olarak çökerten bir talimata çevrilir. Bu çok makul.

Olumsuz oy veren: GCC size kesinlikle katılmıyor.


Standart bir şeyi "tanımlanmamış davranış" olarak nitelendirdiğinde, bu, davranışın Standardın yetki alanı dışında olduğundan daha fazla veya daha az anlamına gelmez . Standart, yetki alanı dışındaki şeylerin makul olup olmadığına karar vermek için hiçbir girişimde bulunmadığından ve uyumlu bir uygulamanın mantıksız bir şekilde yararsız olabileceği tüm yolları yasaklamak için hiçbir girişimde bulunmadığından, Standardın belirli bir durumda gereksinimleri empoze edememesi, herhangi bir yargı anlamına gelmez. olası eylemler eşit derecede "makul" dir.
supercat

6

Bir derleyicinin 6 sonucunu elde etmek için yapabileceği makul bir şey yoktur , ancak bu mümkün ve meşrudur. 4'ün bir sonucu tamamen makul ve 5 sınır çizgisinin sonucunun makul olduğunu düşünüyorum. Hepsi tamamen yasaldır.

Hey bekle! Ne olacağı net değil mi? Ekleme, iki artımın sonucuna ihtiyaç duyar, bu nedenle bunların önce gerçekleşmesi gerektiği açıktır . Ve soldan sağa gidiyoruz, yani ... argh! Keşke bu kadar basit olsaydı. Ne yazık ki, durum bu değil. Biz do not sağa sola sapar ve bu sorun da bu.

Bellek konumunu iki yazmaç halinde okumak (veya her ikisini de aynı değişmez değerden başlatmak, belleğe gidiş dönüşü optimize etmek) derleyicinin yapması gereken çok makul bir şeydir. Bu, etkili bir şekilde , her biri 2 değerine sahip ve sonunda 4'ün bir sonucuna eklenecek olan gizli bir şekilde iki farklı değişken olma etkisine sahip olacaktır. Bu "makul" çünkü hızlı ve verimli ve her ikisine de uygun. standart ve kod ile.

Benzer şekilde, bellek konumu bir kez okunabilir (veya değişmez değerden başlatılan değişken) ve bir kez artırılabilir ve bundan sonra başka bir kayıt defterindeki bir gölge kopya artırılabilir, bu da 2 ve 3'ün birlikte eklenmesiyle sonuçlanır. Bu, diyebilirim ki, tamamen yasal olsa da , sınırda makul. Bunu makul buluyorum çünkü biri ya da diğeri değil. Bu ne "makul" optimize edilmiş yol ne de "mantıklı" tam anlamıyla bilgiçlikçi bir yol değil. Biraz ortada.

Bellek konumunu iki kez artırmak (3 değeriyle sonuçlanır) ve ardından 6'nın nihai sonucu için bu değeri kendisine eklemek yasaldır, ancak bellek döngüleri yapmak tam olarak verimli olmadığından pek makul değildir. İyi bir mağaza iletimi olan bir işlemci üzerinde, mağaza çoğunlukla görünmez olması gerektiğinden, bunu yapmak "makul" olabilir ...
Derleyici, aynı konum olduğunu "bildiği" için, artırmayı da seçebilir değeri bir kayıt içinde iki kez girin ve ardından kendisine de ekleyin. Her iki yaklaşım da size 6'nın sonucunu verecektir.

Derleyici, standardın ifadesiyle, size böyle bir sonuç vermesine izin verilmiş olsa da, ben şahsen, oldukça beklenmedik bir şey olduğundan (yasal veya değil, her zaman en az miktarda sürpriz vermeye çalışmak, yapılacak iyi bir şeydir!). Yine de, Tanımsız Davranış'ın nasıl işin içine girdiğini görünce, ne yazık ki kimse "beklenmedik" hakkında gerçekten tartışamaz, eh.

Öyleyse, aslında, derleyicide sahip olduğunuz kod nedir? Güzelce sorarsak bize gösterecek olan clang'a soralım (ile çağırarak -ast-dump -fsyntax-only):

ast.cpp:4:9: warning: multiple unsequenced modifications to 'i' [-Wunsequenced]
int x = ++i + ++i;
        ^     ~~
(some lines omitted)
`-CompoundStmt 0x2b3e628 <line:2:1, line:5:1>
  |-DeclStmt 0x2b3e4b8 <line:3:1, col:10>
  | `-VarDecl 0x2b3e430 <col:1, col:9> col:5 used i 'int' cinit
  |   `-IntegerLiteral 0x2b3e498 <col:9> 'int' 1
  `-DeclStmt 0x2b3e610 <line:4:1, col:18>
    `-VarDecl 0x2b3e4e8 <col:1, col:17> col:5 x 'int' cinit
      `-BinaryOperator 0x2b3e5f0 <col:9, col:17> 'int' '+'
        |-ImplicitCastExpr 0x2b3e5c0 <col:9, col:11> 'int' <LValueToRValue>
        | `-UnaryOperator 0x2b3e570 <col:9, col:11> 'int' lvalue prefix '++'
        |   `-DeclRefExpr 0x2b3e550 <col:11> 'int' lvalue Var 0x2b3e430 'i' 'int'
        `-ImplicitCastExpr 0x2b3e5d8 <col:15, col:17> 'int' <LValueToRValue>
          `-UnaryOperator 0x2b3e5a8 <col:15, col:17> 'int' lvalue prefix '++'
            `-DeclRefExpr 0x2b3e588 <col:17> 'int' lvalue Var 0x2b3e430 'i' 'int'

Gördüğünüz gibi, aynı lvalue Var 0x2b3e430önek ++iki konuma uygulanmıştır ve bu ikisi ağaçta aynı düğümün altındadır ve bu, sıralama veya benzeri hakkında özel bir şey söylenmeyen çok özel olmayan bir operatördür (+). Bu neden önemli? Okumaya devam edin.

Şu uyarıya dikkat edin: "'i'ye birden fazla sıralanmamış değişiklik" . Oh oh, kulağa hoş gelmiyor. Bunun anlamı ne? [basic.exec] bize yan etkiler ve sıralama hakkında bilgi verir ve bize (paragraf 10), aksi açıkça belirtilmedikçe, varsayılan olarak , tek tek operatörlerin işlenenlerinin ve tek tek ifadelerin alt ifadelerinin değerlendirmelerinin sırasız olduğunu söyler . Lanet olsun, durum bu operator+- başka türlü hiçbir şey söylenmiyor, yani ...

Ama önceden sıralı, belirsiz bir şekilde sıralı veya sırasız olmasını önemsiyor muyuz? Zaten kim bilmek ister ki?

Aynı paragraf bize sıralı olmayan değerlendirmelerin çakışabileceğini ve aynı bellek konumuna (durum bu!) Ve birinin potansiyel olarak eşzamanlı olmadığına başvurduklarında davranışın tanımsız olduğunu da söyler . İşin gerçekten çirkinleştiği yer burasıdır çünkü bu, hiçbir şey bilmediğiniz anlamına gelir ve "makul" olma konusunda hiçbir garantiniz yoktur. Mantıksız olan şey aslında tamamen kabul edilebilir ve "makul".


"Makul" kelimesinin kullanılması, herhangi birinin "derleyici HER ŞEYİ yapabilir, hatta 'x'i 7'ye ayarla' tek bir talimat bile verebilir" demesini engellemek içindi belki de açıklığa kavuşturmalıydım.
tarçın

@cinnamon Yıllar önce, ben genç ve deneyimsizken, Sun'daki derleyici mühendisleri bana derleyicilerinin o zamanlar mantıksız bulduğum tanımlanmamış davranışlar için kesinlikle makul bir kod üretme görevi yaptığını söylediler. Ders öğrenildi.
gnasher729

Standardın nesne kodu hakkında söyleyecek hiçbir şeyi yoktur. Bu, önerilen uygulamalarınızın dil tanımına göre nasıl gerekçelendirildiği veya bununla ilişkili olduğu konusunda parçalanmış ve belirsizdir.
philipxy

@philipxy: Standart, neyin iyi biçimlendirilmiş ve iyi tanımlanmış olduğunu ve neyin olmadığını tanımlar. Bu Q durumunda, davranışı tanımsız olarak tanımlar. Yasal olmanın ötesinde, verimli kod üreten derleyicilerin makul varsayımı da vardır. Evet, haklısınız, standart böyle olmasını gerektirmiyor. Yine de makul bir varsayımdır.
Damon

1

Bir kural var :

Önceki ve sonraki sıra noktası arasında, bir skaler nesnenin depolanmış değeri, bir ifadenin değerlendirilmesiyle en fazla bir kez değiştirilmiş olmalıdır, aksi takdirde davranış tanımsızdır.

Dolayısıyla x = 100 bile olası bir geçerli sonuçtur.

Benim için bu örnekteki en mantıklı sonuç 6'dır, çünkü i'nin değerini iki kez artırıyoruz ve onlar kendisine ekliyor. "+" Nın her iki tarafından hesaplama değerlerinden önce toplama yapmak zordur.

Ancak derleyici geliştiricileri başka herhangi bir mantığı uygulayabilir.


0

Görünüşe göre ++ i bir lvalue döndürüyor ama i ++ bir rvalue döndürüyor.
Yani bu kod tamam:

int i = 1;
++i = 10;
cout << i << endl;

Bu değil:

int i = 1;
i++ = 10;
cout << i << endl;

Yukarıdaki iki ifade VisualC ++, GCC7.1.1, CLang ve Embarcadero ile uyumludur.
Bu nedenle VisualC ++ ve GCC7.1.1'deki kodunuz aşağıdakine benzer

int i = 1;
... do something there for instance: ++i; ++i; ...
int x = i + i;

Sökmeye bakıldığında, önce i'yi artırır, i'yi yeniden yazar. Eklemeye çalışırken aynı şeyi yapar, i'yi artırır ve yeniden yazar. Sonra i'ye i ekler. CLang ve Embarcadero'nun farklı davrandığını fark ettim. Bu nedenle, ilk ifade ile tutarlı değildir, ilk ++ i'den sonra sonucu bir rvalue içinde depolar ve sonra onu ikinci i ++ 'ya ekler.
görüntü açıklamasını buraya girin


"Look line an lvalue" ile ilgili sorun, bir derleyici değil, C ++ standardı perspektifinden konuşmanızdır.
MSalters

@MSalters İfade, VisualStudio 2019, GCC7.1.1, clang ve Embarcadero ve ilk kod parçasıyla uyumludur. Yani spesifikasyon tutarlıdır. Ancak ikinci kod parçası için farklı çalışır. İkinci kod parçası, VisualStudio 2019 ve GCC7.1.1 ile uyumludur ancak clang ve Embarcadero ile tutarlı değildir.
armagedescu

3
Pekala, cevabınızdaki ilk kod parçası yasal C ++, bu yüzden uygulamaların spesifikasyonla tutarlı olduğu açıktır. Soruya kıyasla, "bir şeyler yap" seçeneğiniz noktalı virgülle biter ve onu tam bir ifade yapar. Bu, C ++ standardının gerektirdiği, ancak soruda bulunmayan bir sıralama oluşturur.
MSalters

@MSalters Bunu eşdeğer bir sözde kod olarak yapmak istedim. Yine de onu nasıl yeniden biçimlendireceğimden emin değilim
armagedescu

0

Şahsen ben sizin örneğinizde bir derleyicinin 6 çıktısını almasını beklemiyordum. Sorunuza zaten iyi ve ayrıntılı cevaplar var. Kısa bir versiyon deneyeceğim.

Temel olarak, ++ibu bağlamda 2 aşamalı bir süreçtir:

  1. Değerini artırın i
  2. Değerini okuyun i

++i + ++iEklemenin iki tarafı bağlamında standarda göre herhangi bir sırada değerlendirilebilir. Bu, iki artışın bağımsız kabul edildiği anlamına gelir. Ayrıca iki terim arasında bağımlılık yoktur. Artış ve okuma ibu nedenle araya eklenebilir. Bu, potansiyel düzeni verir:

  1. Artırma isol işlenen için
  2. Artırma isağ işlenen için
  3. iSol işlenen için geri okuyun
  4. iDoğru işlenen için tekrar okuyun
  5. İkisini toplayın: 6 verir

Şimdi, bunu düşündüğüme göre 6 standarda göre en mantıklı olanı. 4'ün bir sonucu için, önce ibağımsız olarak okuyan , sonra değeri artıran ve aynı konuma geri yazan bir CPU'ya ihtiyacımız var ; temelde bir yarış durumu. 5 değeri için geçiciler sunan bir derleyiciye ihtiyacımız var.

Ancak standart ++i, değişkeni döndürmeden, yani mevcut kod satırını fiilen çalıştırmadan önce artırdığını söyler . Toplam operatörünün artışları uyguladıktan sonra +toplaması gerekir i + i. C ++ 'nın bir değer semantikinde değil değişkenler üzerinde çalışması gerektiğini söyleyebilirim. Bu nedenle, CPU'ların yürütme modeline değil, dilin anlambilimine dayandığından, benim için 6 şimdi en mantıklı olanıdır.


0
#include <stdio.h>


void a1(void)
{
    int i = 1;
    int x = ++i;
    printf("i=%d\n",i);
    printf("x=%d\n",x);
    x = x + ++i;    // Here
    printf("i=%d\n",i);
    printf("x=%d\n",x);
}


void b2(void)
{
    int i = 1;
    int x = ++i;
    printf("i=%d\n",i);
    printf("x=%d\n",x);
    x = i + ++i;    // Here
    printf("i=%d\n",i);
    printf("x=%d\n",x);
}


void main(void)
{
    a1();
    // b2();
}

Stackoverflow'a hoş geldiniz! Cevabınızda herhangi bir sınırlama, varsayım veya basitleştirme sağlayabilir misiniz? Bu bağlantıda nasıl yanıt vereceğinizle ilgili daha fazla ayrıntıya bakın: stackoverflow.com/help/how-to-answer
Usama Abdulrehman

0

Bu, derleyicinin tasarımına bağlıdır.Bu nedenle cevap, derleyicinin ifadeleri çözme şekline bağlı olacaktır. Bunun yerine bir mantık oluşturmak için ++ x ve ++ y iki farklı değişken kullanmak daha iyi bir seçim olacaktır. not: çıktı, güncellenmişse ms visual studio'daki en son dil sürümüne bağlıdır.Yani kurallar değiştiyse çıktı


0

Bunu dene

int i = 1;
int i1 = i, i2 = i;   // i1 = i2 = 1
++i1;                 // i1 = 2
++i2;                 // i2 = 2
int x = i1 + i2;      // x = 4

-4

Pratikte, tanımlanmamış davranışa başvuruyorsunuz. Her şey "makul" düşünün ve genellikle şeyler değil, sadece şeyler olabilir mi makul düşünmüyoruz gerçekleşmesi. Her şey tanımı gereği "makul".

Çok makul bir derleme, derleyicinin bir ifadenin yürütülmesinin tanımlanmamış davranışa neden olacağını gözlemlemesi, bu nedenle ifade hiçbir zaman yürütülemez, bu nedenle uygulamanızı kasıtlı olarak çökerten bir talimata çevrilir. Bu çok makul. Sonuçta, derleyici bu çökmenin asla olmayacağını bilir.


1
Soruyu yanlış anladığına inanıyorum. Soru, belirli sonuçlara (x = 4, 5 veya 6'nın sonuçları) götürebilecek genel veya özel davranışlarla ilgilidir. "Makul" kelimesini kullanmamdan hoşlanmıyorsanız, sizi yanıtladığınız yukarıdaki yorumuma yönlendiriyorum: "'Makul' ifadesinin kullanımı, herhangi birinin 'derleyici HER ŞEYİ yapabilir' demesini engellemek içindi. tek bir yönerge bile versin '"x'i 7'ye ayarlayın." "" Genel fikri koruyan soru için daha iyi bir üslupunuz varsa, ona açığım. Ayrıca, cevabınızı yeniden göndermişsiniz gibi görünüyor.
tarçın

2
her ikisi de birbirine çok benzediği için iki cevabınızdan birini silmenizi önerin
MM
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.