Bir yapıyı indekslemek yasal mı?


104

Kod ne kadar 'kötü' olursa olsun ve hizalamanın vb. Derleyici / platformda bir sorun olmadığını varsayarsak, bu tanımsız veya bozuk davranış mı?

Bunun gibi bir yapım varsa: -

struct data
{
    int a, b, c;
};

struct data thing;

Öyle mi hukuki erişim a, bve colarak (&thing.a)[0], (&thing.a)[1]ve (&thing.a)[2]?

Her durumda, denediğim her derleyici ve platformda, denediğim her ayarla 'işe yaradı'. Sadece derleyicinin b ve şey [1] 'in aynı şey olduğunu ve' b'ye depolananların bir kayıt defterine konulabileceğini ve şey [1] 'in bellekten yanlış değeri okuduğunu (örneğin) fark edemeyeceğinden endişeleniyorum . Her durumda denedim ama doğru olanı yaptım. (Bunun pek bir kanıt olmadığını anlıyorum)

Bu benim kodum değil; bu kod ile çalışmam gerekiyor, bunun kötü kod mu yoksa bozuk kod mu olduğu ile ilgileniyorum çünkü farklı olanlar onu değiştirmek için önceliklerimi büyük ölçüde etkiliyor :)

C ve C ++ etiketli. Çoğunlukla C ++ ile ilgileniyorum ama aynı zamanda C farklıysa, sadece ilgi için.


51
Hayır, "yasal" değil. Tanımlanmamış bir davranıştır.
Sam Varshavchik

10
Bu çok basit durumda sizin için işe yarar çünkü derleyici üyeler arasına herhangi bir dolgu eklemez. Farklı boyutlarda türler kullanan yapıları deneyin ve çökecek.
Bazı programcı ahbap

7
Geçmişi araştırmak - UB, takma isimli nazal cinlerdi .
Adrian Colomitchi

21
Harika, burada tökezledim çünkü C etiketini takip ediyorum, soruyu okudum ve sonra sadece C için geçerli bir cevap yazıyorum çünkü C ++ etiketini görmedim. C ve C ++ burada çok farklı! C, birleşimlerle tür punning'e izin verir, C ++ ise izin vermez.
Lundin

7
Elemanlara bir dizi olarak erişmeniz gerekiyorsa, bunları bir dizi olarak tanımlayın. Farklı isimlere sahip olmaları gerekiyorsa isimleri kullanın. Pastanızı yemeye ve onu yemeye çalışmak, sonunda hazımsızlığa yol açacaktır - muhtemelen akla gelebilecek en uygunsuz zamanda. (Bence 0 indisi C'de geçerli; indeks 1 veya 2 değil. Tek bir öğenin 1 boyutlu bir dizi olarak değerlendirildiği bağlamlar var.)
Jonathan Leffler

Yanıtlar:


73

Yasadışıdır 1 . Bu, C ++ 'da tanımlanmamış bir davranıştır.

Üyeleri bir dizi biçiminde alıyorsunuz, ancak burada C ++ standardının söylediği şey (vurgu benim):

[dcl.array / 1] : ... Dizi türündeki bir nesne, bitişik olarak ayrılmış, boş olmayan, T türünde N altnesneden oluşan birküme içerir ...

Ancak üyeler için böyle bir bitişik gereklilik yoktur :

[class.mem / 17] : ...; Uygulama uyum gereksinimleri , iki bitişik üyenin birbirinin hemen ardından tahsis edilmemesine neden olabilir ...

Yukarıdaki iki tırnak struct, sizin yaptığınız gibi dizine eklemenin neden C ++ standardı tarafından tanımlanmış bir davranış olmadığına işaret etmek için yeterli olsa da , bir örnek seçelim: ifadeye bakın (&thing.a)[2]- Alt simge operatörü ile ilgili olarak:

[expr.post//expr.sub/1] : Köşeli parantez içinde bir ifadenin izlediği bir sonek ifadesi bir sonek ifadesidir. İfadelerden biri, "T dizisi" türünde bir glvalue veya "T'ye işaretçi" türünde bir prvalue olmalı ve diğeri, kapsama alınmamış numaralandırma veya integral türünün bir pr değeri olacaktır. Sonuç "T" tipindedir. "T" türü, tamamen tanımlanmış bir nesne türü olacaktır.66 İfade E1[E2], (tanım gereği) ile aynıdır.((E1)+(E2))

Yukarıdaki alıntının kalın metnini derinlemesine incelersek: bir işaretçi tipine bir integral türü eklemeyle ilgili (burada vurguya dikkat edin) ..

[ifade.add / 4] : Bir işaretleyiciye integral tipli bir ifade eklendiğinde veya çıkarıldığında, sonuç, işaretçi işleneninin tipine sahip olur. Eğer ekspresyonPelemanına puanx[i]arasında bir dizi nesnex N elemanlar, ifadelerleP + JveJ + P(Jdeğerine sahiptirj(muhtemelen varsayımsal-) elemana) noktasıx[i + j] ise0 ≤ i + j ≤ n; aksi takdirde davranış tanımsızdır. ...

Not dizi gereksinimini eğer maddesi; Başka aksi yukarıdaki alıntıda. İfade (&thing.a)[2]açıkça if cümlesi için uygun değildir ; Bu nedenle, Tanımsız Davranış.


Bir yan not: Kodu ve varyasyonlarını çeşitli derleyiciler üzerinde kapsamlı bir şekilde denemiş olsam ve burada herhangi bir dolgu eklemiyorlar, (işe yarıyor ); bakım açısından, kod son derece kırılgandır. Bunu yapmadan önce uygulamanın üyeleri bitişik olarak tahsis ettiğini yine de iddia etmelisiniz. Ve sınırlar içinde kalın :-). Ama yine de Tanımsız davranışı ...

Bazı uygulanabilir geçici çözümler (tanımlanmış davranışla) diğer yanıtlarla sağlanmıştır.



Yorumlarda haklı olarak belirtildiği gibi , önceki düzenlememde olan [basic.lval / 8] geçerli değildir. Teşekkürler @ 2501 ve @MM

1 : thing.aYapı üyesine bu parttern aracılığıyla erişebileceğiniz tek yasal dava için @ Barry'nin bu soruya cevabına bakın.


1
@jcoder Bu tanımlanır class.mem . Asıl metin için son paragrafa bakın.
NathanOliver

4
Kesin değişiklik burada geçerli değildir. İnt türü, toplama türünün içinde bulunur ve bu tür, int olarak adlandırılabilir. - an aggregate or union type that includes one of the aforementioned types among its elements or non-static data members (including, recursively, an element or non-static data member of a subaggregate or contained union),
2501

1
@ Olumsuz oy verenler, yorum yapmak ister misiniz? - ve bu cevabın yanlış olduğu yeri geliştirmek veya işaret etmek için?
WhiZTiM

4
Kesin örtüşme bununla alakasızdır. Dolgu, bir nesnenin depolanan değerinin parçası değildir. Ayrıca bu yanıt, en yaygın durumu ele almakta başarısızdır: dolgu olmadığında ne olur. Aslında bu cevabı silmenizi tavsiye ederim.
MM

1
Bitti! Kesin örtüşme ile ilgili paragrafı kaldırdım.
WhiZTiM

48

Hayır. C'de, dolgu olmasa bile bu tanımlanmamış bir davranıştır.

Tanımlanmamış davranışa neden olan şey, sınır dışı erişimdir 1 . Bir skaleriniz olduğunda (yapıdaki a, b, c üyeleri) ve onu bir sonraki varsayımsal öğeye erişmek için bir dizi 2 olarak kullanmaya çalıştığınızda , aynı türde başka bir nesne olsa bile tanımsız davranışa neden olursunuz. bu adres.

Bununla birlikte, struct nesnesinin adresini kullanabilir ve ofseti belirli bir üyeye göre hesaplayabilirsiniz:

struct data thing = { 0 };
char* p = ( char* )&thing + offsetof( thing , b );
int* b = ( int* )p;
*b = 123;
assert( thing.b == 123 );

Bu, her üye için ayrı ayrı yapılmalıdır, ancak bir dizi erişimine benzeyen bir işleve konulabilir.


1 (Alıntı: ISO / IEC 9899: 201x 6.5.6 Ekleme operatörleri 8)
Sonuç, dizi nesnesinin son elemanını bir geçiyorsa , değerlendirilen tekli * bir operatörün işleneni olarak kullanılmamalıdır.

2 (Alıntı: ISO / IEC 9899: 201x 6.5.6 Ekleme operatörleri 7)
Bu operatörlerin amaçları doğrultusunda, bir dizinin öğesi olmayan bir nesneye yönelik bir işaretçi, bir dizinin ilk öğesine işaretçi gibi davranır. öğe türü olarak nesnenin türü ile bir uzunluk dizisi.


3
Bunun yalnızca sınıf standart bir düzen türü olduğunda işe yaradığını unutmayın. Değilse, hala UB'dir.
NathanOliver

@NathanOliver Cevabımın yalnızca C. Düzenlendi. Bu, bu tür ikili etiket dili sorularının sorunlarından biridir.
2501

Teşekkürler, bu yüzden C ++ ve C'yi ayrı ayrı sordum çünkü farklılıkları görmek ilginç
jcoder

@NathanOliver İlk üyenin adresi, standart düzen ise C ++ sınıfının adresiyle aynı olacak şekilde garantilidir. Ancak, bu ne erişimin iyi tanımlandığını garanti eder ne de diğer sınıflar üzerindeki bu tür erişimlerin tanımsız olduğunu ima eder.
Potatoswatter

bunun char* p = ( char* )&thing.a + offsetof( thing , b );tanımlanmamış davranışlara yol açtığını söyleyebilir misiniz ?
MM

43

C ++ 'da gerçekten ihtiyacınız varsa - işleci oluşturun []:

struct data
{
    int a, b, c;
    int &operator[]( size_t idx ) {
        switch( idx ) {
            case 0 : return a;
            case 1 : return b;
            case 2 : return c;
            default: throw std::runtime_error( "bad index" );
        }
    }
};


data d;
d[0] = 123; // assign 123 to data.a

sadece çalışması garanti edilmez, kullanım daha basittir, okunamayan ifadeler yazmanıza gerek yoktur (&thing.a)[0]

Not: Bu cevap, zaten alanları olan bir yapınız olduğu ve indeks yoluyla erişim eklemeniz gerektiği varsayımıyla verilmiştir. Hız bir sorunsa ve yapıyı değiştirebiliyorsanız, bu daha etkili olabilir:

struct data 
{
     int array[3];
     int &a = array[0];
     int &b = array[1];
     int &c = array[2];
};

Bu çözüm yapının boyutunu değiştirir, böylece yöntemleri de kullanabilirsiniz:

struct data 
{
     int array[3];
     int &a() { return array[0]; }
     int &b() { return array[1]; }
     int &c() { return array[2]; }
};

1
Bir C programının punning kullanarak demonte edilmesine karşı bunun demontajını görmeyi çok isterim. Ama, ama ... C ++, C kadar hızlı ... değil mi? Sağ?
Lundin

6
@Lundin, bu yapının hızını önemsiyorsanız, veriler ayrı alanlar olarak değil, ilk etapta bir dizi olarak düzenlenmelidir.
Slava

2
@Lundin ikisinde de okunamayan ve Tanımsız Davranış mı demek istiyorsun? Hayır teşekkürler.
Slava

1
@Lundin Operatör aşırı yükleme, normal işlevlere kıyasla herhangi bir ek yüke neden olmayan bir derleme zamanı sözdizimsel özelliktir. Derleyicinin C ++ ve C kodunu derlerken gerçekte ne yaptığını görmek için godbolt.org/g/vqhREz sayfasına bir göz atın . Yaptıkları ve onlardan bekledikleri şey inanılmaz. Ben şahsen C ++ 'ın daha iyi tip güvenliğini ve ifade gücünü C'den milyon kez tercih ediyorum. Ve dolgu ile ilgili varsayımlara güvenmeksizin her zaman çalışır.
Jens

2
Bu referanslar en azından şeyin boyutunu ikiye katlayacak. Sadece yap thing.a().
TC

14

C ++ için: Bir üyeye adını bilmeden erişmeniz gerekiyorsa, üye değişkenine bir işaretçi kullanabilirsiniz.

struct data {
  int a, b, c;
};

typedef int data::* data_int_ptr;

data_int_ptr arr[] = {&data::a, &data::b, &data::c};

data thing;
thing.*arr[0] = 123;

1
Bu, dil olanaklarını kullanıyor ve sonuç olarak iyi tanımlanmış ve tahmin ettiğim gibi verimli. En iyi cevap.
Peter - Monica'yı Yeniden

2
Verimli mi varsayalım? Ben tersini varsayıyorum. Bakın oluşturulan koda.
JDługosz

1
@ JDługosz, oldukça haklısınız. Bir göz alarak montaj oluşturulan en, gcc 6.2 kullanarak kod eşdeğer oluşturur görünüyor offsetoffC. içinde
Masalcı - Unslander Monica

3
arr constexpr yaparak da işleri geliştirebilirsiniz. Bu, anında oluşturmak yerine veri bölümünde tek bir sabit arama tablosu oluşturacaktır.
Tim

10

ISO C99 / C11'de, birleşim temelli tür işleme yasaldır, bu nedenle işaretçileri dizi olmayanlara dizinlemek yerine bunu kullanabilirsiniz (çeşitli diğer yanıtlara bakın).

ISO C ++, sendika tabanlı tür karıştırmaya izin vermez. GNU C ++, bir uzantı olarak bunu yapar ve genel olarak GNU uzantılarını desteklemeyen bazı diğer derleyicilerin birleşim tipi araştırmayı desteklediğini düşünüyorum. Ancak bu, kesinlikle taşınabilir kod yazmanıza yardımcı olmaz.

Gcc ve clang'ın mevcut sürümlerinde, switch(idx)bir üye seçmek için a kullanarak bir C ++ üye işlevi yazmak , derleme zamanı sabit dizinlerini optimize edecek, ancak çalışma zamanı dizinleri için korkunç dallı asm üretecektir. Bunun doğal olarak yanlış bir tarafı yok switch(); bu sadece mevcut derleyicilerde gözden kaçan bir optimizasyon hatasıdır. Slava'nın switch () işlevini verimli bir şekilde derleyebilirler.


Bunun çözümü / geçici çözümü, bunu başka şekilde yapmaktır: sınıfınıza / yapınıza bir dizi üyesi verin ve belirli öğelere adlar eklemek için erişimci işlevleri yazın.

struct array_data
{
  int arr[3];

  int &operator[]( unsigned idx ) {
      // assert(idx <= 2);
      //idx = (idx > 2) ? 2 : idx;
      return arr[idx];
  }
  int &a(){ return arr[0]; } // TODO: const versions
  int &b(){ return arr[1]; }
  int &c(){ return arr[2]; }
};

Godbolt derleyici kaşifinde farklı kullanım durumları için asm çıktısına bir göz atabiliriz . Bunlar eksiksiz x86-64 System V işlevleridir, sondaki RET talimatı satır içi olduklarında ne elde edeceğinizi daha iyi göstermek için atlanmıştır. ARM / MIPS / benzer ne olursa olsun.

# asm from g++6.2 -O3
int getb(array_data &d) { return d.b(); }
    mov     eax, DWORD PTR [rdi+4]

void setc(array_data &d, int val) { d.c() = val; }
    mov     DWORD PTR [rdi+8], esi

int getidx(array_data &d, int idx) { return d[idx]; }
    mov     esi, esi                   # zero-extend to 64-bit
    mov     eax, DWORD PTR [rdi+rsi*4]

Karşılaştırmak gerekirse, @ Slava'nın switch()C ++ için kullandığı yanıtı , çalışma zamanı değişken dizini için asm'yi böyle yapar. (Önceki Godbolt bağlantısındaki kod).

int cpp(data *d, int idx) {
    return (*d)[idx];
}

    # gcc6.2 -O3, using `default: __builtin_unreachable()` to promise the compiler that idx=0..2,
    # avoiding an extra cmov for idx=min(idx,2), or an extra branch to a throw, or whatever
    cmp     esi, 1
    je      .L6
    cmp     esi, 2
    je      .L7
    mov     eax, DWORD PTR [rdi]
    ret
.L6:
    mov     eax, DWORD PTR [rdi+4]
    ret
.L7:
    mov     eax, DWORD PTR [rdi+8]
    ret

Bu, C (veya GNU C ++) sendika tabanlı tür punning sürümüne kıyasla açıkça korkunç:

c(type_t*, int):
    movsx   rsi, esi                   # sign-extend this time, since I didn't change idx to unsigned here
    mov     eax, DWORD PTR [rdi+rsi*4]

@MM: iyi nokta. Daha çok çeşitli yorumlara bir cevap ve Slava'nın cevabına bir alternatif. Açılış kısmını yeniden yazdım, bu yüzden en azından orijinal soruya bir cevap olarak başlıyor. Bunu belirttiğiniz için teşekkürler.
Peter Cordes

Kullanırken birlik tabanlı tip cinaslı gcc ve clang çalışma gibi görünürken []bir birlik üyesi doğrudan operatörü, Standart tanımlar array[index]eşdeğer olarak *((array)+(index)), gcc ne de çınlama ve ne güvenilir bir erişim anlayacaktır *((someUnion.array)+(index))bir erişim olduğu someUnion. Görebildiğim tek açıklama olduğunu someUnion.array[index]ne de *((someUnion.array)+(index))Standardı tarafından tanımlanan, ancak popüler uzantılar sadece vardır ve gcc / çınlama ikinci desteklemek için seçti ama en azından şu anda, ilk desteklemiyor gibi değildir.
supercat

9

C ++ 'da, bu çoğunlukla tanımlanmamış bir davranıştır (hangi dizine bağlıdır).

[Expr.unary.op] kaynağından:

İşaretçi aritmetiği (5.7) ve karşılaştırma (5.9, 5.10) amaçları için, adresi bu şekilde alınan bir dizi elemanı olmayan bir nesnenin, tek tip elemanlı bir diziye ait olduğu kabul edilir T.

Bu &thing.anedenle, ifadenin bir dizisine gönderme yaptığı kabul edilir int.

[Expr.sub] 'dan:

İfade E1[E2]aynıdır (tanım gereği)*((E1)+(E2))

Ve [ifade.add] 'dan:

İntegral türü olan bir ifade bir işaretçiye eklendiğinde veya çıkarıldığında, sonuç işaretçi işleneninin türüne sahiptir. Sentezleme ise Pelemana noktaları x[i]bir dizi nesne xile nelemanlarının, ifadeler P + Jve J + P(burada Jdeğerine sahiptir j(muhtemelen varsayımsal-) elemana) noktası x[i + j]ise 0 <= i + j <= n; aksi takdirde davranış tanımsızdır.

(&thing.a)[0]mükemmel bir şekilde biçimlendirilmiş çünkü &thing.a1 boyutlu bir dizi olarak kabul ediliyor ve bu ilk dizini alıyoruz. Bu, alınmasına izin verilen bir indekstir.

(&thing.a)[2]ön şart ihlal ettiğini 0 <= i + j <= n, elimizdeki beri i == 0, j == 2, n == 1. Basitçe işaretçiyi oluşturmak &thing.a + 2tanımsız bir davranıştır.

(&thing.a)[1]ilginç durum. Aslında [ifade.add] 'deki hiçbir şeyi ihlal etmez. Dizinin sonunun bir ötesine bir işaretçi almamıza izin verilir - ki bu olabilir. Burada, [basic.compound] 'da bir nota dönüyoruz:

Bir nesnenin sonuna işaret eden veya onu geçen bir işaretçi türünün bir değeri, nesne53 tarafından işgal edilen bellekteki ilk baytın (1.7) adresini veya nesnenin kapladığı depolama bitiminden sonra bellekteki ilk baytın adresini temsil eder. , sırasıyla. [Not: Bir nesnenin (5.7) sonunu geçen bir işaretçinin, o adreste bulunabilecek nesnenin türünün ilgisiz bir nesnesini işaret ettiği kabul edilmez.

Bu nedenle, işaretçiyi almak &thing.a + 1tanımlanmış bir davranıştır, ancak onu referans almak tanımsızdır çünkü hiçbir şeyi göstermez.


+ 1'i değerlendirmek (& şey.a) hemen hemen yasaldır çünkü bir dizinin sonunu geçen bir işaretçi yasaldır; orada depolanan veriyi okumak veya yazmak tanımsız bir davranış, & şey.b ile <,>, <=,> = ile karşılaştırmak tanımsız bir davranıştır. (& şey.a) + 2 kesinlikle yasa dışı.
gnasher729

@ gnasher729 Evet, cevabı biraz daha açıklamaya değer.
Barry

Bu (&thing.a + 1), ele alamadığım ilginç bir dava. +1! ... Sadece merak ediyorum, ISO C ++ komitesinde misiniz?
WhiZTiM

Bu aynı zamanda çok önemli bir durumdur çünkü aksi takdirde yarı açık aralık olarak işaretçileri kullanan her döngü UB olacaktır.
Jens

Son standart alıntıyla ilgili olarak. C ++, burada C'den daha iyi belirtilmelidir.
2501

8

Bu tanımlanmamış bir davranıştır.

C ++ 'da, derleyiciye ne yaptığınızı anlamak için biraz umut vermeye çalışan birçok kural vardır, böylece bu konuda akıl yürütebilir ve optimize edebilir.

Örtüşme (verilere iki farklı işaretçi türü aracılığıyla erişim), dizi sınırları vb. Hakkında kurallar vardır.

Bir değişkeniniz olduğunda x, onun bir dizinin üyesi olmaması, derleyicinin hiçbir []temelli dizi erişiminin onu değiştiremeyeceğini varsayabileceği anlamına gelir . Bu nedenle, her kullandığınızda verileri bellekten sürekli olarak yeniden yüklemesi gerekmez; sadece birisi isminden değiştirmiş olsaydı .

Bu nedenle (&thing.a)[1], derleyici tarafından atıfta bulunulmadığı varsayılabilir thing.b. Bu gerçeği, okumaları ve yazmaları yeniden sıralamak için thing.b, gerçekte ne yapmasını söylediğinizi geçersiz kılmadan yapmasını istediğiniz şeyi geçersiz kılmak için kullanabilir.

Bunun klasik bir örneği, const'ı atmaktır.

const int x = 7;
std::cout << x << '\n';
auto ptr = (int*)&x;
*ptr = 2;
std::cout << *ptr << "!=" << x << '\n';
std::cout << ptr << "==" << &x << '\n';

burada genellikle 7 sonra 2! = 7 diyen bir derleyici ve ardından iki özdeş işaretçi elde edersiniz; ptrişaret ettiği gerçeğine rağmen x. Derleyici, xdeğerini sorduğunuzda okumaya zahmet etmemek için sabit bir değer olduğu gerçeğini alır x.

Ama adresini xaldığınızda, onu var olmaya zorlarsınız. Daha sonra const'ı atarsınız ve onu değiştirirsiniz. Dolayısıyla, xdeğiştirilmiş olan bellekteki gerçek konum , derleyici okurken onu gerçekten okumamakta özgürdür x!

Derleyici, ptrokumak için takip etmekten nasıl kaçınılacağını anlayacak kadar akıllı olabilir *ptr, ancak çoğu zaman değildir. ptr = ptr+argc-1Optimize edici sizden daha akıllı hale geliyorsa , gidin ve kullanmaktan veya biraz kafa karıştırmaktan çekinmeyin .

operator[]Doğru öğeyi alan bir özel sağlayabilirsiniz .

int& operator[](std::size_t);
int const& operator[](std::size_t) const;

her ikisine de sahip olmak faydalıdır.


"bir dizinin üyesi olmaması, derleyicinin [] tabanlı dizi erişiminin onu değiştiremeyeceğini varsayabileceği anlamına gelir." - doğru değil, örneğin (&thing.a)[0]değiştirebilir
MM

Const örneğinin soruyla nasıl bir ilgisi olduğunu anlamıyorum. Bu, yalnızca bir const nesnesinin değiştirilemeyeceğine dair belirli bir kural olduğu için başarısız olur, başka bir nedenle değil.
MM

1
@MM, bir yapı içine indeksleme bir örnek değil, ama bir var çok onun tarafından referans şeye tanımsız davranışı kullanarak nasıl iyi illüstrasyon belirgin derleyici, çünkü, beklenenden daha farklı çıktı neden olabilir bellekte konumu başka bir şey yapmak ile UB istediğinden daha fazla.
Wildcard

@MM Maalesef, nesnenin kendisine bir işaretçi aracılığıyla önemsiz bir dizi dışında bir dizi erişimi yok. İkincisi, tanımlanmamış davranışın yan etkilerinin görülmesi kolay bir örnektir; derleyici okumaları optimize eder xçünkü onu tanımlanmış bir şekilde değiştiremeyeceğinizi bilir . Derleyici, onu değiştirebilecek tanımlanmış bir erişim olmadığını kanıtlayabilirse, baracılığıyla değişiklik yaptığınızda benzer optimizasyon gerçekleşebilir ; böyle bir değişiklik derleyicideki, çevreleyen koddaki veya her neyse, zararsız değişikliklerden kaynaklanabilir. Yani çalıştığını test etmek bile yeterli değil. (&blah.a)[1]b
Yakk - Adam Nevraumont

6

Bir üye dizisindeki öğelere ada göre erişmek için bir proxy sınıfı kullanmanın bir yolu. Oldukça C ++ 'dır ve sözdizimsel tercih dışında ref-dönen erişimci işlevlerine göre hiçbir faydası yoktur. Bu, ->operatörün öğelere üyeler olarak erişmesini aşırı yükler , bu nedenle kabul edilebilir olması için, hem erişimcilerin sözdiziminden ( d.a() = 5;) hoşlanmamalı hem de ->işaretçi olmayan bir nesneyi kullanmayı tolere etmelidir . Bunun, koda aşina olmayan okuyucuların kafasını karıştıracağını umuyorum, bu yüzden bu, üretime sokmak istediğiniz bir şeyden daha düzgün bir numara olabilir.

DataBu kod yapı, aynı zamanda erişim dizine böylece iç elemanlara simge operatör için aşırı yük içerir ardizi üyesi, aynı zamanda beginve endyineleme için işlevler. Ayrıca, bunların tümü, tamlık için dahil edilmesi gerektiğini düşündüğüm const olmayan ve const sürümleriyle aşırı yüklenmiş.

Tüm Data's ->(örneğin: adıyla bir eleman erişmek için kullanılan my_data->b = 5;), bir Proxynesne döndürülür. Daha sonra, bu Proxydeğer bir işaretçi olmadığından, kendi ->operatörü otomatik zincir olarak adlandırılır ve kendisine bir işaretçi döndürür. Bu şekilde, Proxynesne somutlaştırılır ve ilk ifadenin değerlendirilmesi sırasında geçerli kalır.

A Konstrüksiyon Proxynesnesi olarak 3 referans üye doldurur a, bve ctürü şablon parametre olarak verilen, en az 3 değerlerini içeren bir tampona noktasına varsayılır yapıcıda geçirilen bir işaretçi göre T. Bu nedenle, Datasınıfın üyeleri olan adlandırılmış referansları kullanmak yerine , bu, referansları erişim noktasında doldurarak hafızadan tasarruf sağlar (ancak maalesef operatörü kullanarak ->değil .).

Derleyicinin eniyileyicisinin, kullanımıyla ortaya çıkan tüm indirimi ne kadar iyi ortadan kaldırdığını test etmek için Proxy, aşağıdaki kod main(),. #if 1Versiyonu kullanır ->ve []operatörler ve #if 0sürümü ama sadece direkt erişerek, prosedürler eşdeğer dizi gerçekleştirir Data::ar.

Nci()Fonksiyonu, sadece doğrudan her içine sabit değerleri takmayı en iyi duruma engeller dizi elemanları, başlatmak için çalışma zamanı tam sayı değerleri üretir std::cout <<çağrı.

Gcc 6.2 için, -O3 kullanarak, her iki sürüm main()de aynı derlemeyi oluşturur ( karşılaştırmak için ilki arasında #if 1ve #if 0öncesinde geçiş yapınmain() ): https://godbolt.org/g/QqRWZb

#include <iostream>
#include <ctime>

template <typename T>
class Proxy {
public:
    T &a, &b, &c;
    Proxy(T* par) : a(par[0]), b(par[1]), c(par[2]) {}
    Proxy* operator -> () { return this; }
};

struct Data {
    int ar[3];
    template <typename I> int& operator [] (I idx) { return ar[idx]; }
    template <typename I> const int& operator [] (I idx) const { return ar[idx]; }
    Proxy<int>       operator -> ()       { return Proxy<int>(ar); }
    Proxy<const int> operator -> () const { return Proxy<const int>(ar); }
    int* begin()             { return ar; }
    const int* begin() const { return ar; }
    int* end()             { return ar + sizeof(ar)/sizeof(int); }
    const int* end() const { return ar + sizeof(ar)/sizeof(int); }
};

// Nci returns an unpredictible int
inline int Nci() {
    static auto t = std::time(nullptr) / 100 * 100;
    return static_cast<int>(t++ % 1000);
}

#if 1
int main() {
    Data d = {Nci(), Nci(), Nci()};
    for(auto v : d) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << d->b << "\n";
    d->b = -5;
    std::cout << d[1] << "\n";
    std::cout << "\n";

    const Data cd = {Nci(), Nci(), Nci()};
    for(auto v : cd) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << cd->c << "\n";
    //cd->c = -5;  // error: assignment of read-only location
    std::cout << cd[2] << "\n";
}
#else
int main() {
    Data d = {Nci(), Nci(), Nci()};
    for(auto v : d.ar) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << d.ar[1] << "\n";
    d->b = -5;
    std::cout << d.ar[1] << "\n";
    std::cout << "\n";

    const Data cd = {Nci(), Nci(), Nci()};
    for(auto v : cd.ar) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << cd.ar[2] << "\n";
    //cd.ar[2] = -5;
    std::cout << cd.ar[2] << "\n";
}
#endif

Şık. Esas olarak, bunun optimize edildiğini kanıtladığınız için oy verildi. BTW, bunu main()zamanlama fonksiyonları ile bir bütün olarak değil, çok basit bir fonksiyon yazarak çok daha kolay yapabilirsiniz ! örneğin int getb(Data *d) { return (*d)->b; }sadece mov eax, DWORD PTR [rdi+4]/ ret( godbolt.org/g/89d3Np ) olarak derlenir . (Evet, Data &dsözdizimini kolaylaştırırdı, ancak ->bu şekilde aşırı yüklemenin tuhaflığını vurgulamak için ref yerine bir işaretçi kullandım .)
Peter Cordes

Her neyse, bu harika. Gibi diğer fikirler int tmp[] = { a, b, c}; return tmp[idx];ortadan kalkmaz, bu yüzden bunun yapması çok güzel.
Peter Cordes

operator.C ++ 17'yi kaçırmamın bir nedeni daha var .
Jens

2

Değerleri okumak yeterliyse ve verimlilik bir sorun değilse veya derleyicinize işleri iyi optimize etmesine güveniyorsanız veya yapı sadece 3 bayt ise, bunu güvenle yapabilirsiniz:

char index_data(const struct data *d, size_t index) {
  assert(sizeof(*d) == offsetoff(*d, c)+1);
  assert(index < sizeof(*d));
  char buf[sizeof(*d)];
  memcpy(buf, d, sizeof(*d));
  return buf[index];
}

Yalnızca C ++ sürümü için, muhtemelen standart düzene sahip static_assertolduğunu doğrulamak struct datave bunun yerine geçersiz dizine istisna atmak için kullanmak isteyeceksiniz .


1

Yasadışı, ancak bir çözüm var:

struct data {
    union {
        struct {
            int a;
            int b;
            int c;
        };
        int v[3];
    };
};

Şimdi v:


6
Birçok c ++ projesi, her yerde alt yönetimin iyi olduğunu düşünüyor. Yine de kötü uygulamaları vaaz etmemeliyiz.
StoryTeller - Unslander Monica

2
Sendika, her iki dilde de katı örtüşme sorununu çözüyor. Ancak sendikalar aracılığıyla punning türü yalnızca C'de iyidir, C ++ 'da değil.
Lundin

1
yine de, bu tüm c ++ derleyicilerinin% 100'ünde çalışırsa şaşırmam. hiç.
Sven Nilsson

1
En agresif optimizasyon ayarlarıyla gcc'de deneyebilirsiniz.
Lundin

1
@Lundin: Union type punning, GNU C ++ 'da ISO C ++ üzerinde bir uzantı olarak yasaldır . Kılavuzda çok net ifade edilmiyor gibi görünüyor , ancak bundan oldukça eminim. Yine de bu cevabın nerede geçerli olduğunu ve nerede olmadığını açıklaması gerekiyor.
Peter Cordes
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.