Kesin takma adlandırma kuralı nedir?


804

C'de yaygın olarak tanımlanmamış davranışlar sorulduğunda , insanlar bazen katı takma adlandırma kuralına başvururlar.
Ne hakkında konuşuyorlar?


12
@Ben Voigt: Diğer adlandırma kuralları c ++ ve c için farklıdır. Bu soru neden cve ile etiketlendi c++faq?
MikeMB

6
@MikeMB: Geçmişi kontrol ederseniz, diğer bazı uzmanların soruyu mevcut yanıtlar altında değiştirme girişimine rağmen, etiketleri asıl oldukları gibi tuttuğumu göreceksiniz. Ayrıca, dile bağımlılık ve sürüm bağımlılığı, "Katı takma kural nedir?" Cevabının çok önemli bir parçasıdır. ve farklılıkları bilmek, C ve C ++ arasında kod geçiren veya her ikisinde kullanmak için makro yazan ekipler için önemlidir.
Ben Voigt

6
@Ben Voigt: Aslında - anlayabildiğim kadarıyla - çoğu cevap sadece c ++ ile değil, c ++ ile de ilgilidir, ayrıca sorunun ifadesi C-kurallarına odaklandığını gösterir (veya OP sadece farkında değildi, bir fark var ). Genelde kurallar ve genel Fikir elbette aynıdır, ancak özellikle sendikalar söz konusu olduğunda, cevaplar c ++ için geçerli değildir. Biraz endişeliyim, bazı c ++ programcıları sıkı takma kuralını arayacak ve burada belirtilen her şeyin c ++ için de geçerli olduğunu varsayacaktır.
MikeMB

Öte yandan, birçok iyi yanıt gönderildikten sonra sorunun değiştirilmesinin sorunlu olduğunu kabul ediyorum ve sorun yine de küçük bir sorudur.
MikeMB

1
@MikeMB: Sanırım C'nin C ++ için yanlış yapan kabul edilen cevaba odaklanmasının üçüncü taraflarca düzenlendiğini göreceksiniz. Bu bölüm muhtemelen yeniden gözden geçirilmelidir.
Ben Voigt

Yanıtlar:


562

Sıkı takma ad sorunlarıyla karşılaştığınız tipik bir durum, bir yapıyı (aygıt / ağ msj'si gibi) sisteminizin kelime boyutundaki bir arabellek üzerine yerleştirirken ( uint32_ts veya uint16_ts'ye bir işaretçi gibi ). Bir yapıyı böyle bir tamponun üzerine yerleştirdiğinizde veya böyle bir yapıyı işaretçi döküm yoluyla üzerine yerleştirdiğinizde katı takma kurallarını kolayca ihlal edebilirsiniz.

Bu tür bir kurulumda, bir şeye mesaj göndermek istersem, aynı bellek yığınına işaret eden iki uyumsuz işaretçiye sahip olmalıydım. Sonra naif (böyle bir sistemde sizeof(int) == 2) böyle bir şey kodlayabilirsiniz :

typedef struct Msg
{
    unsigned int a;
    unsigned int b;
} Msg;

void SendWord(uint32_t);

int main(void)
{
    // Get a 32-bit buffer from the system
    uint32_t* buff = malloc(sizeof(Msg));

    // Alias that buffer through message
    Msg* msg = (Msg*)(buff);

    // Send a bunch of messages    
    for (int i =0; i < 10; ++i)
    {
        msg->a = i;
        msg->b = i+1;
        SendWord(buff[0]);
        SendWord(buff[1]);   
    }
}

Katı takma adlandırma kuralı bu ayarı geçersiz kılar: C 2011 6.5 paragraf 7 1 tarafından izin verilen ve diğer türlerden biriyle uyumlu olmayan bir nesneyi takma bir işaretçiyi yeniden adlandırma , tanımsız bir davranıştır. Ne yazık ki, yine de bu şekilde kod yazabilir, belki bazı uyarılar alabilirsiniz, iyi derlenmesini sağlayın, sadece kodu çalıştırdığınızda garip beklenmedik davranışlara sahip olmak için.

(GCC, takma uyarılar verme yeteneğinde biraz tutarsız görünüyor, bazen bize dostça bir uyarı veriyor ve bazen vermiyor.)

Bu davranışın neden tanımlanmadığını görmek için, sıkı örtüşme kuralının derleyiciyi satın aldığını düşünmeliyiz. Temel olarak, bu kuralla, buffdöngünün her çalışmasının içeriğini yenilemek için talimatlar eklemeyi düşünmek zorunda değildir . Bunun yerine, optimizasyon yaparken, diğer adlandırma hakkında can sıkıcı bir şekilde güçlendirilmemiş varsayımlarla, bu talimatları atlayabilir , ve döngü çalıştırılmadan önce] CPU kayıtlarına yükleyebilir buff[0]ve buff[1döngü gövdesini hızlandırabilir. Sıkı takma ad kullanılmadan önce, derleyici içeriğinin buffherhangi bir zamanda herhangi bir yerden herkes tarafından değişebileceği bir paranoya durumunda yaşamak zorunda kaldı . Ekstra bir performans avantajı elde etmek ve çoğu insanın punto işaretçileri kullanmadığını varsayarsak, katı takma kural getirildi.

Unutmayın, örneğin uygun olduğunu düşünüyorsanız, gönderimi sizin için yapan başka bir işleve bir tampon geçirirseniz, bunun yerine, bu bile olabilir.

void SendMessage(uint32_t* buff, size_t size32)
{
    for (int i = 0; i < size32; ++i) 
    {
        SendWord(buff[i]);
    }
}

Ve bu kullanışlı fonksiyondan yararlanmak için önceki döngümüzü yeniden yazdık

for (int i = 0; i < 10; ++i)
{
    msg->a = i;
    msg->b = i+1;
    SendMessage(buff, 2);
}

Derleyici, satır içi SendMessage'ı denemek için yeterince akıllı olabilir veya olmayabilir ve buff'ı tekrar yüklemeye veya yüklememeye karar verebilir veya vermeyebilir. Eğer SendMessageayrı derlenmiş başka API parçasıdır, muhtemelen yük tutkunu içeriğine talimatlar bulunur. Sonra tekrar, belki C ++ ve bu sadece şablon derleyici satır içi olduğunu düşündüğü templated başlık uygulamasıdır. Ya da belki de kendi kolaylığınız için .c dosyanıza yazdığınız bir şeydir. Her neyse tanımlanmamış davranış yine de ortaya çıkabilir. Davlumbazın altında neler olduğunu bilsek bile, bu hala kuralın ihlalidir, bu nedenle iyi tanımlanmış bir davranış garanti edilmez. Yani sadece sınırlanmış tampon kelimemizi alan bir işlevi sararak yardımcı olmak zorunda değildir.

Peki bunun üstesinden nasıl gelebilirim?

  • Birlik kullanın. Çoğu derleyici bunu sıkı bir takma addan şikayet etmeden destekler. Buna C99'da izin verilir ve C11'de açıkça izin verilir.

    union {
        Msg msg;
        unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
    };
  • Derleyicinizdeki katı takma adı devre dışı bırakabilirsiniz ( f [no-] gcc cinsinden katı takma adlandırma )

  • char*Sisteminizin sözcüğü yerine takma ad için kullanabilirsiniz . Kurallar char*( signed charve dahil unsigned char) için bir istisnaya izin verir . Her zaman char*diğer türlerin takma adı olduğu varsayılır . Ancak bu başka şekilde çalışmaz: Yapınızın bir tampon karakterini taklit ettiği varsayımı yoktur.

Acemi dikkat edin

Bu, iki türü birbirine bindirirken potansiyel bir mayın tarlasıdır. Ayrıca , endianness , kelime hizalama ve yapıları doğru bir şekilde paketleme yoluyla hizalama sorunlarıyla nasıl başa çıkılacağını öğrenmelisiniz .

dipnot

1 C 2011 6.5 7'nin bir değer erişimine izin verdiği türler şunlardır:

  • nesnenin etkili türüyle uyumlu bir tür,
  • nesnenin etkili türüyle uyumlu bir türün nitelikli bir sürümü,
  • nesnenin etkili türüne karşılık gelen imzalı veya imzasız tür olan bir tür,
  • etkin nesne türünün nitelikli bir sürümüne karşılık gelen imzalı veya imzasız tür olan bir tür,
  • üyeleri arasında yukarıda belirtilen türlerden birini içeren bir toplu veya sendika türü (özyineli olarak, bir alt kümenin veya içerdiği birliğin bir üyesi dahil) veya
  • karakter türü.

16
Muhtemelen savaştan sonra geliyorum .. bunun yerine unsigned char*çok char*kullanılabilir mi? Baytlarım imzalanmadığı ve imzalanmış davranışın tuhaflığını istemediğim için altta yatan türden unsigned charziyade kullanma eğilimindeyim (özellikle taşması için wrt)charbyte
Matthieu M.

30
@Matthieu: İmza, takma ad kurallarında bir fark yaratmaz, bu nedenle kullanmak unsigned char *iyidir.
Thomas Eding

22
Bir sendika üyesinden en son yazılandan farklı okumak tanımlanmamış bir davranış değil mi?
R. Martinho Fernandes

23
Bollocks, bu cevap tamamen geri . Yasadışı olarak gösterdiği örnek aslında yasaldır ve yasal olarak gösterdiği örnek aslında yasadışıdır.
R. Martinho Fernandes

7
Sizin uint32_t* buff = malloc(sizeof(Msg));ve sonraki birleşim unsigned int asBuffer[sizeof(Msg)];arabellek bildirimlerinin farklı boyutları vardır ve ikisi de doğru değildir. mallocÇağrı o hata bana hiçbiri, seansları (bunu yapmayın) ve sendika olması gerekenden 4 kat daha büyük olacak ... Ben netlik için olduğunu biliyoruz, ancak kaputun altında 4 bayt hizalama ağırlık vermektedir daha az ...
nonsensickle

233

Bulduğum en iyi açıklama, Sıkı Takma Adı Anlamak Mike Acton tarafından . Biraz PS3 gelişimine odaklandı, ancak bu sadece GCC.

Makaleden:

"Katı kenar yumuşatma, C (veya C ++) derleyicisi tarafından yapılan ve farklı türdeki nesnelere yönelik kayıttan kaldırmanın asla aynı bellek konumuna (yani, diğer ad.) Karşılık gelmeyeceği bir varsayımdır."

Yani temelde bir int*içeren bir belleğe işaret ediyorsanız intve sonra float*o belleğe a işaret ederseniz ve bunu floatkuralı ihlal ettiğinizde kullanırsınız . Kodunuz buna uymuyorsa, derleyicinin optimize edici muhtemelen kodunuzu kıracaktır.

Kuralın istisnası char*, herhangi bir türü işaret etmesine izin verilen bir a'dır.


6
Peki aynı belleği 2 farklı değişkenle yasal olarak kullanmanın standart yolu nedir? ya da herkes sadece kopyalıyor mu?
jiggunjer

4
Mike Acton'un sayfası kusurlu. "Bir sendika (2) aracılığıyla döküm" bölümü, en azından düpedüz yanlıştır; yasal olduğunu iddia ettiği kod değildir.
davmac

11
@davmac: C89'un yazarları hiçbir zaman programcıları çemberlerden atlamaya zorlamamıştı. Optimizasyonun tek amacı için var olan bir kuralın, programcıların bir optimizatörün gereksiz kodu kaldıracağı ümidiyle verileri gereksiz bir şekilde kopyalayan kod yazmasını gerektirecek şekilde yorumlanması gerektiği fikrini tamamen tuhaf buluyorum.
supercat

1
@curiousguy: "Sendika olamaz" mı? İlk olarak, sendikaların asıl / birincil amacı hiçbir şekilde takma ad ile ilgili değildir. İkincisi, modern dil spesifikasyonu sendikaların takma ad olarak kullanılmasına açıkça izin vermektedir. Derleyicinin bir birliğin kullanıldığını ve durumu ele almanın özel bir yol olduğunu fark etmesi gerekir.
17'de AnT

5
@curiousguy: Yanlış. Birincisi, sendikaların arkasındaki orijinal kavramsal fikir, herhangi bir anda , verilen sendika nesnesinde sadece bir üye nesnenin "aktif" olduğu, diğerlerinin ise mevcut olmadığıydı. Yani, inandığınız gibi "aynı adreste farklı nesneler" yoktur. İkincisi, herkesin bahsettiği takma ad ihlalleri , sadece aynı adrese sahip iki nesneye sahip olmakla değil, bir nesneye farklı bir nesne olarak erişmekle ilgilidir . Yazık bir erişim olmadığı sürece sorun yoktur. Orijinal fikir buydu. Daha sonra, sendikalar aracılığıyla tip-çiftçiliğe izin verildi.
17'de AnT

133

Bu, C ++ 03 standardının 3.10 bölümünde bulunan katı takma adlandırma kuralıdır (diğer cevaplar iyi bir açıklama sağlar, ancak hiçbiri kuralın kendisini sağlamaz):

Bir program, bir nesnenin depolanan değerine aşağıdaki türlerden farklı bir değer üzerinden erişmeye çalışırsa, davranış tanımsızdır:

  • nesnenin dinamik türü,
  • nesnenin dinamik türünün cv nitelikli bir sürümü,
  • nesnenin dinamik türüne karşılık gelen imzalı veya imzasız tür olan bir tür,
  • nesnenin dinamik türünün cv nitelikli sürümüne karşılık gelen imzalı veya imzasız tür olan bir tür,
  • üyeleri arasında yukarıda belirtilen türlerden birini içeren bir toplu veya sendika türü (özyineli olarak, bir alt kümenin veya içerdiği birliğin bir üyesi dahil),
  • nesnenin dinamik türünün (muhtemelen cv-nitelikli) temel sınıf türü olan bir tür,
  • a charveya unsigned chartip.

C ++ 11 ve C ++ 14 ifadeleri (vurgulanan değişiklikler):

Bir program, bir nesnenin depolanan değerine aşağıdaki türlerden birinden farklı bir değerle erişmeye çalışırsa , davranış tanımsızdır:

  • nesnenin dinamik türü,
  • nesnenin dinamik türünün cv nitelikli bir sürümü,
  • nesnenin dinamik türüne (4.4'te tanımlandığı gibi) benzer bir tür,
  • nesnenin dinamik türüne karşılık gelen imzalı veya imzasız tür olan bir tür,
  • nesnenin dinamik türünün cv nitelikli sürümüne karşılık gelen imzalı veya imzasız tür olan bir tür,
  • elemanları veya statik olmayan veri üyeleri arasında yukarıda belirtilen türlerden birini içeren bir toplama veya birleştirme türü (bir alt kümenin veya içerdiği birliğin bir öğesi veya statik olmayan veri üyesi dahil ),
  • nesnenin dinamik türünün (muhtemelen cv-nitelikli) temel sınıf türü olan bir tür,
  • a charveya unsigned chartip.

İki değişiklikler küçük: glvalue yerine lvalue ve agrega / birlik halinde açıklığa kavuşturulması.

Üçüncü değişiklik daha güçlü bir garanti sağlar (güçlü takma kuralını gevşetir): Takma ad için artık güvenli olan benzer türlerin yeni konsepti .


Ayrıca C ifadesi (C99; ISO / IEC 9899: 1999 6.5 / 7; ISO / IEC 9899: 2011 §6.5 ¶7'de aynı ifadeler kullanılır):

Bir nesnenin depolanmış değerine yalnızca aşağıdaki 73) veya 88) türlerinden birine sahip bir değer değeri ifadesiyle erişilmesi gerekir :

  • nesnenin etkili türüyle uyumlu bir tür,
  • etkin nesne türüyle uyumlu bir türün kalitatif versiyonu,
  • nesnenin etkili türüne karşılık gelen imzalı veya imzasız tür olan bir tür,
  • etkin nesne türünün kalifiye sürümüne karşılık gelen imzalı veya imzasız tip,
  • üyeleri arasında yukarıda belirtilen türlerden birini içeren bir toplu veya sendika türü (özyineli olarak, bir alt kümenin veya içerdiği birliğin bir üyesi dahil) veya
  • karakter türü.

73) veya 88) Bu listenin amacı, bir nesnenin diğer adının örtüştüğü veya örtüştüğü koşulları belirtmektir.


7
Ben, insanlar burada sıkça yönlendirildiği için, bütünlük uğruna kendime C standardına referans eklememe izin verdim.
Kos


2
Bir yapı türü lvalue varsa, bir üyenin adresini alır ve bunu üye türüne işaretçi olarak kullanan bir işleve, üye türünün bir nesnesine (legal) erişim olarak kabul edilirse, veya yapı tipinde bir nesne (yasak)? Bir çok kod, bu tür yapılara erişmenin yasal olduğunu varsayar ve bence birçok insan bu tür eylemleri yasakladığı anlaşılan bir kuralla boğulur, ancak kesin kuralların ne olduğu belirsizdir. Ayrıca, sendikalar ve yapılar aynı şekilde ele alınır, ancak her biri için mantıklı kurallar farklı olmalıdır.
supercat

2
@supercat: Yapılar için kuralın ifadesi, gerçek erişim her zaman ilkel tiptir. Daha sonra, türler eşleştiğinden ilkel türe yapılan bir başvuru yoluyla erişim yasaldır ve içerdiği yapı türüne bir başvuru yoluyla erişim yasaldır, çünkü özellikle izin verilir.
Ben Voigt

2
@BenVoigt: Birlikler aracılığıyla erişim yapılmadığı sürece ortak ilk dizinin işe yaradığını düşünmüyorum. Bkz goo.gl/HGOyoK ne yaptığını gcc görmek için. Bir üye türünün bir değerine (sendika-üye-erişim operatörü kullanmayan) bir sendika türüne erişme yasal ise, wow(&u->s1,&u->s2)değiştirmek için bir işaretçi kullanıldığında bile yasal olması gerekir uve bu, takma kural kolaylaştırmak için tasarlanmıştır.
supercat

81

Not

Bu, "Sıkı Takma Adlandırma Kuralı nedir ve neden önemsiyoruz?" yazma-up.

Katı takma ad nedir?

C ve C ++ 'da örtüşme, hangi değer türlerine depolanmış değerlere erişmemize izin verildiğiyle ilgilidir. Hem C hem de C ++ 'da standart, hangi ifade türlerinin hangi türlere takma olmasına izin verildiğini belirtir. Derleyici ve optimize edicinin, diğer adlandırma kurallarına sıkı sıkıya uyduğumuzu, dolayısıyla katı aliasing kuralını kullandığımızı varsayalım . İzin verilmeyen bir türü kullanarak bir değere erişmeye çalışırsak, tanımlanmamış davranış ( UB ) olarak sınıflandırılır . Tanımlanmamış bir davranışa sahip olduğumuzda tüm bahisler kapanır, programımızın sonuçları artık güvenilir değildir.

Ne yazık ki katı takma ad ihlallerinde, sık sık beklediğimiz sonuçları elde edeceğiz ve yeni bir optimizasyon ile bir derleyicinin gelecekteki bir versiyonunun geçerli olduğunu düşündüğümüz kodu kırabilme olasılığını bırakıyoruz. Bu istenmeyen bir durumdur ve katı örtüşme kurallarını ve bunları ihlal etmekten nasıl kaçınacağınızı anlamak faydalı olacaktır.

Neden umursadığımız hakkında daha fazla bilgi edinmek için, katı takma adlandırma kurallarını ihlal ederken ortaya çıkan sorunları tartışacağız, kurnazlık yazarken kullanılan yaygın teknikler genellikle sıkı takma kurallarını ve nasıl doğru şekilde yazılacağını ihlal ettiğinden, kurnazlık yazın.

Ön örnekler

Bazı örneklere bakalım, o zaman standartların tam olarak ne söylediğinden bahsedebiliriz, bazı diğer örnekleri inceleyebilir ve daha sonra katı takma adlardan kaçınmayı ve kaçırdığımız ihlalleri nasıl yakalayacağımızı görebiliriz. Şaşırtıcı olmaması gereken bir örnek ( canlı örnek ):

int x = 10;
int *ip = &x;

std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";

Biz var * int belleğe işaret bir tarafından işgal int ve bu geçerli örtüşme olduğunu. Optimize edici, ip yoluyla atamaların x tarafından kullanılan değeri güncelleyebileceğini varsaymalıdır .

Sonraki örnek, tanımlanmamış davranışa neden olan diğer adı gösterir ( canlı örnek ):

int foo( float *f, int *i ) { 
    *i = 1;               
    *f = 0.f;            

   return *i;
}

int main() {
    int x = 0;

    std::cout << x << "\n";   // Expect 0
    x = foo(reinterpret_cast<float*>(&x), &x);
    std::cout << x << "\n";   // Expect 0?
}

Foo işlevinde bir int * ve bir float * alırız, bu örnekte foo diyoruz ve her iki parametreyi de bu örnekte bir int içeren aynı bellek konumuna işaret edecek şekilde ayarladık . Not reinterpret_cast onun şablon parametresi tarafından specificed türü varmış gibi ifade tedavi etmek derleyici anlatıyor. Bu durumda, & x ifadesine sanki tip float * varmış gibi davranmasını söylüyoruz . Biz safça ikinci sonucu bekleyebiliriz cout olmak 0 ancak optimizasyon ile kullanılarak etkin -o2 hem gcc, clang şu sonucu:

0
1

Bu beklenmeyebilir, ancak tanımlanmamış davranışlar başlattığımız için mükemmel bir şekilde geçerlidir. Bir kayan nokta , int nesnesini geçerli olarak takma adlandıramaz . Bu nedenle optimizasyon aracı , f üzerinden yapılan bir mağaza int nesnesini geçerli bir şekilde etkileyemediğinden , kayıttan çıkarma i i değeri olarak kaydedilen sabit 1'i kabul edebilir . Kodun Derleyici Gezgini'ne eklenmesi, bunun tam olarak ne olduğunu gösterir ( canlı örnek ):

foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1  
mov dword ptr [rdi], 0
mov eax, 1                       
ret

Tür Tabanlı Takma Ad Analizi (TBAA) kullanan optimize edici 1'in döndürüleceğini varsayar ve sabit değeri doğrudan dönüş değerini taşıyan eax kaydına taşır. TBAA, yükleri ve depoları optimize etmek için hangi türlerin diğer adlara izin verileceğine ilişkin dil kurallarını kullanır. Bu durumda TBAA bildiği bir şamandıra olamaz takma ve int ve yük uzakta optimize i .

Şimdi, Kural Kitabına

Standart tam olarak ne yapmamıza ve yapmamıza izin vermiyor diyor? Standart dil basit değildir, bu yüzden her bir öğe için anlamı gösteren kod örnekleri sağlamaya çalışacağım.

C11 standardı ne diyor?

C11 standart bölümünde aşağıdaki diyor 6.5 İfadeler paragraf 7 :

Bir nesnenin depolanmış değerine yalnızca aşağıdaki türlerden birine sahip bir değer değeri ifadesi ile erişilir: 88) - nesnenin etkili türüyle uyumlu bir tür,

int x = 1;
int *p = &x;   
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int

- nesnenin etkili türüyle uyumlu bir türün nitelikli bir versiyonu,

int x = 1;
const int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int

- nesnenin etkin tipine karşılık gelen imzalı veya imzasız tip,

int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to 
                     // the effective type of the object

gcc / clang bir uzantısı var ve ayrıca atama sağlayan işaretsiz int * için * int uyumlularsa türleri olmamalarına karşın.

- nesnenin etkin türünün nitelikli bir sürümüne karşılık gelen imzalı veya imzasız tür olan bir tür,

int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type 
                     // that corresponds with to a qualified verison of the effective type of the object

- üyeleri arasında yukarıda belirtilen türlerden birini içeren bir toplu veya sendika türü (özyineli olarak, bir alt kümenin veya içerdiği birliğin bir üyesi dahil) veya

struct foo {
  int x;
};

void foobar( struct foo *fp, int *ip );  // struct foo is an aggregate that includes int among its members so it can
                                         // can alias with *ip

foo f;
foobar( &f, &f.x );

- bir karakter türü.

int x = 65;
char *p = (char *)&x;
printf("%c\n", *p );  // *p gives us an lvalue expression of type char which is a character type.
                      // The results are not portable due to endianness issues.

C ++ 17 Taslak Standardı ne diyor

[Basic.lval] paragraf 11'deki C ++ 17 taslak standardı şöyle diyor:

Bir program, bir nesnenin depolanan değerine aşağıdaki türlerden birinden farklı bir değerle erişmeye çalışırsa, davranış tanımsızdır: 63 (11.1) - nesnenin dinamik türü,

void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0};        // Placement new changes the dynamic type of the object to int
std::cout << *ip << "\n";        // *ip gives us a glvalue expression of type int which matches the dynamic type 
                                  // of the allocated object

(11.2) - nesnenin dinamik türünün cv nitelikli bir sürümü,

int x = 1;
const int *cip = &x;
std::cout << *cip << "\n";  // *cip gives us a glvalue expression of type const int which is a cv-qualified 
                            // version of the dynamic type of x

(11.3) - nesnenin dinamik türüne (7.5'te tanımlandığı gibi) benzer bir tür,

(11.4) - nesnenin dinamik tipine karşılık gelen imzalı veya imzasız tip,

// Both si and ui are signed or unsigned types corresponding to each others dynamic types
// We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
signed int foo( signed int &si, unsigned int &ui ) {
  si = 1;
  ui = 2;

  return si;
}

(11.5) - nesnenin dinamik türünün cv onaylı sürümüne karşılık gelen imzalı veya imzasız tür olan bir türü,

signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing

(11.6) - elemanları veya statik olmayan veri üyeleri arasında yukarıda belirtilen türlerden birini içeren bir toplu veya birleşim türü (bir alt kümenin veya içerdiği birliğin bir öğesi veya statik olmayan veri üyesi dahil),

struct foo {
 int x;
};

// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
 fp.x = 1;
 ip = 2;

 return fp.x;
}

foo f; 
foobar( f, f.x ); 

(11.7) - nesnenin dinamik türünün (muhtemelen cv-nitelikli) temel sınıf türü olan bir tür,

struct foo { int x ; };

struct bar : public foo {};

int foobar( foo &f, bar &b ) {
  f.x = 1;
  b.x = 2;

  return f.x;
}

(11.8) - karakter, işaretsiz karakter veya std :: bayt türü.

int foo( std::byte &b, uint32_t &ui ) {
  b = static_cast<std::byte>('a');
  ui = 0xFFFFFFFF;                   

  return std::to_integer<int>( b );  // b gives us a glvalue expression of type std::byte which can alias
                                     // an object of type uint32_t
}

Worth noting Char imzalı yukarıdaki listede yer almayan, bu bir dikkat çekici fark C diyor bir karakter türü .

Punning Tipi Nedir

Bu noktaya geldik ve merak ediyor olabiliriz, neden takma ad isteyelim ki? Cevap genellikle pun yazmaktır , genellikle kullanılan yöntemler sıkı takma kurallarını ihlal eder.

Bazen yazı sistemini atlatmak ve bir nesneyi farklı bir yazı olarak yorumlamak istiyoruz. Bu, bir bellek parçasını başka bir tür olarak yeniden yorumlamak için tip kısırlaştırma olarak adlandırılır . Punning türü , görüntülemek, taşımak veya değiştirmek için bir nesnenin temel temsiline erişmek isteyen görevler için yararlıdır. Kullanılmakta olan punning türünün tipik alanları derleyiciler, serileştirme, ağ kodu vb.

Geleneksel olarak bu, nesnenin adresini alarak, onu yeniden yorumlamak istediğimiz türdeki bir göstergeye çevirerek ve sonra değere erişerek veya başka bir deyişle diğer adla gerçekleştirerek başarılmıştır. Örneğin:

int x =  1 ;

// In C
float *fp = (float*)&x ;  // Not a valid aliasing

// In C++
float *fp = reinterpret_cast<float*>(&x) ;  // Not a valid aliasing

printf( "%f\n", *fp ) ;

Daha önce gördüğümüz gibi, bu geçerli bir takma ad değildir, bu nedenle tanımlanmamış davranışları çağırıyoruz. Ancak geleneksel olarak derleyiciler katı takma kurallarından yararlanmadı ve bu tür kodlar genellikle yeni çalıştı, geliştiriciler maalesef bir şeyleri bu şekilde yapmaya alışkınlar. Tip punning için yaygın bir alternatif yöntem, C için geçerli olan ancak C ++ 'da tanımlanmamış davranış olan sendikalardır ( canlı örneğe bakın ):

union u1
{
  int n;
  float f;
} ;

union u1 u;
u.f = 1.0f;

printf( "%d\n”, u.n );  // UB in C++ n is not the active member

Bu C ++ için geçerli değildir ve bazıları sendikaların amacının yalnızca varyant türlerini uygulamak olduğunu düşünmek ve sendika türünü kullanmak için sendikaları kullanmak bir istismardır.

Pun'u nasıl doğru yazabiliriz?

Hem C hem de C ++ 'da punning tipi için standart yöntem memcpy'dir . Bu biraz ağır görünebilir, ancak optimize edici tip punning için memcpy kullanımını tanımalı ve onu optimize etmeli ve hareketi kaydetmek için bir kayıt oluşturmalıdır. Örneğin , int64_t öğesinin double ile aynı boyutta olduğunu biliyorsanız :

static_assert( sizeof( double ) == sizeof( int64_t ) );  // C++17 does not require a message

memcpy kullanabiliriz :

void func1( double d ) {
  std::int64_t n;
  std::memcpy(&n, &d, sizeof d); 
  //...

Yeterli bir optimizasyon seviyesinde, herhangi bir iyi modern derleyici, daha önce bahsedilen reinterpret_cast yöntemine veya tip çiftliği için birleştirme yöntemine özdeş kod üretir . Oluşturulan kod incelendiğinde sadece mov ( live Compiler Explorer Örnek ) 'i kullandığını görüyoruz .

C ++ 20 ve bit_cast

C ++ 20'de , bir punta bağlamında kullanılabilir olmanın yanı sıra, pun-punto için basit ve güvenli bir yol sağlayan bit_cast ( uygulama teklif bağlantısında mevcuttur ) kazanabiliriz.

Aşağıda, float için imzasız bir int yazmak için bit_cast'in nasıl kullanılacağına bir örnek verilmiştir ( canlı olarak görün ).

std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)

Durumda etmek ve Dan tipleri aynı boyutu yoktur, bu bir ara struct15 kullanmamızı gerektiriyor. Biz içeren bir yapı kullanır sizeof (işaretsiz int) karakter dizisi ( varsayar 4 bayt işaretsiz int olmak) Gönderen tipi ve imzasız int olarak To yazın .:

struct uint_chars {
 unsigned char arr[sizeof( unsigned int )] = {} ;  // Assume sizeof( unsigned int ) == 4
};

// Assume len is a multiple of 4 
int bar( unsigned char *p, size_t len ) {
 int result = 0;

 for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
   uint_chars f;
   std::memcpy( f.arr, &p[index], sizeof(unsigned int));
   unsigned int result = bit_cast<unsigned int>(f);

   result += foo( result );
 }

 return result ;
}

Bu ara tipe ihtiyacımız olması talihsiz bir durum ama bit_cast'in şu anki kısıtlaması bu .

Sıkı Takma Ad İhlalleri Yakalamak

C ++ 'da sıkı takma yakalamak için çok iyi bir aracımız yok, sahip olduğumuz araçlar bazı sıkı takma ihlallerini ve bazı yanlış hizalanmış yük ve mağaza vakalarını yakalayacak.

gcc bayrağı -fstrict-aliasing ve -Wstrict-aliasing kullanarak yanlış pozitif / negatif olmasa da bazı durumları yakalayabilir. Örneğin, aşağıdaki durumlar gcc cinsinden bir uyarı oluşturur ( canlı yayına bakın ):

int a = 1;
short j;
float f = 1.f; // Originally not initialized but tis-kernel caught 
               // it was being accessed w/ an indeterminate value below

printf("%i\n", j = *(reinterpret_cast<short*>(&a)));
printf("%i\n", j = *(reinterpret_cast<int*>(&f)));

bu ek durumu yakalamayacak olmasına rağmen ( canlı olarak görün ):

int *p;

p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));

Clang bu bayraklara izin verse de görünüşe göre aslında uyarıları uygulamıyor.

Elimizdeki diğer bir araç, yanlış hizalanmış yükleri ve depoları yakalayabilen ASan'dır. Bunlar doğrudan katı takma ad ihlalleri olmamasına rağmen, sıkı takma ad ihlallerinin ortak bir sonucudur. Örneğin, aşağıdaki durumlar -fsanitize = adres kullanılarak clang ile oluşturulduğunda çalışma zamanı hataları oluşturur

int *x = new int[2];               // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6);     // regardless of alignment of x this will not be an aligned address
*u = 1;                            // Access to range [6-9]
printf( "%d\n", *u );              // Access to range [6-9]

Tavsiye edeceğim son araç C ++ 'a özgüdür ve kesinlikle bir araç değil, bir kodlama uygulamasıdır, C stili dökümlere izin vermeyin. Hem gcc hem de clang, -Wold tarzı döküm kullanarak C stili dökümler için bir tanı oluşturacaktır . Bu, tanımlanamayan tipteki puntoları reinterpret_cast kullanmaya zorlar, genel olarak reinterpret_cast daha yakın kod incelemesi için bir bayrak olmalıdır. Denetim gerçekleştirmek için kod tabanınızda reinterpret_cast aramak da daha kolaydır.

C için zaten tüm araçlarımız var ve aynı zamanda bir programı C dilinin büyük bir alt kümesi için kapsamlı bir şekilde analiz eden statik bir analizör olan tis-tercümanımız var. -Fstrict-aliasing kullanmanın bir vakayı kaçırdığı önceki örneğin C sürümleri verildi ( canlı olarak görün )

int a = 1;
short j;
float f = 1.0 ;

printf("%i\n", j = *((short*)&a));
printf("%i\n", j = *((int*)&f));

int *p; 

p=&a;
printf("%i\n", j = *((short*)p));

tis-interpeter üçünü de yakalayabilir, aşağıdaki örnek tis-kernal'i tis-interpreter olarak çağırır (çıktı kısalık için düzenlenmiştir):

./bin/tis-kernel -sa example1.c 
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
              rules by accessing a cell with effective type int.
...

example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
              accessing a cell with effective type float.
              Callstack: main
...

example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
              accessing a cell with effective type int.

Son olarak şu anda geliştirilmekte olan TySan var . Bu dezenfektan, gölge bellek segmentine tip kontrolü bilgileri ekler ve diğer adlandırma kurallarını ihlal edip etmediklerini görmek için erişimleri kontrol eder. Araç potansiyel olarak tüm takma ad ihlallerini yakalayabilmelidir, ancak büyük bir çalışma zamanı yükü olabilir.


Yorumlar uzun tartışmalar için değildir; bu sohbet sohbete taşındı .
Bhargav Rao

3
+10, iyi yazılmış ve açıklanmış olsaydım, her iki taraftan da derleyici yazarları ve programcıları ... tek eleştiri: Standartın neyin yasak olduğunu görmek için yukarıdaki karşı örneklere sahip olmak güzel olurdu, bu açık değil tür :-)
Gabriel

2
Çok iyi bir cevap. Sadece ilk örneklerin C ++ 'da verildiğine üzülüyorum, bu da benim gibi sadece C'yi bilen veya önemseyen ve ne reinterpret_castyapabileceğini veya ne anlama gelebileceğini bilmeyen insanları takip etmeyi zorlaştırıyor cout. (C ++ 'dan bahsetmek doğru, ancak asıl soru C ve IIUC hakkındaydı, bu örnekler C'de de geçerli bir şekilde yazılabilir)
Gro-Tsen

Tip puning ile ilgili olarak: eğer dosyaya bir tür X türü bir dizi yazarsam, o dosyadan bu diziyi void * ile işaretlenmiş belleğe okursam, o zaman kullanmak için bu işaretçiyi verilerin gerçek tipine koyarım - bu tanımlanmamış davranış?
Michael IV

44

Sıkı takma ad sadece işaretçileri değil, referansları da etkiler, destek geliştirici wiki için bu konuda bir makale yazdım ve o kadar iyi karşılandı ki danışmanlık web sitemdeki bir sayfaya dönüştürdüm. Ne olduğunu, insanları neden bu kadar şaşırttığını ve bu konuda ne yapacağını tamamen açıklıyor. Sıkı Takma Beyaz Kağıt . Özellikle sendikaların neden C ++ için riskli davranışlar olduğunu ve neden memcpy kullanmanın hem C hem de C ++ için taşınabilir tek düzeltme olduğunu açıklar. Umarım bu yardımcı olur.


3
" Katı takma ad sadece işaretçileri değil, referansları da etkiler " Aslında değerlere atıfta bulunur . " Memcpy kullanarak taşınabilir tek düzeltme " Duymak!
curiousguy

5
İyi kağıt. Benim almam: (1) bu takma ad 'sorun' kötü programlamaya karşı aşırı bir tepki - kötü programcıyı kötü alışkanlıklarından korumaya çalışıyor. Programcı iyi alışkanlıklara sahipse, bu takma ad sadece bir sıkıntıdır ve kontroller güvenli bir şekilde kapatılabilir. (2) Derleyici tarafı optimizasyonu yalnızca iyi bilinen durumlarda yapılmalı ve şüpheye düşüldüğünde kaynak koduna kesinlikle uyulmalıdır; programcıyı derleyicinin kendine özgü ifadelerine hitap etmek için kod yazmaya zorlamak yanlıştır. Daha da kötüsü standardın bir parçası.
slashmais

4
@slashmais (1) " kötü programlamaya karşı aşırı tepki " Saçmalık. Kötü alışkanlıkların reddi. Onu yap? Fiyatı ödersiniz: sizin için garanti yok! (2) İyi bilinen vakalar? Hangileri? Katı takma kural "iyi bilinen" olmalıdır!
curiousguy

5
@curiousguy: Birkaç kafa karışıklığı giderdikten sonra, örtüşme kurallarına sahip C dilinin programların tip agnostik bellek havuzları uygulamasını imkansız kıldığı açıktır. Bazı program türleri malloc / free ile elde edilebilir, ancak diğerleri eldeki görevlere daha iyi uyarlanmış bellek yönetimi mantığına ihtiyaç duyar. Acaba C89 gerekçesi, takma kuralın nedeninin böyle crummy bir örneğini kullandığını merak ediyorum, çünkü örnekleri, kuralın herhangi bir makul görevi yerine getirmede büyük bir zorluk yaratmayacak gibi görünüyor.
supercat

5
@curiousguy, derleyici paketlerinin çoğu -O3'te varsayılan olarak -fstrict-aliasing dahil ve bu gizli sözleşme, TBAA'yı hiç duymamış ve bir sistem programcının nasıl olabileceği gibi kod yazmış olan kullanıcılara zorlanmıştır. Sistem programcılarına farklı gelmek istemiyorum, ancak bu tür bir optimizasyon varsayılan -O3 tercihinin dışında bırakılmalı ve TBAA'nın ne olduğunu bilenler için bir optimizasyon optimizasyonu olmalıdır. TBAA'yı ihlal eden kullanıcı kodu olduğu, özellikle de kullanıcı kodundaki kaynak seviyesi ihlali izlenen derleyici 'bug'e bakmak eğlenceli değil.
kchoi

34

Doug T.'in yazdıklarına ek olarak, işte muhtemelen gcc ile tetikleyen basit bir test örneği:

check.c

#include <stdio.h>

void check(short *h,long *k)
{
    *h=5;
    *k=6;
    if (*h == 5)
        printf("strict aliasing problem\n");
}

int main(void)
{
    long      k[1];
    check((short *)k,k);
    return 0;
}

İle derleyin gcc -O2 -o check check.c. Genellikle (denediğim çoğu gcc sürümünde) bu "sıkı örtüşme sorunu" üretir, çünkü derleyici "h" işlevinin "kontrol" işlevindeki "k" ile aynı adres olamayacağını varsayar. Bu nedenle derleyici if (*h == 5)uzak optimize ve her zaman printf çağırır.

Burada ilgilenenler için x64 için ubuntu 12.04.2 üzerinde çalışan, gcc 4.6.3 tarafından üretilen x64 montajcı kodu:

movw    $5, (%rdi)
movq    $6, (%rsi)
movl    $.LC0, %edi
jmp puts

Yani if ​​koşulu, birleştirici kodundan tamamen gider.


Eğer kontrol etmek için ikinci bir kısa * j eklerseniz () ve onu kullanırsanız (* j = 7), eğer g ve j aslında aynı değere işaret etmiyorsa, ggc bunu yapmaz. evet optimizasyon gerçekten akıllı.
philippe lhardy

2
İşleri daha eğlenceli hale getirmek için, uyumlu olmayan ancak aynı boyut ve temsile sahip türlere işaretçiler kullanın (örn. long long*Ve int64_t* için geçerli olan bazı sistemlerde ). Bir aklı başında derleyici, a'nın aynı şekilde depolandıklarında a long long*ve int64_t*aynı depoya erişebileceğini fark etmeleri beklenebilir , ancak bu tür bir tedavi artık moda değildir.
17'de supercat

Grr ... x64 bir Microsoft sözleşmesidir. Bunun yerine amd64 veya x86_64 kullanın.
SS Anne

Grr ... x64 bir Microsoft sözleşmesidir. Bunun yerine amd64 veya x86_64 kullanın.
SS Anne

17

İşaretçi kalıpları (bir birleşim kullanmanın aksine) ile yazım kısırlığı , sıkı bir örtüşme kırmanın önemli bir örneğidir.


1
Benim Bkz özellikle dipnotlar alakalı sözler için buraya cevabı ancak kötü ilk başta ifadeli rağmen sendikalar aracılığıyla cinaslı türü her zaman C izin edilmiştir. Sen benim cevabını açıklığa kavuşturmak istiyorum.
Shafik Yaghmour

@ShafikYaghmour: C89, uygulayıcıların sendikalar aracılığıyla kurnazlık tipini kullanacakları veya kullanamayacakları durumları seçmelerine açıkça izin verdi. Örneğin bir uygulama, bir programa yazma ve ardından başka bir okumanın, yazım ve okuma arasında aşağıdakilerden herhangi birini yapması durumunda, başka bir türün okunması için, tür yazım olarak tanınacağını belirtebilir : (1) aşağıdakileri içeren bir değeri değerlendirir sendika türü [dizinin doğru noktasında yapılırsa bir üyenin adresini almak yeterlidir]; (2) bir işaretçiyi bir türe diğerine bir işaretçiye dönüştürün ve bu ptr ile erişin.
17'de supercat

@ShafikYaghmour: Bir uygulama ayrıca, örneğin tamsayı ve kayan nokta değerleri arasındaki kurnazlık türünün yalnızca kod fpsync()fp olarak yazma ile int veya tersi okuma arasında bir yönerge yürüttüğünde [ayrı tamsayı ve FPU boru hatları ve önbellekleri olan uygulamalarda) güvenilir bir şekilde çalışacağını belirtebilir. , böyle bir yönerge pahalı olabilir, ancak derleyicinin her sendika erişiminde böyle bir senkronizasyon gerçekleştirmesi kadar maliyetli olmayabilir]. Veya bir uygulama, Ortak Başlangıç ​​Dizilerini kullanan durumlar dışında, elde edilen değerin hiçbir zaman kullanılamayacağını belirtebilir.
17'de supercat

@ShafikYaghmour: Altında C89, uygulamalar olabilir çoğu tip cinaslı biçimlerini, sendikalar aracılığıyla dahil, ancak tip cinaslı vermedi uygulamalarda izin verildi ima üyelerine sendikalar ve işaretçiler işaretçileri arasında denklik korusun açıkça yasaklıyorum.
supercat

17

C89 gerekçesine göre, Standardın yazarları derleyicilere aşağıdaki gibi kod verilmesini istemediler:

int x;
int test(double *p)
{
  x=5;
  *p = 1.0;
  return x;
}

işaret xetme olasılığına izin vermek için atama ve döndürme ifadesi arasındaki değerin yeniden yüklenmesi ve atamanın sonuç olarak değerini değiştirebilmesi gerekir . Bir derleyicinin , yukarıdaki gibi durumlarda örtüşme olmayacağını varsayması gerektiği fikri tartışmalıdır.px*px

Ne yazık ki, C89'un yazarları kurallarını, kelimenin tam anlamıyla okunması durumunda, aşağıdaki fonksiyonun bile Tanımlanmamış Davranışı çağıracak şekilde yazdılar:

void test(void)
{
  struct S {int x;} s;
  s.x = 1;
}

Bu tür bir lvalue kullandığı için interişim tip bir nesne struct Sve intbir erişim kullanılabilir türleri arasında değil struct S. Karakter tipi olmayan yapı ve birlik üyelerinin tüm kullanımlarını Tanımsız Davranış olarak ele almak saçma olacağı için, hemen hemen herkes, başka türde bir nesneye erişmek için bir tür değerin kullanılabileceği en azından bazı koşulların olduğunu kabul eder. . Ne yazık ki, C Standartları Komitesi bu koşulların ne olduğunu tanımlayamadı.

Sorunun çoğu, aşağıdaki gibi bir programın davranışını soran Hata Raporu # 028'in bir sonucudur:

int test(int *ip, double *dp)
{
  *ip = 1;
  *dp = 1.23;
  return *ip;
}
int test2(void)
{
  union U { int i; double d; } u;
  return test(&u.i, &u.d);
}

Kusur Raporu # 28, programın "double" türünde bir birleşim üyesi yazma ve "int" türünden birini okuma eylemi, Uygulama Tanımlı davranışı çağırdığı için Tanımsız Davranışı çağırdığını belirtir. Böyle bir akıl mantıksızdır, ancak orijinal soruna yönelik hiçbir şey yapmazken dili gereksiz yere zorlaştıran Etkili Tip kurallarının temelini oluşturur.

Orijinal sorunu çözmenin en iyi yolu, muhtemelen kuralın amacı hakkındaki dipnotu normatifmiş gibi ele almak ve takma adlar kullanarak çakışan erişim içeren durumlar dışında kuralı uygulanamaz hale getirmektir. Gibi bir şey verildi:

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   s.x = 1;
   p = &s.x;
   inc_int(p);
   return s.x;
 }

İçinde herhangi bir çakışma yoktur, inc_intçünkü erişilen depolama alanına tüm erişimler *pbir tür değerle yapılır intve hiçbir çelişki yoktur, testçünkü pa'dan bir struct Ssonraki kullanımda ve bir dahaki sefere skullanıldığında, o depolamaya yapılan tüm erişimler içinden pzaten olacak.

Kod biraz değiştirildiyse ...

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   p = &s.x;
   s.x = 1;  //  !!*!!
   *p += 1;
   return s.x;
 }

Burada, işaretli hatta perişim arasında bir örtüşme çakışması vardır, s.xçünkü yürütme sırasında aynı depolama alanına erişmek için kullanılacak başka bir başvuru vardır .

Kusur Raporu 028, orijinal örneğin iki göstergenin oluşturulması ve kullanılması arasındaki çakışma nedeniyle UB'yi başlattığını söylemişti, bu da "Etkili Türler" veya başka bir karmaşıklık eklemek zorunda kalmadan işleri çok daha açık hale getirecektir.


Açıkçası, az çok karmaşıklık getirmeden hedeflerine ulaşan az çok "standartlar komitesinin yapabilecekleri" türden bir teklif okumak ilginç olurdu.
jrh

1
@jrh: Bence oldukça basit olurdu. 1. Bir işlev veya döngünün belirli bir yürütülmesi sırasında takma adın oluşması için, bu yürütme sırasında çakışan fasonda aynı depolamayı ele almak için iki farklı işaretçi veya değer kullanılması gerekir ; 2. Bir işaretçi veya lvalue'nun bir diğerinden yeni bir şekilde görsel olarak türetildiği bağlamlarda, ikinciye erişimin birinciye erişim olduğunu kabul edin; 3. Gerçekte takma ad içermeyen durumlarda kuralın uygulanması amaçlanmadığını kabul edin.
supercat

1
Bir derleyicinin yeni türetilmiş bir değeri tanıdığı kesin koşullar Uygulama Kalitesi sorunu olabilir, ancak uzaktan uygun herhangi bir derleyici gcc ve clang'ın kasıtlı olarak yoksaydığı formları tanıyabilmelidir.
supercat

11

Cevapların çoğunu okuduktan sonra bir şeyler eklemeye ihtiyacım var:

Sıkı takma adlandırma (birazdan açıklayacağım) önemlidir çünkü :

  1. Bellek erişimi pahalı olabilir (performans açısından), bu nedenle veriler fiziksel belleğe yazılmadan önce CPU kayıtlarında işlenir.

  2. İki farklı CPU kaydındaki veriler aynı bellek alanına yazılacaksa , C olarak kodladığımızda hangi verilerin "hayatta kalacağını" tahmin edemeyiz.

    CPU kayıtlarının yüklenmesini ve boşaltılmasını manuel olarak kodladığımız montajda, hangi verilerin bozulmadan kaldığını bileceğiz. Ancak C (şükür ki) bu ayrıntıyı soyutlamaktadır.

İki işaretçi bellekte aynı yeri gösterebileceğinden, bu, olası çarpışmaları işleyen karmaşık kodlarla sonuçlanabilir .

Bu ekstra kod yavaştır ve hem yavaş hem de (muhtemelen) gereksiz ekstra bellek okuma / yazma işlemleri gerçekleştirdiğinden performansı düşürür.

Sıkı örtüşme kuralı bize gereksiz makine kodunu önlemek için izin verir o hangi durumlarda olmalıdır (ayrıca bkz İki işaretçileri aynı bellek bloğu işaret olmadığını varsaymak güvenli restrictanahtar kelime).

Katı kenar yumuşatma, farklı türlere yönelik işaretçilerin bellekte farklı konumları işaret ettiğini varsaymanın güvenli olduğunu belirtir.

Bir derleyici, iki işaretçinin farklı türlere (örneğin, a int *ve a float *) işaret ettiğini fark ederse, bellek adresinin farklı olduğunu varsayar ve bellek adresi çarpışmalarına karşı koruma sağlamaz , bu da daha hızlı makine koduna neden olur.

Örneğin :

Aşağıdaki işlevi varsayalım:

void merge_two_ints(int *a, int *b) {
  *b += *a;
  *a += *b;
}

a == b(Her iki işaretçi de aynı belleğe işaret ediyor) durumunu işlemek için , bellekten CPU kayıtlarına veri yükleme şeklimizi sipariş etmeli ve test etmeliyiz, böylece kod şu şekilde sonuçlanabilir:

  1. yük ave bbellekten.

  2. eklemek aiçin b.

  3. kaydedin b ve yeniden yükleyin a .

    (CPU kaydından belleğe kaydedin ve bellekten CPU kaydına yükleyin).

  4. eklemek biçin a.

  5. a(CPU kaydından) belleğe kaydedin .

3. adım çok yavaştır çünkü fiziksel belleğe erişmesi gerekir. Ancak, aynı bellek adresini gösteren ave bişaret eden durumlara karşı korunmak gerekir .

Sıkı örtüşme, derleyiciye bu bellek adreslerinin belirgin şekilde farklı olduğunu söyleyerek bunu önlememize olanak tanır (bu durumda işaretçiler bir bellek adresini paylaşırsa gerçekleştirilemeyecek daha fazla optimizasyon sağlar).

  1. Bu, derleyiciye işaret etmek için farklı türler kullanılarak iki şekilde söylenebilir. yani:

    void merge_two_numbers(int *a, long *b) {...}
  2. restrictAnahtar kelimeyi kullanma . yani:

    void merge_two_ints(int * restrict a, int * restrict b) {...}

Şimdi, Sıkı Aliasing kuralını karşılayarak, 3. adımdan kaçınılabilir ve kod önemli ölçüde daha hızlı çalışacaktır.

Aslında, restrictanahtar kelime ekleyerek , tüm işlev aşağıdakiler için optimize edilebilir:

  1. yük ave bbellekten.

  2. eklemek aiçin b.

  3. save hem sonuçlanabilir ave b.

Bu optimizasyon, olası çarpışma nedeniyle daha önce yapılamazdı (iki kat yerine üç kat nereye ave büç katına çıkar).


restrict anahtar kelimesi ile, 3. adımda, sonucu yalnızca 'b' ye kaydetmemelisiniz? Toplamanın sonucu 'a'da da saklanacak gibi geliyor. Tekrar 'b' yüklenmesi gerekiyor mu?
NeilB

1
@NeilB - Yap haklısın. Sadece tasarruf ediyoruz b(yeniden yüklemiyoruz) ve yeniden yüklüyoruz a. Umarım şimdi daha açıktır.
Myst

Tür temelli takma ad, daha önce bazı faydalar sunmuş olabilir restrict, ancak ikincisinin çoğu durumda daha etkili olacağını düşünüyorum ve bazı kısıtlamaları gevşetmek, yardımcı olmayacağı registerbazı durumlarda doldurmasına izin verecektir restrict. Standardı, programcıların derleyicilerin takma kanıtlarını tanımasını beklemeleri gereken tüm durumları tam olarak tanımlamanın, yalnızca derleyicilerin belirli bir kanıtı olmadığında bile takma adı almaları gereken yerleri tanımlamak yerine "önemli" olduğundan emin değilim .
supercat

Ana RAM'den yükleme çok yavaş olsa da (ve aşağıdaki işlemler sonuca bağlıysa CPU çekirdeğini uzun süre durdurabilir), L1 önbellekten yükleme oldukça hızlıdır ve bu nedenle son zamanlarda yazılan bir önbellek satırına yazıyor aynı çekirdek tarafından. Bu nedenle, bir adrese ilk okuma veya yazma dışındaki tümü genellikle oldukça hızlı olacaktır: reg / mem adresi erişimi arasındaki fark, önbelleğe alınan / önbelleğe alınmayan bellek adresi arasındaki farktan daha küçüktür.
curiousguy

@curiousguy - haklı olmanıza rağmen, bu durumda "hızlı" görecelidir. L1 önbelleği muhtemelen hala CPU kayıtlarından daha yavaş bir büyüklük sırasıdır (10 kattan daha yavaş olduğunu düşünüyorum). Buna ek olarak, restrictanahtar kelime sadece işlemlerin hızını değil, aynı zamanda sayılarını da en aza indirir, bu da anlamlı olabilir ... Yani, sonuçta, en hızlı işlem hiç bir işlem değildir :)
Myst

6

Katı takma adlandırma, aynı verilere farklı işaretçi türlerine izin vermez.

Bu makale sorunu ayrıntılı olarak anlamanıza yardımcı olacaktır.


4
Referanslar arasında ve bir referans ile bir işaretçi arasındaki diğer adı da kullanabilirsiniz. Benim öğretici dbp-consulting.com/tutorials/StrictAliasing.html
phorgan1

4
Aynı verilere farklı işaretçi türlerine sahip olmasına izin verilir. Katı takma adın geldiği yer, aynı bellek konumunun bir işaretçi türüyle yazılması ve diğerinin okunmasıdır. Ayrıca, bazı farklı tiplere izin verilir (örn. intVe bir içeren yapı int).
MM

-3

Teknik olarak C ++ 'da, kesin örtüşme kuralı muhtemelen hiçbir zaman uygulanamaz.

İndirection ( * operator ) tanımına dikkat edin :

Unary * operatörü dolaylı olarak gerçekleştirir: uygulandığı ifade, bir nesne tipine bir işaretçi veya bir işlev tipine bir işaretçi olacaktır ve sonuç, ifadenin işaret ettiği nesneye veya işleve atıfta bulunan bir değerdir .

Ayrıca glvalue tanımından

Glvalue, değerlendirmesi bir nesnenin kimliğini belirleyen bir ifadedir, (... snip)

Bu nedenle, iyi tanımlanmış herhangi bir program izlemede, bir glvalue bir nesneye karşılık gelir. Dolayısıyla, sıkı takma adlandırma kuralı hiç geçerli değildir. Tasarımcıların istediği bu olmayabilir.


4
C Standardı, bir dizi farklı kavramı ifade etmek için "nesne" terimini kullanır. Bunlar arasında, sadece bir amaç için ayrılan byte bir dizi, belirli bir türdeki bir değer olan / 'e bayt dizisi için değil-zorunlu münhasır bir referans olabilir yazılı veya okuma ya da bu tür bir referans aslında var bir bağlamda erişilmiş veya erişilebilir. Standardın kullandığı yöntemle tutarlı olan "Nesne" terimini tanımlamanın mantıklı bir yolu olduğunu düşünmüyorum.
supercat

@supercat Yanlış. Hayal gücünüze rağmen, aslında oldukça tutarlıdır. ISO C'de "içeriği değerleri gösterebilen yürütme ortamında veri depolama bölgesi" olarak tanımlanır. ISO C ++ 'da benzer bir tanım vardır. Yorumunuz yanıttan daha da önemsizdir, çünkü bahsettiğiniz tek şey nesnelerin içeriğine atıfta bulunmanın temsil yollarıdır , cevap ise nesnelerin kimliği ile sıkı bir şekilde ilgili olan bir tür ifadelerin C ++ konseptini (glvalue) gösterir . Ve tüm diğer adlandırma kuralları temel olarak kimlikle ilgilidir, içerikle ilgili değildir.
FrankHB

1
@FrankHB: Eğer biri beyan ederse int foo;, lvalue ifadesi ile neye erişilir *(char*)&foo? Bu tür bir nesne charmi? Bu nesne aynı zamanda ortaya çıkıyor foomu? fooYukarıda belirtilen türdeki nesnenin depolanan değerini değiştirmek için yazmak ister misiniz char? Öyleyse, türdeki bir nesnenin depolanan değerine bir tür değeri charkullanarak erişilmesine izin verecek herhangi bir kural var intmı?
Supercat

@FrankHB: 6.5p7 olmadığında, her depolama bölgesinin aynı anda o depolama alanına sığabilecek her türden tüm nesneleri içerdiğini ve bu depolama bölgesine erişmenin aynı anda hepsine eriştiğini söyleyebiliriz. Bununla birlikte, 6.5p7'de "nesne" teriminin kullanımını yorumlamak, karakter tipi olmayan değerlere sahip, açıkça saçma bir sonuç olacak ve kuralın amacını tamamen yenecek bir şey yapmayı yasaklayacaktır. Ayrıca, 6.5p6 dışında her yerde kullanılan "nesne" kavramı statik derleme zamanı tipine sahiptir, ancak ...
Supercat

1
sizeof (int) 4'tür, bildirim int * (char *) & i` ve int i;her karakter türünden dört nesne oluşturur mu ? Son olarak, Standartta, nitelikli bir işaretçinin bile "nesne" tanımını karşılamayan donanım kayıtlarına erişmesine izin veren hiçbir şey yoktur . in addition to one of type ? I see no way to apply a consistent definition of "object" which would allow for operations on both ivolatile
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.