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?
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?
Yanıtlar:
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_t
s veya uint16_t
s'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, buff
dö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[1
döngü gövdesini hızlandırabilir. Sıkı takma ad kullanılmadan önce, derleyici içeriğinin buff
herhangi 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 SendMessage
ayrı 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 char
ve 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 .
1 C 2011 6.5 7'nin bir değer erişimine izin verdiği türler şunlardır:
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 char
ziyade kullanma eğilimindeyim (özellikle taşması için wrt)char
byte
unsigned char *
iyidir.
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 ...
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 int
ve sonra float*
o belleğe a işaret ederseniz ve bunu float
kuralı 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.
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
char
veyaunsigned char
tip.
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
char
veyaunsigned char
tip.
İ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.
wow(&u->s1,&u->s2)
değiştirmek için bir işaretçi kullanıldığında bile yasal olması gerekir u
ve bu, takma kural kolaylaştırmak için tasarlanmıştır.
Bu, "Sıkı Takma Adlandırma Kuralı nedir ve neden önemsiyoruz?" yazma-up.
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.
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 .
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 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.
[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ü .
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.
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'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 .
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.
reinterpret_cast
yapabileceğ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)
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.
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.
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.
İş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.
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.
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 x
etme 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.p
x
*p
x
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 int
erişim tip bir nesne struct S
ve int
bir 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 *p
bir tür değerle yapılır int
ve hiçbir çelişki yoktur, test
çünkü p
a'dan bir struct S
sonraki kullanımda ve bir dahaki sefere s
kullanıldığında, o depolamaya yapılan tüm erişimler içinden p
zaten 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 p
eriş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.
Cevapların çoğunu okuduktan sonra bir şeyler eklemeye ihtiyacım var:
Sıkı takma adlandırma (birazdan açıklayacağım) önemlidir çünkü :
Bellek erişimi pahalı olabilir (performans açısından), bu nedenle veriler fiziksel belleğe yazılmadan önce CPU kayıtlarında işlenir.
İ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 restrict
anahtar 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:
yük a
ve b
bellekten.
eklemek a
için b
.
kaydedin b
ve yeniden yükleyin a
.
(CPU kaydından belleğe kaydedin ve bellekten CPU kaydına yükleyin).
eklemek b
için a
.
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 a
ve b
iş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).
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) {...}
restrict
Anahtar 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, restrict
anahtar kelime ekleyerek , tüm işlev aşağıdakiler için optimize edilebilir:
yük a
ve b
bellekten.
eklemek a
için b
.
save hem sonuçlanabilir a
ve b
.
Bu optimizasyon, olası çarpışma nedeniyle daha önce yapılamazdı (iki kat yerine üç kat nereye a
ve b
üç katına çıkar).
b
(yeniden yüklemiyoruz) ve yeniden yüklüyoruz a
. Umarım şimdi daha açıktır.
restrict
, ancak ikincisinin çoğu durumda daha etkili olacağını düşünüyorum ve bazı kısıtlamaları gevşetmek, yardımcı olmayacağı register
bazı 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 .
restrict
anahtar 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 :)
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.
int
Ve bir içeren yapı int
).
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.
int foo;
, lvalue ifadesi ile neye erişilir *(char*)&foo
? Bu tür bir nesne char
mi? Bu nesne aynı zamanda ortaya çıkıyor foo
mu? foo
Yukarı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 char
kullanarak erişilmesine izin verecek herhangi bir kural var int
mı?
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
i
volatile
c
ve ile etiketlendic++faq
?