C ++ standardı, başlatılmamış bir bool'un bir programı çökmesine izin veriyor mu?


500

C ++ bir "tanımsız davranış" derleyicinin istediği her şeyi yapmasına izin verebilir biliyorum. Ancak, kodun yeterince güvenli olduğunu düşündüğüm için beni şaşırtan bir çöküş yaşadım.

Bu durumda, asıl sorun sadece belirli bir derleyici kullanan belirli bir platformda ve sadece optimizasyon etkinleştirildiğinde gerçekleşti.

Sorunu yeniden oluşturmak ve en üst düzeye çıkarmak için birkaç şey denedim. Burada Serialize, bool parametresini alan ve dizeyi trueveya falsevarolan bir hedef arabelleğe kopyalanacak bir işlev özütü verilmiştir .

Bu işlev bir kod incelemesinde olurdu, aslında bool parametresi başlatılmamış bir değer olsaydı çökebileceğini söylemenin bir yolu olmaz mı?

// Zero-filled global buffer of 16 characters
char destBuffer[16];

void Serialize(bool boolValue) {
    // Determine which string to print based on boolValue
    const char* whichString = boolValue ? "true" : "false";

    // Compute the length of the string we selected
    const size_t len = strlen(whichString);

    // Copy string into destination buffer, which is zero-filled (thus already null-terminated)
    memcpy(destBuffer, whichString, len);
}

Bu kod clang 5.0.0 + optimizasyonlarıyla yürütülürse çökebilir / çökebilir.

Beklenen üçlü operatör boolValue ? "true" : "false"benim için yeterince güvenli görünüyordu, "Çöp değeri ne olursa olsun boolValueönemli değil, çünkü yine de doğru veya yanlış olarak değerlendirilecek."

Sökme, burada tam örnek sorunu gösteren bir Derleyici Gezgini örneği kurduk. Not: sorunu tekrarlamak için, işe yaradığını bulduğum kombinasyon, Clang 5.0.0'ı -O2 optimizasyonu ile kullanmaktır.

#include <iostream>
#include <cstring>

// Simple struct, with an empty constructor that doesn't initialize anything
struct FStruct {
    bool uninitializedBool;

   __attribute__ ((noinline))  // Note: the constructor must be declared noinline to trigger the problem
   FStruct() {};
};

char destBuffer[16];

// Small utility function that allocates and returns a string "true" or "false" depending on the value of the parameter
void Serialize(bool boolValue) {
    // Determine which string to print depending if 'boolValue' is evaluated as true or false
    const char* whichString = boolValue ? "true" : "false";

    // Compute the length of the string we selected
    size_t len = strlen(whichString);

    memcpy(destBuffer, whichString, len);
}

int main()
{
    // Locally construct an instance of our struct here on the stack. The bool member uninitializedBool is uninitialized.
    FStruct structInstance;

    // Output "true" or "false" to stdout
    Serialize(structInstance.uninitializedBool);
    return 0;
}

Sorun optimizer doğar: Bu "gerçek" ve "yanlış" sadece 1. Yani yerine gerçekten uzunluğunu hesaplayarak uzunluğunda farklılık dizeleri, hangisi, bool kendisinin değerini kullandığı anlamak için zeki yeterliydi gerektiği teknik olarak 0 veya 1 olabilir ve şöyle gider:

const size_t len = strlen(whichString); // original code
const size_t len = 5 - boolValue;       // clang clever optimization

Bu "zekice" olsa da, benim sorum şu: C ++ standardı, bir derleyicinin bir boolun yalnızca dahili sayısal temsili '0' veya '1' olabileceğini ve bu şekilde kullanmasını sağlıyor mu?

Yoksa bu, uygulama tarafından tanımlanmış bir durum mudur, bu durumda uygulama, tüm havuzlarının yalnızca 0 veya 1 içereceğini ve başka herhangi bir değerin tanımlanmamış davranış bölgesi olduğunu varsayar mı?


200
Harika bir soru. Tanımsız davranışın nasıl sadece teorik bir endişe olmadığının sağlam bir örneğidir. İnsanlar UB'nin bir sonucu olarak herhangi bir şey olabileceğini söylediklerinde, bu "her şey" gerçekten şaşırtıcı olabilir. Tanımsız davranışın hala öngörülebilir şekillerde ortaya çıktığı varsayılabilir, ancak bu günlerde modern optimizatörlerle bu hiç de doğru değildir. OP bir MCVE oluşturmak için zaman ayırdı, problemi iyice araştırdı, sökme işlemini inceledi ve bu konuda açık ve anlaşılır bir soru sordu. Daha fazla bilgi için sorun değil.
John Kugelman

7
“Sıfırdan farklı olarak değerlendirilen” gereksiniminin, “ truebir boole atama” ( static_cast<bool>()spesifikasyonlara bağlı olarak örtük olarak çağırabilecek ) dahil Boole işlemleri hakkında bir kural olduğunu gözlemleyin . Bununla birlikte bool, derleyici tarafından seçilen bir dahili temsil ile ilgili bir gereklilik değildir .
Euro Micelli

2
Yorumlar uzun tartışmalar için değildir; bu sohbet sohbete taşındı .
Samuel Liew

3
Çok ilgili bir kayda göre bu, ikili uyumsuzluğun "eğlenceli" bir kaynağıdır. Bir işlevi çağırmadan önce değerleri sıfırlayan bir ABI A'ya sahipseniz, ancak parametreleri sıfır dolgulu olarak kabul edecek şekilde fonksiyonları derler ve bunun tersi olan bir ABI B (sıfır dolgusu değil, sıfır kabul etmez) -padded parametreleri), çoğunlukla işe yarayacaktır, ancak B ABI kullanan bir işlev, 'küçük' bir parametre alan A ABI kullanan bir işlevi çağırırsa sorunlara neden olur. IIRC bunu c86 ve ICC ile x86'da var.
TLW

1
@TLW: Standart, uygulamaların dış kod tarafından çağrılması veya çağrılması için herhangi bir araç sağlamasını gerektirmese de, ilgili olduğu uygulamalar için bu tür şeyleri belirtmenin bir yoluna sahip olmak yararlı olurdu (bu tür ayrıntıların bulunmadığı uygulamalar) alakalı) bu nitelikleri göz ardı edebilir).
Supercat

Yanıtlar:


285

Evet, ISO C ++ bu seçimi yapmak için uygulamalara izin verir (ancak zorunlu değildir).

Ancak, programın UB ile karşılaşması durumunda, örneğin hataları bulmanıza yardımcı olacak bir yol olarak, ISO C ++ 'ın bir derleyicinin bilerek çökmesini (örneğin geçersiz bir talimatla) yayınlamasına izin verdiğini unutmayın. (Ya da bir DeathStation 9000 olduğu için. C ++ uygulamasının herhangi bir gerçek amaç için yararlı olması için kesinlikle uygun olmak yeterli değildir). Böylece ISO C ++, bir derleyicinin başlatılmamış bir kodu okuyan benzer kodda bile çökmesini (tamamen farklı nedenlerle) yapmasına izin verecektir uint32_t. Her ne kadar bu, tuzak temsili olmayan sabit düzenli bir tip olmalıdır.

Gerçek uygulamaların nasıl çalıştığı hakkında ilginç bir soru, ancak cevap farklı olsa bile, kodunuzun hala güvenli olmayacağını unutmayın, çünkü modern C ++, montaj dilinin taşınabilir bir sürümü değildir.


X86-64 System V ABI için derlersiniz ; bu bool, bir kayıttaki bir işlev argümanı olarakfalse=0true=1 kayıt 1'in bit desenleri ve düşük 8 bitiyle temsil edildiğini belirtir . Bellekte, boolyine 0 veya 1 tamsayı değerine sahip olması gereken 1 baytlık bir türdür.

(Bir ABI, aynı platform için derleyicilerin üzerinde anlaştığı bir dizi uygulama seçeneğidir, böylece tür boyutları, yapı düzeni kuralları ve çağrı kuralları dahil olmak üzere birbirlerinin işlevlerini çağıran kodlar oluşturabilirler.)

ISO C ++ bunu belirtmez, ancak bu ABI kararı yaygındır, çünkü bool-> int dönüşümünü ucuz yapar (sadece sıfır uzatma) . boolHerhangi bir mimari (sadece x86 için değil) için derleyicinin 0 veya 1 varsaymasına izin vermeyen ABI'lerin farkında değilim . Bu optimizasyonlar gibi tanır !myboolile xor eax,1: düşük bit çevirmek için tek işlemci talimatında 0 ile 1 arasında bir bit / tamsayı / bool çevirebilirsiniz Herhangi olası bir kod . Veya türler a&&biçin bit yönünde VE derleme bool. Bazı derleyiciler gerçekte Boolean değerlerini derleyicilerde 8 bit olarak kullanırlar. Üzerlerindeki işlemler yetersiz mi? .

Genel olarak, as-if kuralı, derleyicinin derlenmekte olduğu hedef platformda doğru olan şeylerden yararlanmasına izin verir , çünkü sonuç, C ++ kaynağıyla aynı dıştan görünür davranışı uygulayan yürütülebilir kod olacaktır. (Tanımsız Davranış'ın aslında "harici olarak görünür" olana getirdiği tüm kısıtlamalarla: bir hata ayıklayıcıyla değil, iyi biçimlendirilmiş / yasal bir C ++ programındaki başka bir iş parçacığından.)

Derleyici kesinlikle onun kod-gen bir ABI teminat tam olarak yararlanabilmek ve optimize hangi bulundu gibi bir kod yapmak için izin strlen(whichString)için
5U - boolValue.
(BTW, bu optimizasyon biraz zekidir, ancak memcpyanlık verilerin depolanması olarak dallanma ve satır içi ile karşılaştırıldığında dar görüşlü olabilir 2 )

Veya derleyici bir işaretçi tablosu oluşturabilir ve boolyine 0 veya 1 olduğunu varsayarak tamsayı değeri ile dizine ekleyebilirdi . ( Bu olasılık @ Barmar'ın cevabının önerdiği şeydir .)


Kişisel __attribute((noinline))optimizasyonu ile yapıcı olarak kullanılmak üzere yığınından bir bayt yüklenirken sadece tınlamak yol açtı etkin uninitializedBool. Bu nesne için yer yapılmış mainolan push rax(olabildiğince verimli olarak ilgili küçük ve çeşitli nedenle hangi sub rsp, 8olursa olsun bu yüzden çöp girişte AL oldu) mainonun için kullanılan değerdir uninitializedBool. Bu yüzden aslında sadece olmayan değerlere sahipsiniz 0.

5U - random garbagebüyük bir imzasız değere kolayca sarılabilir, memcpy'nin eşlenmemiş belleğe gitmesine neden olur. Hedef yığını değil, statik depolama alanındadır, bu nedenle bir dönüş adresinin veya başka bir şeyin üzerine yazmazsınız.


Diğer uygulamalar farklı seçimler yapabilir, örn. false=0Ve true=any non-zero value. Sonra clang muhtemelen bu özel UB örneği için kilitlenen kod yapmaz . (Ama yine de isteseydi izin verilecekti.) X86-64'ün yaptıklarından başka bir şey seçen herhangi bir uygulama bilmiyorum bool, ancak C ++ standardı kimsenin yapmadığı ve hatta yapmak istemeyeceği birçok şeye izin veriyor mevcut CPU'lar gibi bir donanım.

ISO C ++, a nesnesinin temsilini incelediğinizde veya değiştirdiğinizde ne bulacağınızı belirtmeden bırakırbool . (örn memcpy. booliçine girerek unsigned charizin verebilirsiniz, çünkü char*herhangi bir şeyi takma adlandırabilirsiniz. Ve unsigned charhiçbir dolgu bitine sahip olmadığı garanti edilir, bu nedenle C ++ standardı resmi olarak herhangi bir UB olmadan nesne temsillerini hexdump yapmanızı sağlar. Nesneyi kopyalamak için işaretçi döküm temsil, char foo = my_boolelbette, atamaktan farklıdır , bu nedenle 0 veya 1'e booleanization olmaz ve ham nesne temsilini alırsınız.)

Sen ettik kısmen ile derleyici Bu yürütme yolunda UB "gizli"noinline . Bununla birlikte, satır içi olmasa bile, süreçler arası optimizasyonlar hala başka bir fonksiyonun tanımına bağlı olan fonksiyonun bir versiyonunu yapabilir. (Birincisi, clang, sembol-interpozisyonun olabileceği Unix paylaşılan kütüphanesini değil, yürütülebilir bir dosya yapıyor. İkincisi, tanımın içindeki class{}tanım, böylece tüm çeviri birimlerinin aynı tanıma sahip olması gerekir. inlineAnahtar kelimede olduğu gibi.)

Böylece bir derleyici , tanım olarak sadece bir retveya ud2(yasadışı talimat) yayabilir main, çünkü mainkaçınılmaz olarak üstünden başlayan yürütme yolu, Tanımlanamayan Davranış ile karşılaşır. (Derleyici, satır içi olmayan kurucu aracılığıyla yolu izlemeye karar verirse derleme zamanında görebileceği.)

UB ile karşılaşan herhangi bir program tüm varlığı için tanımsızdır. Ama if()aslında hiç çalışmayan bir fonksiyonun veya dalın içindeki UB , programın geri kalanını bozmaz. Uygulamada bu, derleyicilerin yasadışı bir talimat vermeye veya bir retşey yaymaya veya herhangi bir şey yaymaya ve bir sonraki bloğa / fonksiyona girmeye karar verebileceği anlamına gelir; derleme zamanında UB içerdiği veya yol açtığı kanıtlanabilen tüm temel blok için.

GCC ve pratikte Clang do aslında bazen yayarlar ud2yerine bile hiçbir anlam yürütme yolları için kod oluşturmak için çalışmak yerine, UB üzerinde. Veya bir voidişlev dışı durumun sonundan düşme gibi durumlarda , gcc bazen bir rettalimatı atlar . Eğer "işlevim RAX'taki çöp ne olursa olsun geri dönecek" diye düşünüyorsan, yanılıyorsun. Modern C ++ derleyicileri artık portatif bir montaj dili gibi davranmıyor. Programınızın, işlevinizin bağımsız, satır içi olmayan bir sürümünün nasıl görünebileceği konusunda varsayımlar yapmadan, gerçekten geçerli C ++ olması gerekir.

Başka bir eğlenceli örnek, mmap'ed belleğe hizalanmamış erişim neden AMD64'te bazen segfault oluyor? . x86, hizalanmamış tamsayılarda hata değil, değil mi? Öyleyse yanlış hizalanmış uint16_t*bir sorun neden sorun olsun ki? Çünkü alignof(uint16_t) == 2ve bu varsayımı ihlal etmek, SSE2 ile otomatik vektörleştirilirken bir segfault yol açmıştır.

Ayrıca bkz. Her C Programcısının bir clang geliştiricisi tarafından yazılan Tanımsız Davranış # 1/3 Hakkında Bilmesi Gerekenler .

Anahtar nokta: derleyici derleme zamanında UB fark ettiyseniz, bu olabilir "mola" nedenler bile herhangi bit deseni için geçerli bir nesne temsilidir ABI hedefleyen eğer UB senin kodu ile yol (şaşırtıcı asm yayarlar) bool.

Programcı tarafından, özellikle modern derleyicilerin uyardığı birçok hataya karşı tam bir düşmanlık bekliyoruz. Bu yüzden -Walluyarıları kullanmalı ve düzeltmelisiniz. C ++ kullanıcı dostu bir dil değildir ve C ++ 'da bir şey, derlediğiniz hedefe güvenli bir şekilde güvenli olsa bile güvenli olmayabilir. (örneğin, imzalı taşma C ++ 'da UB'dir ve siz kullanmadıkça, 2'nin tamamlayıcısı x86 için derleme yaparken bile derleyiciler gerçekleşmeyeceğini varsayar clang/gcc -fwrapv.)

Derleme zamanı görünür UB her zaman tehlikelidir ve UB'yi derleyiciden gerçekten gizlediğinizden ve böylece ne tür bir asm üreteceğinden emin olabileceğinizden (bağlantı zamanı optimizasyonu ile) gerçekten zor.

Aşırı dramatik olmamak; genellikle derleyiciler bazı şeylerden kurtulmanıza izin verir ve bir şey UB olsa bile beklediğiniz gibi kod yayarlar. Ancak, derleyici geliştiricilerin değer aralıkları hakkında daha fazla bilgi toplayan bir optimizasyon uygulaması gelecekte de bir sorun olacaktır (örneğin, bir değişkenin negatif olmadığı, belki de işaret uzantısını x86- 64). Örneğin, mevcut gcc ve clang'da yapmak her zaman yanlış olarak tmp = a+INT_MINoptimize edilmez a<0, sadece bu tmpher zaman negatiftir. (Çünkü INT_MIN+ a=INT_MAXbu 2'nin tamamlayıcı hedefinde negatiftir ve abundan daha yüksek olamaz.)

Dolayısıyla, gcc / clang şu anda bir hesaplamanın girdileri için aralık bilgisi türetmek için geriye doğru ilerlemiyor , sadece imzalı taşma varsayımına dayanan sonuçlara dayanıyor: Godbolt örneği . Bu optimizasyon kasıtlı olarak kullanıcı dostu ya da ne adına "özledim" olup olmadığını bilmiyorum.

Ayrıca, uygulamaların (derleyiciler olarak da bilinir) ISO C ++ 'nın tanımsız bıraktığı davranışı tanımlamasına izin verildiğini unutmayın . Örneğin, tüm derleyiciler (gibi destek Intel'in intrinsics o _mm_add_ps(__m128, __m128)manuel SIMD vektörleştirme için) bile eğer C ++ UB olduğunu yanlış hizalanmış işaretçileri, şekillendirme izin vermelidir yok onlara KQUEUE. a veya değil, __m128i _mm_loadu_si128(const __m128i *)yanlış hizalanmış bir __m128i*argüman alarak hizalanmamış yükler yapar . Donanım vektörü işaretçisi ve karşılık gelen tip arasındaki reinterpret_cast tanımsız bir davranış mı?void*char*

GNU C / C ++ -fwrapv, normal işaretli taşma UB kurallarından ayrı olarak negatif işaretli bir sayıyı (olmadan da ) sola kaydırma davranışını da tanımlar . ( Bu, ISO C ++ 'da UB'dir, işaretli sayıların sağ kaymaları uygulama tanımlıdır (mantıksal ve aritmetik); kaliteli uygulamalar HW'de aritmetik sağ kayması olan aritmetiği seçer, ancak ISO C ++ belirtmez). Bu, C standartlarının bir şekilde tanımlanması için uygulamaların gerektirdiği uygulama tanımlı davranışı tanımlamanın yanı sıra GCC kılavuzunun Tamsayı bölümünde belgelenmiştir .

Derleyici geliştiricilerin önem verdiği kesinlikle uygulama kalitesi sorunları vardır; genellikle kasıtlı olarak düşmanca olan derleyiciler yapmaya çalışmazlar , ancak daha iyi optimize etmek için C ++ 'daki tüm UB çukurlarından (tanımlamayı seçtikleri hariç) faydalanmak zaman zaman neredeyse ayırt edilemez olabilir.


Dipnot 1 : Üst 56 bit, her zamanki gibi bir kayıttan daha dar tipler için callee'nin göz ardı etmesi gereken çöp olabilir.

( Diğer ABI yapmak burada farklı seçimler yapmak . Geçirilen veya MIPS64 ve PowerPC64 gibi fonksiyonlar, döndüğünde bazı sıfır veya oturum genişletilmiş bir kayıt doldurmak için olmak dar tamsayı tiplerini gerektirir. Son bölümüne bakın bu X86-64 cevap önceki ISA'larla karşılaştırılan )

Örneğin, bir arayan a & 0x01010101, arama yapmadan önce RDI'da hesaplamış ve başka bir şey için kullanmış olabilir bool_func(a&1). Arayan bunu optimize edebilir, &1çünkü bunu zaten bir parçası olarak düşük bayta yaptı ve arayanın and edi, 0x01010101yüksek baytları görmezden gelmesi gerektiğini biliyor.

Veya 3. bağımsız değişken olarak bir bool iletilirse, kod boyutu için optimize eden bir arayan, mov dl, [mem]bunun yerine yükler movzx edx, [mem], RDX'in eski değerine yanlış bir bağımlılık pahasına 1 bayt kaydeder (veya diğer kısmi kayıt efekti, CPU modelinde). Ya da ilk argüman mov dil, byte [r10]yerine movzx edi, byte [r10], her ikisi de yine de bir REX öneki gerektirir.

Bu yüzden çınlama yayar olduğu movzx eax, diliçinde Serializeyerine, sub eax, edi. (Tamsayı args için, çınlama yerine sıfır veya gcc ve clang belgesiz davranışa bağlı olarak, bu ABI kural ihlal eder. 32 bit, dar tamsayılar oturum uzanan bir işareti mi ya da bir işaretçiye ofset 32bit eklerken sıfır uzantısı gerekli x86-64 ABI? bu yüzden aynı şeyi yapmaz görmeye ilgilenen edildi bool.)


Dipnot 2: Dallanma sonrasında, sadece 4 bayt- movorta veya 4 bayt + 1 baytlık bir mağazanız olur. Uzunluk, mağaza genişlikleri + ofsetlerde örtüktür.

OTOH, glibc memcpy, uzunluğa bağlı bir çakışma ile iki adet 4 baytlık yük / depo yapacak, bu gerçekten her şeyi booleandaki koşullu dallardan arındırır. Glibc'nin memcpy / memmove içindeki L(between_4_7):bloğa bakın . Ya da en azından, memcpy'nin dallamasındaki boole için bir yığın boyutu seçmek için aynı şekilde gidin.

Satır içi ise, 2x mov-immediate + cmovve koşullu bir ofset kullanabilirsiniz veya dize verilerini bellekte bırakabilirsiniz.

Veya Intel Ice Lake için ayarlama yapıyorsanız ( Hızlı Kısa REP MOV özelliği ile ), gerçek bir rep movsboptimal olabilir. glibc , bu özelliğe sahip CPU'lardaki küçük boyutlar için memcpykullanmaya başlayarak rep movsbçok fazla dallanma tasarrufu sağlayabilir.


UB tespiti ve başlatılmamış değerlerin kullanımı için araçlar

Gcc ve clang'da, -fsanitize=undefinedçalışma zamanında gerçekleşen UB'yi uyaracak veya hata verecek çalışma zamanı enstrümanları eklemek için derleyebilirsiniz . Yine de bu birimselleştirilmiş değişkenleri yakalamaz. (Çünkü "başlatılmamış" bir bit için yer açmak için tip boyutlarını arttırmaz).

Bkz. Https://developers.redhat.com/blog/2014/10/16/gcc-undefined-behavior-sanitizer-ubsan/

Başlatılmamış verilerin kullanımını bulmak için clang / LLVM'de Adres Temizleyici ve Bellek Temizleyici bulunur. https://github.com/google/sanitizers/wiki/MemorySanitizer , clang -fsanitize=memory -fPIE -piebaşlatılmamış bellek okumalarını algılama örneklerini gösterir . Optimizasyon olmadan derlerseniz en iyi sonucu verebilir , bu nedenle değişkenlerin tüm okumaları asm'deki bellekten yüklenir. -O2Yükün optimize edilmeyeceği bir durumda kullanıldığını gösterirler . Ben kendim denemedim. (Bazı durumlarda, örneğin bir diziyi toplamadan önce bir akümülatör başlatma değil, clang -O3, hiç başlatılmadığı bir vektör kaydına toplanan bir kod yayar. . Fakat-fsanitize=memory oluşturulan grubu değiştirir ve bunun için bir kontrolle sonuçlanabilir.)

Başlatılmamış belleğin kopyalanmasını ve bununla birlikte basit mantık ve aritmetik işlemleri tolere edecektir. Genel olarak, MemorySanitizer başlatılmamış verilerin bellekteki yayılmasını sessizce izler ve başlatılmamış bir değere bağlı olarak bir kod dalı alındığında (veya alınmadığında) bir uyarı bildirir.

MemorySanitizer, Valgrind'de (Memcheck aracı) bulunan bir işlev alt kümesi uygular.

Çağrı glibc için çünkü bu durum için çalışması gerektiğini memcpybir ile lengthbaşlatılmamış bellekten hesaplanan bir dalda sonucu (kütüphane içinde) dayalı olacaktır length. Az önce kullanılan cmov, dizine alma ve iki mağaza kullanan tamamen dalsız bir sürümü satır içine alsaydı, işe yaramamış olabilir.

Valgrind'smemcheck de bu tür bir sorunu arayacak, yine programın başlatılmamış verilerin etrafına kopyalanıp kopyalanmadığından şikayet etmeyecek. Ancak, başlatılmamış verilere bağlı olarak dışarıdan görünen herhangi bir davranışı yakalamaya çalışmak için "Koşullu atlama veya hareketin başlatılmamış değerlere bağlı olduğunu" algılayacağını söylüyor.

Belki de sadece bir yükü işaretlememenin arkasındaki fikir, yapıların dolgusu olabileceğidir ve tüm yapıyı (dolgu dahil) geniş bir vektör yükü / deposu ile kopyalamak, tek tek üyeler bir seferde sadece bir tane yazılsa bile bir hata değildir. Asm seviyesinde, neyin dolgu olduğu ve değerin gerçekte neyin bir parçası olduğu hakkındaki bilgiler kaybolmuştur.


2
Değişkenin 8 bit tam sayı aralığında değil, sadece tüm CPU kaydının bir değerini aldığı daha kötü bir durum gördüm. Ve Itanium'un daha da kötüsü var, başlatılmamış bir değişkenin kullanımı açıkça çökebilir.
Joshua

2
@Joshua: oh doğru, iyi bir nokta, Itanium'un açık spekülasyonu, kayıt değerlerini "sayı değil" eşdeğeriyle etiketleyecek, böylece değer hataları kullanacak.
Peter Cordes

11
Dahası, bu aynı zamanda UB özellik türünün neden C ve C ++ dillerinin tasarımında ilk olarak tanıtıldığını gösterir: çünkü derleyiciye tam olarak bu tür bir özgürlük verir, bu da şimdi en modern derleyicilerin bu yüksek kaliteyi gerçekleştirmesine izin vermiştir. C / C ++ 'ı bu kadar yüksek performanslı orta seviye diller yapan optimizasyonlar.
The_Sympathizer

2
Ve böylece yararlı programlar yazmaya çalışan C ++ derleyici yazarları ve C ++ programcıları arasındaki savaş devam ediyor. Bu soruyu cevaplamada tamamen kapsamlı olan bu cevap, statik analiz araçları satıcıları için ikna edici bir reklam kopyası olarak da kullanılabilir ...
davidbak

4
@The_Sympathizer: UB, uygulamaların müşterileri için en yararlı olacağı şekilde davranmasına izin vermek için dahil edildi . Tüm davranışların eşit derecede yararlı kabul edilmesi önerilmemiştir.
Supercat

56

Derleyicinin, bağımsız değişken olarak iletilen bir boolean değerinin geçerli bir boolean değeri (yani, başlatılmış veya dönüştürülmüş trueveya false) bir değer olduğunu varsayabilir . trueDeğeri tamsayı 1 ile aynı olmak zorunda değildir - çeşitli temsiller olabilir, gerçekten truevefalse - ama parametresi "geçerli gösterimi", uygulamanıza olan bu iki değerden, birinin geçerli bazı temsili olmalıdır tanımladı.

Bu nedenle bool, a başlatamazsanız veya farklı türde bir işaretçi ile üzerine yazmayı başarırsanız, derleyicinin varsayımları yanlış olur ve Tanımsız Davranış ortaya çıkar. Uyarıldınız:

50) Bool değerini, başlatılmamış bir otomatik nesnenin değerini incelemek gibi, bu Uluslararası Standart tarafından “tanımsız” olarak tanımlanan şekillerde kullanmak, değerin doğru ya da yanlış gibi davranmasına neden olabilir. (§6.9.1, Temel Türler paragraf 6'nın dipnotu)


11
" trueDeğerin tamsayı 1 ile aynı olması gerekmez" bir tür yanıltıcıdır. Elbette, gerçek bit kalıbı başka bir şey olabilir , ancak dolaylı olarak dönüştürüldüğünde / tanıtıldığında ( true/ dışında bir değer görmenin tek yolu false) trueher zaman 1ve falseher zaman olur0 . Tabii ki, böyle bir derleyici de bu derleyicinin kullanmaya çalıştığı hileyi kullanamazdı ( boolgerçek bit modelinin sadece 0veya olabileceği gerçeğini kullanarak 1), bu yüzden OP'nin problemiyle alakasız.
ShadowRanger

4
@ShadowRanger Nesne gösterimini her zaman doğrudan denetleyebilirsiniz.
TC

7
@shadowranger: Demek istediğim, uygulama sorumlu. trueBit deseninin geçerli temsillerini sınırlıyorsa 1, bu ayrıcalıktır. Başka bir temsil kümesini seçerse, gerçekten burada belirtilen optimizasyonu kullanamazdı. Belirli bir temsili seçerse, o zaman yapabilir. Yalnızca dahili olarak tutarlı olması gerekir. Sen edebilir bir temsilini incelemek boolbir bayt dizisi kopyalayarak; bu UB değil (ama uygulama tanımlı)
rici

3
Evet, derleyicileri optimize etmek (yani gerçek dünya C ++ uygulaması) bazen veya boolbir bit modeline bağlı olan kod yayar . Bellekten her okuduklarında (veya işlev argümanını tutan bir kayıtta) yeniden booleanize etmezler . Bu cevap bunu söylüyor. örnekler : gcc4.7 + optimize edebilir için dönen bir fonksiyonu olarak , ya da msvc optimize edebilir için . x86 en bir olup bitsel eğer öyleyse, ve test setleri bayraklar göre . 01boolreturn a||bor eax, ediboola&btest cl, dltest andcl=1dl=2cl&dl = 0
Peter Cordes

5
Tanımlanmamış davranışla ilgili nokta , derleyicinin bu konuda çok daha fazla sonuç çıkarmasına izin verilmesidir, örneğin, tam olarak başlatılmamış bir değere erişmeye yol açacak bir kod yolunun hiçbir zaman alınmadığını varsaymaktır; . Bu nedenle, sadece düşük seviye değerlerinin sıfır veya birden farklı olması olasılığı ile ilgili değildir.
Holger

52

İşlevin kendisi doğrudur, ancak test programınızda işlevi çağıran ifade, başlatılmamış bir değişkenin değerini kullanarak tanımlanmamış davranışa neden olur.

Hata, çağrı fonksiyonundadır ve kod inceleme veya çağrı fonksiyonunun statik analizi ile tespit edilebilir. Derleyici gezgin bağlantınızı kullanarak gcc 8.2 derleyicisi hatayı algılar. (Belki sorunu bulamadığı clang'a karşı bir hata raporu gönderebilirsiniz).

Tanımsız davranış , tanımlanmamış davranışı tetikleyen olaydan sonra birkaç satır çökmesini içeren herhangi bir şey olabileceği anlamına gelir .

NB. "Tanımlanmamış davranış _____'a neden olabilir mi?" her zaman "Evet" tir. Kelimenin tam anlamıyla tanımsız davranışın tanımı budur.


2
İlk fıkra doğru mu? Yalnızca başlatılmamış bir UB'yi kopyalamakbool?
Joshua Green

10
@JoshuaGreen bkz. [Dcl.init] / 12 "Bir değerlendirme tarafından belirsiz bir değer üretilirse, aşağıdaki durumlar dışında davranış tanımsızdır:" (ve bu durumların hiçbirinin bir istisnası yoktur bool). Kopyalama kaynağın değerlendirilmesini gerektirir
MM

8
@JoshuaGreen Bunun nedeni, bazı türler için bazı geçersiz değerlere erişirseniz, bir donanım hatasını tetikleyen bir platformunuz olabilir. Bunlara bazen "tuzak temsili" denir.
David Schwartz

7
Itanium, belirsiz olsa da, hala üretimde olan, tuzak değerlerine sahip ve en az iki yarı modern C ++ derleyicisine (Intel / HP) sahip bir CPU'dur. Tam anlamıyla sahiptir true, falseve not-a-thingboolelerde değerleri.
MSalters

3
Kapak tarafında, "Standart tüm derleyicilerin bir şeyi belirli bir şekilde işlemesini gerektirir mi?" Cevabı genellikle "hayır" dır, özellikle / herhangi bir kaliteli derleyicinin bunu yapması gerektiği açık durumlarda bile; daha açık bir şey varsa, Standardın yazarlarının bunu söylemesine daha az ihtiyaç duyulmalıdır.
supercat

23

Bir bool yalnızca dahili olarak kullanılan uygulama bağımlı değerleri tutmak için izin verilir trueve falseve oluşturulan kod sadece bu iki değerden birini yapacak varsayabiliriz.

Tipik olarak, uygulama tamsayı kullanır 0için falseve 1için truearasında dönüşüm kolaylaştırmak için, boolve intve yapmak if (boolvar)aynı kodu üretmek if (intvar). Bu durumda, ödevdeki üçlü için oluşturulan kodun, değeri iki dizeye işaretçi dizisine dizin olarak kullanacağını düşünebiliriz, yani şöyle bir şeye dönüştürülebilir:

// the compile could make asm that "looks" like this, from your source
const static char *strings[] = {"false", "true"};
const char *whichString = strings[boolValue];

Eğer boolValuebaşlatılmamış olması, aslında bunun sebebi sınırları dışında erişen herhangi bir tamsayı değeri, tutunabileceği stringsdizisi.


1
@SidS Teşekkürler. Teorik olarak, iç temsiller, tamsayılara / tamsayılardan nasıl davrandıklarının tersi olabilir, ancak bu sapkın olacaktır.
Barmar

1
Haklısınız ve örneğiniz de çökecek. Ancak, başlatılmamış bir değişkeni bir dizinin dizini olarak kullandığınız bir kod incelemesinde "görünür" olur. Ayrıca, hata ayıklamada bile çökebilir (örneğin, bazı hata ayıklayıcı / derleyici, çökme zamanını görmeyi kolaylaştırmak için belirli desenlerle başlatılır). Örneğimde, şaşırtıcı olan kısım, bool kullanımının görünmez olmasıdır: Optimizer, kaynak kodunda bulunmayan bir hesaplamada kullanmaya karar verdi.
Remz

3
@Remz Ben sadece oluşturulan kod eşdeğeri ne göstermek için dizi kullanarak, aslında herkes bunu yazmak öneririz değil.
Barmar

1
@Remz için Değişiklik booliçin intbirlikte *(int *)&boolValueve ayıklama amacıyla yazdırabilirsiniz o başka bir şey olup olmadığını görmek 0veya 1ne zaman çöker. Durum buysa, derleyicinin inline-if'i neden çöktüğünü açıklayan bir dizi olarak optimize ettiği teorisini doğrular.
Havenard

2
@ MSalters: std::bitset<8>Farklı bayraklarım için bana güzel isimler vermiyor. Ne olduklarına bağlı olarak, bu önemli olabilir.
Martin Bonner, Monica

15

Sorunuzu çok özetlersek, soruyorsunuz C ++ standardı, bir derleyicinin boolyalnızca dahili sayısal temsili '0' veya '1' olduğunu varsayabilir ve bunu bu şekilde kullanabilir mi?

Standart bir şirketin iç temsili hakkında hiçbir şey söylemiyor bool . Yalnızca a'ya boolbir intveya (veya tersi) uygulandığında ne olacağını tanımlar . Çoğunlukla, bu ayrılmaz dönüşümler (ve insanların kendilerine oldukça fazla güvenmesi) nedeniyle, derleyici 0 ve 1'i kullanacaktır, ancak bunu yapmak zorunda değildir (kullandığı daha düşük seviyeli ABI'nin kısıtlamalarına uymak zorunda olsa da) ).

Yani, derleyici, a'yı gördüğünde bool, bahsedilenin bool' true' veya ' false' bit desenlerini içerdiğini ve hissettiği her şeyi yaptığını düşünme hakkına sahiptir . Değerleri Yani eğer trueve false1, 0, sırasıyla derleyici gerçekten optimize girmeye bırakılırstrlen için 5 - <boolean value>. Başka eğlenceli davranışlar da mümkün!

Burada tekrar tekrar belirtildiği gibi, tanımsız davranış tanımsız sonuçlara sahiptir. Dahil olmak üzere, ancak bunlarla sınırlı değildir

  • Kodunuz beklediğiniz gibi çalışıyor
  • Kodunuz rastgele zamanlarda başarısız oluyor
  • Kodunuz hiç çalıştırılmıyor.

Tanımlanmamış davranışlar hakkında her programcının bilmesi gerekenleri görün

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.