Sorunun cevabı
Geçmiş: C++11'de tanıtılan std::initializer_list, C tarzı agregat başlatmayı modern C++ kapsayıcı yapıcılarıyla birleştirmek için tasarlanmıştır. Derleyici tarafından üretilen const elemanlardan oluşan bir diziye işaret eden iki gösterici (veya bir gösterici ve bir boyut) içeren hafif bir agregat olarak uygulanmıştır. Bu tasarım, sabit listelerin std::vector'un yapıcıları gibi fonksiyonlara sıfır aşım ile geçirilmesini önceliklendirir.
Sorun: Temel dizi, std::initializer_list'in oluşturulduğu tam ifadenin yaşam döngüsüne bağlı olan geçici bir nesnedir. Bir sınıf std::initializer_list'i kendisi depoladığında, yalnızca serbest bırakılan yığın belleğine işaret eden göstericileri korur. Sonraki herhangi bir erişim belirsiz davranış yaratır ve bu da yeniden üretilmesi zor olan çöp veriler veya çöküşler olarak ortaya çıkar.
Çözüm: Asla std::initializer_list'i bir sınıf üyesi olarak saklamayın; bunun yerine, öğeleri std::vector veya std::array gibi bir sahiplik kapsayıcısına hevesle kopyalayın. Eğer sıfır kopyalama kritikse, dışarıdan yönetilen depolama ile std::span (C++20) kullanın veya aralığı yineleyiciler aracılığıyla kabul edin. Bu, verilerin yapıcı çağrısının ömrünü aşmasını ve nesnenin yaşam döngüsü için geçerli kalmasını garanti eder.
class Bad { std::initializer_list<int> list_; public: Bad(std::initializer_list<int> list) : list_(list) {} // TEHLİKE int sum() const { int s = 0; for (int i : list_) s += i; // UB: sarkmış göstericiler return s; } }; class Good { std::vector<int> vec_; public: Good(std::initializer_list<int> list) : vec_(list) {} // Güvenli: veriyi kopyalar int sum() const { return std::accumulate(vec_.begin(), vec_.end(), 0); } };
Hayattan bir durum
Bir yüksek frekanslı ticaret konfigürasyon yükleyicisinde, bir MarketConfig sınıfı, MarketConfig cfg{{1.0, 2.0, 3.0}} gibi sözdizimini desteklemek için yapıcısında varsayılan fiyat katmanlarını bir başlatıcı listesi aracılığıyla kabul etti. Bir junior geliştirici, "yığın tahsisini önlemek" amacıyla std::initializer_list<double>'ı doğrudan bir üye olarak sakladı ve paket işleme sırasında daha sonra katmanlar üzerinde yineleme yapmak istedi.
Bir önerilen çözüm, çağrıcı tarafından geçen bir const std::vector<double>& saklamaktı. Bu, çağrıcının vektörün yaşam süresini koruması durumunda kopyaları ortadan kaldırırdı, ancak kapsüllemeyi ihlal eder ve çağrıcıları geçici listeler için kalıcı depolama yönetimine zorlar. Diğer bir seçenek, katman sayısını derleme zamanında bilmeyi gerektiren bir şablon parametresi olarak std::array<double, N> kullanmaktı, bu ise dinamik olarak JSON yerinden yüklenen konfigürasyonlarda imkansızdı.
Seçilen yaklaşım, başlatıcı listesini oluşturma sırasında hemen bir std::vector<double> üyesine kopyalamaktı. Bu, katman verisinin tek bir tahsis ve kopyalamasını gerektirse de, yapılandırma durumunun güvenliğini ve değişmezliğini garanti etti. Değişiklikten sonra, üretim simülasyon ortamlarında aralıklı çöküşler ortadan kayboldu ve Valgrind artık katman toplama sırasında "başlatılmamış değerin boyutu 8 kullanımı" rapor etmedi.
Adayların sıklıkla gözden kaçırdığı noktalar
Bir std::initializer_list'i const referansa bağlamanın neden sınıf üyesi olarak saklandığında arka plandaki dizinin sarkmasini önlemediği nedir?
Standart, std::initializer_list'in arka plan dizisinin, mevcut kapsamda bir referansa bağlanarak yalnızca başlatıcı_list nesnesinin kendi yaşam döngüsüyle uzatıldığını belirtir. Bir std::initializer_list'i yapıcıya değer olarak geçirdiğinizde, geçici dizi yapıcı dönerken kadar yaşar; listeyi bir üye içine kopyalamak sadece gösterici çiftini çoğaltır. Sonuç olarak, üye, oluşturma ifadesinin sonlandığı anda geri alınan yığın alanına işaret eder, orijinal argümanın nasıl bağlandığına bakılmaksızın.
"Başlatıcı liste yapıcısı kazanır" kuralı, std::vector'ün yapıcı aşırı yükleme kümesi ile nasıl etkileşir ve neden std::vector<int>(5, 10) ile std::vector<int>{5, 10} farklıdır?
Doğrudan liste başlatma (parantezlerle) için aşırı yükleme çözümlemesi sırasında, C++, argüman listesi başlatıcı listenin eleman türüne doğrudan dönüştürülebiliyorsa, std::initializer_list'i alan yapıcıları önceliklendirir. std::vector<int> için, {5, 10} initializer_list<int> yapıcısını seçer, iki eleman (5 ve 10) içeren bir vektör oluşturur. Buna karşılık, parantezler (5, 10) size_t, const int& yapıcısını seçer, on tanımlanmış eleman içeren bir vektör oluşturur. Adaylar genellikle bu önceliğin, normal aşırı yükleme çözümleme kurallarında daha iyi bir eşleşme olmasına rağmen geçerli olduğunu gözden kaçırır.
constexpr fonksiyonları std::initializer_list'i güvenli bir şekilde döndürebilir mi ve evet ise, hangi depolama süresi kısıtlamaları altında?
constexpr fonksiyonları std::initializer_list döndürebilir, ancak arka plandaki dizi, fonksiyonun çalışma zamanında çağrılması durumunda otomatik depolama ömrüne sahiptir. Fonksiyon bir sabit ifade bağlamında çağrıldığında, dizi genellikle statik salt okunur bellekte depolanır, bu durumda güvenlidir. Ancak, çalışma zamanı argümanları ile çağrılan bir constexpr fonksiyonundan std::initializer_list döndürmek, işlev keşfi bittiğinde yine sarkık göstericilere yol açar, ta tıpkı constexpr olmayan fonksiyonlarla olduğu gibi. Adaylar sıklıkla constexpr ile "statik depolama"yı karıştırır ve döndürülen listenin her zaman sonsuza dek geçerli olduğunu yanlış bir şekilde varsayar.