C ++: Akıllı işaretçiler, Ham işaretçiler, İşaretçiler yok mu? [kapalı]


48

C ++ 'da oyun geliştirme kapsamında, işaretçilerin kullanımında tercih ettiğiniz kalıplar hangileridir (hiç biri, ham, kapsamlı, paylaşılmış veya başka türlü akıllı ve dilsiz)?

Düşünebilirsin

  • nesne mülkiyeti
  • kullanım kolaylığı
  • kopya politikası
  • havai
  • döngüsel referanslar
  • hedef platform
  • kaplarla kullanın

Yanıtlar:


32

Çeşitli yaklaşımları denedikten sonra bugün kendimi Google C ++ Stil Kılavuzuyla uyumlu buluyorum :

İşaretçi anlambilimine gerçekten ihtiyacınız varsa, scoped_ptr harikadır. Std :: tr1 :: shared_ptr dosyasını, nesnelerin STL kapları tarafından tutulması gerektiğinde olduğu gibi çok özel koşullar altında kullanmalısınız. Asla auto_ptr kullanmamalısınız. [...]

Genel olarak konuşursak, kodun net nesne sahipliği ile kod tasarlamayı tercih ediyoruz. En net nesne sahipliği, bir işaretçi kullanmadan doğrudan bir alan veya yerel değişken olarak bir nesneyi kullanarak elde edilir. [..]

Önerilmemelerine rağmen, referans sayılan işaretçiler bazen bir sorunu çözmenin en basit ve en şık yoludur.


14
Bugün, scoped_ptr yerine std :: unique_ptr kullanmak isteyebilirsiniz.
Klaim

24

Ayrıca "güçlü mülkiyet" düşünce trenini takip ediyorum. Uygun olduğunda, "bu sınıfın bu üyeye sahip olduğunu" açıkça ifade etmeyi seviyorum.

Nadiren kullanırım shared_ptr. Yaparsam, weak_ptrne zaman yapabilirsem serbestçe kullanırım , böylece referans sayısını artırmak yerine nesneye tutacağı gibi davranabilirim.

Her scoped_ptryerde kullanıyorum. Bariz sahiplik gösterir. Bir üyeye sadece böyle nesneler yapmamamın tek sebebi, eğer bir skop_ptr'de olduklarını bildirmek için onları ileriye götürebilirsin.

Bir nesne listesine ihtiyacım olursa kullanırım ptr_vector. Daha verimli ve kullanmaktan daha az yan etkisi var vector<shared_ptr>. Sanırım ptr_vector içindeki yazıyı bildirmeyi başaramayacağınızı düşünüyorum (bir süre oldu), ancak bunun anlambilimi bence buna değer. Temel olarak listeden bir nesneyi kaldırırsanız, otomatik olarak silinir. Bu aynı zamanda bariz mülkiyeti de göstermektedir.

Bir şeye atıfta bulunmam gerekiyorsa, onu çıplak bir işaretçi yerine referans yapmaya çalışıyorum. Bazen bu pratik değildir (yani, nesne oluşturulduktan sonra bir referansa ihtiyacınız olduğunda). Her iki durumda da, referanslar açıkça nesnenin size ait olmadığını ve başka yerlerde paylaşılan işaretçi anlambilimini takip ediyorsanız çıplak işaretçiler genellikle herhangi bir karışıklığa neden olmaz (özellikle "el ile silmeyi kaldırma" kuralını izlerseniz) .

Bu yöntemle, üzerinde çalıştığım bir iPhone oyunu yalnızca tek bir deleteçağrı yapabiliyordu ve bu yazı Obj-C'den C ++ köprüsüne yazıyordu.

Genel olarak hafıza yönetiminin insanlara bırakılamayacak kadar önemli olduğu kanısındayım. Silme işlemini otomatik hale getirebilirseniz, yapmalısınız. Paylaşılan_ptr ek yükü çalışma zamanında çok pahalıysa (diş açma desteğini kapattığınızı varsayarsak), dinamik ayırma işlemlerinizi düşürmek için muhtemelen başka bir şey kullanıyor olmalısınız.


1
Mükemmel bir özet. Gerçekten smart_ptr'den bahsettiğinizin aksine paylaşılan_ptr mi demek istiyorsunuz?
jmp97

Evet, paylaşılan_ptr demek istedim. Bunu düzelteceğim.
Tetrad

10

İş için doğru aracı kullanın.

Programınız istisnalar atabiliyorsa, kodunuzun istisna olduğunun farkında olun. Akıllı işaretçiler kullanarak, RAII ve 2 fazlı yapımdan kaçınmak iyi bir başlangıç ​​noktasıdır.

Açık bir mülkiyet semantiği olmayan döngüsel referanslarınız varsa, bir çöp toplama kütüphanesi kullanmayı veya tasarımınızı yeniden düzenlemeyi düşünebilirsiniz.

İyi kütüphaneler, konsepte tip değil kodlama yapmanıza izin verir, bu nedenle çoğu durumda kaynak yönetimi sorunlarının ötesinde ne tür bir işaretçi kullandığınız önemli değildir.

Çok iş parçacıklı bir ortamda çalışıyorsanız, nesnenizin potansiyel olarak iş parçacıkları arasında paylaşılıp paylaşılmadığını anladığınızdan emin olun. Boost :: shared_ptr veya std :: tr1 :: shared_ptr komutunu kullanmayı düşünmenizin ana nedenlerinden biri, iş parçacığı güvenli bir başvuru sayımı kullanmasıdır.

Referans sayımlarının ayrı tahsis edilmesinden endişe ediyorsanız, bunun çevresinde birçok yol vardır. Boost :: shared_ptr kütüphanesini kullanarak referans sayaçlarını tahsis edebilir ya da nesneyi ve referans sayısını tek bir tahsis içinde tahsis eden boost :: make_shared (tercihim) özelliğini kullanabilir, böylece çoğu önbellek sorununu gideririz. En yüksek seviyede nesneye bir referans tutarak ve nesneye doğrudan referanslar geçirerek, referans sayısını performans kritik kodunda güncellemenin performans vuruşunu önleyebilirsiniz.

Paylaşılan sahipliğe ihtiyacınız varsa ancak referans sayma veya çöp toplama maliyetini ödemek istemiyorsanız, değişmez nesneler veya yazma deyiminde bir kopya kullanmayı düşünün.

En yüksek performans kazanımlarınızın uzağa ve uzağa bir mimaride, ardından bir algoritma düzeyinde olacağına ve bu düşük seviyeli endişelerin çok önemli olmasına rağmen, sadece önemli sorunları ele aldıktan sonra ele alınmaları gerektiğini unutmayın. Önbellek özü düzeyindeki performans sorunlarıyla uğraşıyorsanız, söz konusu başına göstericilerle hiçbir ilgisi olmayan, aynı şekilde sahte paylaşımın farkında olmanız gereken birçok konunuz vardır.

Akıllı işaretçileri yalnızca dokular veya modeller gibi kaynakları paylaşmak için kullanıyorsanız, Boost.Flyweight gibi daha özel bir kütüphaneyi düşünün.

Yeni standart kabul edildiğinde, semantiği hareket ettirin, referansları değerleyin ve mükemmel iletme pahalı nesnelerle ve konteynerlerle çalışmayı çok daha kolay ve verimli hale getirecek. O zamana kadar işaretçileri, auto_ptr veya unique_ptr gibi yıkıcı kopya semantiklerine sahip bir Konteynerde (standart konsept) saklamayın. Boost.Pointer Container kitaplığını kullanmayı veya paylaşılan sahiplik akıllı işaretçilerini Konteynerler'de depolamayı düşünün. Performans kritik kodunda, her ikisinden de kaçınmayı Boost.Intrusive gibi izinsiz konteynerlerden yana düşünebilirsiniz.

Hedef platform kararınızı gerçekten fazla etkilememeli. Gömülü cihazlar, akıllı telefonlar, aptal telefonlar, PC'ler ve konsollar tüm kodu çalıştırabilir. Sıkı bellek bütçeleri ya da yükleme sonrasında / sonrasında dinamik ayırma olmaması gibi proje gereksinimleri daha geçerli kaygılardır ve seçimlerinizi etkilemelidir.


3
Konsollarda istisna kullanımı biraz tehlikeli olabilir - özellikle XDK istisnai bir düşmanlıktır.
Crashworks

1
Hedef platform tasarımınızı gerçekten etkilemeli. Verilerinizi dönüştüren donanımın bazen kaynak kodunuz üzerinde büyük etkileri olabilir. PS3 mimarisi, donanımınızı, kaynağınızı ve bellek yönetiminizi ve ayrıca işleyicinizi tasarlamak için kullanmanız gereken somut bir örnektir.
Simon

Özellikle GC konusunda sadece hafifçe aynı fikirde değilim. Döngüsel referanslar çoğu zaman referans sayılan şemaları için bir problem değildir. Genellikle bu döngüsel mülkiyet sorunları ortaya çıkar, çünkü insanlar nesnelerin mülkiyeti hakkında doğru bir şekilde düşünmüyorlardı. Bir nesnenin bir şeye işaret etmesi gerektiği için, bu işaretçiye sahip olması gerektiği anlamına gelmez. Yaygın olarak belirtilen örnek, ağaçlardaki işaretçilerdir, ancak ağaçtaki işaretçinin ebeveyni, güvenlikten ödün vermeden güvenli bir şekilde ham işaretçi olabilir.
Tim Seguine,

4

C ++ 0x kullanıyorsanız, kullanın std::unique_ptr<T>.

Tepegöz std::shared_ptr<T>sayısının referans sayma sayısının aksine, performans ek yükü yoktur . Bir benzersiz_ptr imlecine sahiptir ve sahipliğini C ++ 0x hareket anlamıyla aktarabilirsiniz . Onları kopyalayamazsınız - yalnızca taşıyın.

Ayrıca, örneğin std::vector<std::unique_ptr<T>>ikili uyumlu olan ve performansla aynı olan std::vector<T*>, ancak elemanları silerseniz veya vektörü temizlerseniz hafıza sızdırmaz. Bu aynı zamanda STL algoritmaları ile olduğundan daha iyi bir uyumluluğa sahiptir ptr_vector.

IMO birçok amaç için bu ideal bir konteynırdır: rasgele erişim, istisnai güvenli, bellek sızıntılarını önler, vektör yeniden tahsisi için düşük ek yük (sahnelerin arkasındaki işaretçilerin etrafındaki karıştırmalar). Birçok amaç için çok faydalı.


3

Hangi sınıfların hangi işaretçilere sahip olduğunu belgelemek iyi bir uygulamadır. Tercihen, normal nesneler kullanırsınız ve ne zaman istersen işaretçiler kullanmazsınız.

Ancak, kaynakları takip etmeniz gerektiğinde, işaretçileri geçmek tek seçenektir. Bazı durumlar var:

  • İşaretçiyi başka bir yerden aldınız, ancak yönetmeyin: sadece normal bir işaretçi kullanın ve onu silmeye çalıştıktan sonra kodlayıcı olmaması için belgeleyin.
  • İşaretçiyi başka bir yerden alıyorsunuz ve izini sürüyorsunuz: bir scoped_ptr kullanın.
  • İşaretçiyi başka bir yerden aldınız ve izini sürüyorsunuz ancak silmek için özel bir yönteme ihtiyacı var: shared_ptr işlevini özel bir silme yöntemiyle kullanın.
  • İşaretçiye bir STL kabında ihtiyacınız var: etrafına kopyalanacak, böylece boost :: shared_ptr dosyasına ihtiyacınız olacak.
  • Birçok sınıf imleci paylaşır ve kimin sileceği belli değildir: shared_ptr (yukarıdaki durum aslında bu noktanın özel bir halidir).
  • İşaretçiyi kendiniz yaratırsınız ve sadece ihtiyacınız olur: normal bir nesneyi gerçekten kullanamıyorsanız: scoped_ptr.
  • İşaretçiyi yaratırsınız ve diğer sınıflarla paylaşırsınız: shared_ptr.
  • İşaretçiyi yaratıp geçersiniz: normal bir işaretçi kullanın ve arayüzünüzü belgeleyin, böylece yeni mal sahibi kaynağı kendisi yönetmesi gerektiğini bilir!

Bunun şu anda kaynaklarımı nasıl yönettiğimi kapsadığını düşünüyorum. Shared_ptr gibi bir göstericinin bellek maliyeti genellikle normal bir göstericinin bellek maliyetinin iki katıdır. Bu ek yükün çok büyük olduğunu düşünmüyorum, ancak kaynakları azsanız, akıllı işaretçilerin sayısını azaltmak için oyununuzu tasarlamayı düşünmelisiniz. Diğer durumlarda sadece yukarıdaki mermiler gibi iyi prensiplere göre tasarlarım ve profilci bana nerede daha fazla hıza ihtiyacım olacağını söyleyecek.


1

Özellikle işaretçilere destek olmak söz konusu olduğunda, uygulamaları tam olarak ihtiyacınız olan şey olmadıkça, bunlardan kaçınılması gerektiğini düşünüyorum. Başlangıçta beklenenden daha büyük bir maliyetle geliyorlar. Hafızanızın hayati ve önemli kısımlarını atlamanıza ve kaynak yönetimi yapmanıza izin veren bir arayüz sağlarlar.

Herhangi bir yazılım geliştirmeye gelince, verilerinizi düşünmenin önemli olduğunu düşünüyorum. Verilerinizin bellekte nasıl temsil edildiği çok önemlidir. Bunun nedeni, CPU hızının bellek erişim zamanından çok daha yüksek bir oranda artmasıdır. Bu genellikle bellek önbelleklerini çoğu modern bilgisayar oyununun ana darboğazı yapar. Verilerinizin erişim sırasına göre bellekte doğrusal olarak hizalanması önbellek için çok daha uygundur. Bu tür çözümler genellikle daha temiz tasarımlara, daha basit kodlara ve kesinlikle daha kolay hata ayıklama koduna neden olur. Akıllı işaretçiler kolayca kaynakların sık sık dinamik bellek ayırmalarına yol açar, bu da onların tüm belleğe dağılmalarına neden olur.

Bu erken bir optimizasyon değil, mümkün olan en erken zamanda alınabilecek ve alınması gereken sağlıklı bir karardır. Bu, yazılımınızın çalışacağı donanımı mimari olarak anlama meselesidir ve önemlidir.

Düzenleme: Paylaşılan işaretçilerin performansıyla ilgili dikkate alınması gereken birkaç şey vardır:

  • Referans sayacı ayrılan öbek.
  • İplik güvenliği etkin kullanıyorsanız, referans sayımı birbirine kenetlenmiş işlemlerle yapılır.
  • İşaretçiyi değere göre geçirmek referans sayısını değiştirir, bu da büyük olasılıkla bellekte rasgele erişim kullanan kilitlenen işlemler anlamına gelir (kilitler + önbellek kaçırması).

2
Beni ne pahasına olursa olsun kaçınıyorsun 'de kaybettin. O zaman, nadiren gerçek dünya oyunları için endişe verici bir optimizasyon türü tanımlamaya devam edersiniz. Oyun geliştirme işlemlerinin çoğu, CPU önbellek performansının eksikliği ile değil geliştirme sorunları (gecikmeler, hatalar, oynanabilirlik vb.) İle karakterize edilir. Bu yüzden bu tavsiyenin erken bir optimizasyon olmadığı fikrine kesinlikle katılmıyorum.
kevin42

2
Veri düzeninin erken tasarımına katılıyorum. Modern bir konsoldan / mobil cihazdan herhangi bir performans elde etmek önemlidir ve asla gözden kaçırılmaması gereken bir şeydir.
Olly

1
Bu, üzerinde çalıştığım AAA stüdyolarından birinde gördüğüm bir konudur. Ayrıca Insomniac Games, Mike Acton'daki Baş Mimar'ı da dinleyebilirsiniz. Desteğin kötü bir kütüphane olduğunu söylemiyorum, sadece yüksek performanslı oyunlar için uygun değil.
Simon,

1
@ kevin42: Önbellek tutarlılığı, bugün oyun geliştirmede muhtemelen düşük seviye optimizasyonların ana kaynağıdır. @Simon: Çoğu paylaşılan_ptr uygulamaları, Linux ve Windows PC'leri içeren karşılaştır ve takas etmeyi destekleyen herhangi bir platformda kilitlenmekten kaçınır ve Xbox'ı içerdiğine inanıyorum.

1
@Joe Wreschnig: Bu doğru, önbelleklerin özeti, paylaşılan bir işaretçinin herhangi bir şekilde başlatılmasına neden olsa da (kopya, zayıf işaretçiden oluştur, vb.) Hala muhtemel. Modern PC'lerdeki L2 önbellekleme özelliği 200 döngü gibidir ve PPC'de (xbox360 / ps3) daha yüksektir. Yoğun bir oyunla, her oyun nesnesinin ölçeklendirilmelerinin büyük bir endişe kaynağı olduğu sorunlara baktığımızda oldukça az kaynağa sahip olabileceği göz önüne alındığında, 1000'e kadar oyun nesnesine sahip olabilirsiniz. Bu, bir geliştirme döngüsünün sonunda sorunlara neden olur (yüksek miktarda oyun nesnesi vuracağınız zaman).
Simon,

0

Her yerde akıllı işaretçiler kullanma eğilimindeyim. Bunun tamamen iyi bir fikir olup olmadığından emin değilim, ancak tembelim ve bazı C-tipi işaretçi aritmetik yapmak istemediğim hariç hiçbir dezavantajı göremiyorum. Boost :: shared_ptr kullanıyorum çünkü etrafına kopyalayabileceğimi biliyorum - eğer iki varlık bir görüntüyü paylaşıyorsa, biri ölürse diğeri de görüntüyü kaybetmemelidir.

Bunun dezavantajı, bir nesnenin işaret ettiği ve sahibi olduğu bir şeyi silmesi, ancak başka bir şeyin de onu işaret etmesidir, o zaman silinmez.


1
Neredeyse her yerde share_ptr kullanıyorum - ama bugün bir parça veri için gerçekten paylaşılan sahipliğe ihtiyacım olup olmadığını düşünmeye çalışıyorum. Aksi takdirde, bu verileri üst veri yapısına gösterici olmayan bir üye yapmak makul olabilir. Bu net mülkiyetin tasarımları basitleştirdiğini biliyorum.
jmp97

0

İyi akıllı işaretçiler tarafından sağlanan bellek yönetimi ve belgelerin yararları, onları düzenli kullandığım anlamına geliyor. Bununla birlikte, profiler yükselip bana belirli bir kullanımın bana pahalıya mal olduğunu söylerken, daha neolitik işaretçi yönetimine geri döneceğim.


0

Ben yaşlıyım, oldskool ve bir bisiklet sayacı. Kendi işimde ham işaretçiler kullanıyorum ve çalışma zamanında dinamik tahsisler kullanmıyorum (havuzların kendisi hariç). Her şey bir araya toplanmış ve sahiplik çok katı ve asla devredilemez, gerçekten gerekirse özel bir küçük blok ayırıcı yazarım. Oyun sırasında her havuzun kendi kendini temizleyebileceği bir durum olduğundan eminim. İşler kıllı hale geldiğinde nesneleri tutamaçlara sardığım için onları yerlerine yerleştirebilirim, ama istemem. Konteynerler özel ve son derece çıplak kemiklerdir. Ayrıca kodu tekrar kullanmıyorum.
Tüm akıllı işaretçilerin, kapların ve yineleyicilerin erdemlerini asla tartışmasam da, ne olmasa da, son derece hızlı bir şekilde kodlayabildiğim için (ve makul derecede güvenilir) bilinir, ancak başkaları için bazı belirgin nedenlerle koduma atlamaları tavsiye edilmese de, kalp krizi ve sürekli kabuslar gibi).

İş yerinde, elbette, prototip oluşturmadığım sürece, çok şükrettiğim çok şey farklı.


0

Neredeyse hiçbiri bu kuşkusuz garip bir cevap olmasına rağmen ve muhtemelen herkes için uygun hiçbir yere yakın değildir.

Ancak, kişisel durumumda, belirli bir türün tüm örneklerini merkezi, rastgele erişimli bir dizide (iş parçacığı güvenli) ve bunun yerine 32 bitlik dizinlerle (göreceli adresler, vb.) Depolamayı çok daha faydalı buldum. mutlak işaretçiler yerine.

Başlangıç ​​için:

  1. 64 bit platformlarda analog işaretçinin bellek gereksinimini yarıya indirir. Şimdiye kadar hiçbir zaman belirli bir veri türünün ~ 4.29 milyar örneğinden fazlasına ihtiyacım olmadı.
  2. Belirli bir türdeki tüm örneklerin Thiçbir zaman belleğe fazla dağılmamasını sağlar. Bu, düğümler işaretçiler yerine endeksler kullanılarak birbirine bağlanırsa, ağaçlar gibi bağlantılı yapıların arasında bile gezerken, her türlü erişim modeli için önbellek kayıplarını azaltma eğilimindedir.
  3. Paralel veri, ağaçlar veya karma tablolar yerine ucuz paralel diziler (veya seyrek diziler) kullanarak ilişkilendirmek kolaylaşır.
  4. Set kesişmeleri doğrusal zamanda bulunabilir veya paralel bir bit kullanarak diyelim.
  5. Endeksleri radix olarak sıralayabilir ve önbellek dostu sıralı erişim modeli elde edebiliriz.
  6. Belirli bir veri türünden kaç tane tahsis edildiğini izleyebiliriz.
  7. Bu tür bir şeye önem veriyorsanız, istisnai güvenlik gibi şeylerle ilgilenmesi gereken yerlerin sayısını azaltır.

Bununla birlikte, kolaylık hem güvenli hem de tip emniyettir. Biz bir örneğini erişemiyor Terişimi kalmadan hem konteyner ve endeks. Eski düz bir kişi int32_tbize hangi veri türünden bahsettiği hakkında hiçbir şey söylemez, bu nedenle tür güvenliği yoktur. Yanlışlıkla Barbir dizin kullanarak bir erişmeyi deneyebiliriz Foo. İkinci sorunu hafifletmek için genellikle bu tür şeyler yapıyorum:

struct FooIndex
{
    int32_t index;
};

Bu aptalca görünüyor ama bana tür güvenliğini geri veriyor, böylece insanlar yanlışlıkla bir derleyici Bararacılığıyla Foobir derleyici hatası olmadan erişmeyi deneyemezler . Rahatlık açısından, sadece hafif rahatsızlığı kabul ediyorum.

İnsanlar için büyük sıkıntı yaratabilecek başka bir şey de, OOP tarzı kalıtım temelli polimorfizmi kullanamamamdır, çünkü bu, farklı boyut ve hizalama gereklilikleri olan her türlü farklı alt tiplere işaret edebilecek bir temel işaretçi gerektirecektir. Ancak bugünlerde kalıtım çok fazla kullanmıyorum - ECS yaklaşımını tercih ediyorum.

Gelince shared_ptr, çok fazla kullanmamaya çalışıyorum. Bilmiyorum çoğu zaman mülkiyeti paylaşmanın bir anlamı yoktur ve bu yüzden dehşet verici bir şekilde yapmak mantıksal sızıntılara yol açabilir. Çoğu zaman en azından üst düzeyde, bir şey bir şeye ait olma eğilimindedir. Sık sık kullanmaya shared_ptrbaşladığım yerde, gerçekten sahiplikle fazla ilgilenmeyen yerlerdeki bir nesnenin ömrünü uzatmaktaydı, sadece iş parçacığı bitmeden önce nesnenin yok edilmemesini sağlamak için bir iş parçacığındaki yerel bir işlev gibi onu kullanarak.

Bu sorunun üstesinden gelmek için, shared_ptrGC veya bunun gibi bir şey kullanmak yerine, genellikle bir iş parçacığı havuzundan çalışan kısa ömürlü görevleri tercih ederim ve bu iş parçacığı bir nesneyi imha etmek isterse, gerçek imha güvenli bir şekilde ertelenir; sistemin, herhangi bir ipliğin bahsedilen nesne tipine erişmesi gerekmediğinden emin olabileceği süre.

Hala bazen ref-saymayı kullanıyorum ama son çare stratejisi gibi davranıyorum. Kalıcı bir veri yapısının uygulanması gibi mülkiyeti paylaşmanın gerçekten mantıklı olduğu birkaç durum var ve orada hemen ulaşmak için mükemmel bir anlam ifade ediyor shared_ptr.

Her neyse, çoğunlukla endeksleri kullanıyorum ve hem ham hem de akıllı göstergeleri az kullanıyorum. Nesnelerinizin bitişik olarak depolandığını ve hafıza alanı üzerine dağılmadığını bildiğiniz zaman açtıkları indeksleri ve kapı türlerini seviyorum.

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.