1 puan yazan GN⁺ 2024-07-06 | 1 yorum | WhatsApp'ta paylaş
  • C++'ta nesne başlatma, T t;, T t{};, T t{...} gibi benzer sözdizimlerinde bile default/value/list/aggregate initialization olarak ayrılır ve gerçek üye durumları farklı olabilir
  • Varsayılan oluşturucunun nerede = default edildiğine bağlı olarak zero-initialization uygulanıp uygulanmayacağı değişir; sınıf dışında tanımlanırsa üyeler başlatılmamış değerlerle kalabilir
  • const int üyesi olduğu için varsayılan oluşturucusu silinecek gibi görünen bir tür bile aggregate ise A a{};, aggregate initialization üzerinden 0 ile başlatılabilir
  • C++20'deki parantez tabanlı aggregate initialization, süslü parantezle başlatmaya göre daha izin vericidir; narrowing conversion ve dangling reference gibi farklar yaratabilir
  • Pratikte, örtük oluşturucular ile başlatma kurallarının birleşimine güvenmek yerine başlatma niyetini doğrudan oluşturucularla göstermek istisnalardan kaçınmayı kolaylaştırır

C++ başlatma sözdiziminin ayrıldığı noktalar

  • T t; default-initialization yapar
    • T bir sınıf türüyse ve varsayılan oluşturucusu varsa bu oluşturucuyu çalıştırır
    • T bir diziyse her elemanı default-initialize eder
    • Diğer durumlarda hiçbir şey yapmaz
  • T t{}; value-initialization gibi görünür, ancak süslü parantez sözdizimi nedeniyle önce list-initialization olarak işlenir
  • Basit bir örnekte bile fark hemen ortaya çıkar
    • int x{}; value-initialized olur ve 0 ile başlatılır
    • int y; default-initialized olur ve değeri başlatılmaz
    • Varsayılan oluşturucusu olan Pair p; ve Pair q{}; oluşturucuyu çağırır
    • Yalnızca üyeleri olan SimplePair r; default-initialized olur ve üyeleri başlatılmamış kalabilir

Varsayılan oluşturucunun bildirim yerinin sonucu değiştirmesi

  • Kullanıcı bir oluşturucu bildirmezse derleyici implicitly-declared bir varsayılan oluşturucu bildirir
  • T() = default; sınıf içinde ilk bildirimde kullanılırsa standart açısından örtük bildirilmiş oluşturucuya oldukça benzer ele alınır
  • Örtük bildirilmiş veya açıkça defaulted edilmiş varsayılan oluşturucu silinmemişse derleyici bir implicitly-defined default constructor sağlar
    • Uygulama açısından boş gövdeye ve boş üye başlatma listesine sahip T() {} ile eşdeğer olmalıdır
  • Aşağıdaki kodda t.x, 0 olur
struct T {
    int x;
    T() = default;
};
T t{};
std::cout << t.x << std::endl;
  • Bunun nedeni, t'nin value-initialized olması ve T'nin varsayılan oluşturucusunun user-provided olmaması nedeniyle önce zero-initialization gerçekleşip ardından varsayılan oluşturucunun çağrılmasıdır

Sınıf dışındaki = default ve user-provided oluşturucu

  • Aynı = default bile sınıf dışında tanımlandığında user-provided oluşturucu olur
struct T {
    int x;
    T();
};
T::T() = default;
T t{};
std::cout << t.x << std::endl;
  • Bu durumda T::T() = default;, ilk bildirimde defaulted edilmiş değil, sınıf dışında defaulted olarak tanımlanmış bir oluşturucudur
  • Value-initialization kuralları, user-provided veya deleted varsayılan oluşturucu varsa default-initialization yapar
  • Dolayısıyla yukarıdaki örnekte zero-initialization olmadan yalnızca varsayılan oluşturucu çalışır; bu oluşturucu hiçbir şey yapmadığı için t.x çöp değer olur

Varsayılan oluşturucunun silindiği durumlar

  • Derleyici makul bir varsayılan oluşturucu üretemezse örtük bildirilmiş varsayılan oluşturucu deleted olarak tanımlanabilir
  • Tipik koşullar şunlardır
    • Statik olmayan başvuru üyesi olması
    • Default-construct veya destruct edilmesi uygun olmayan statik olmayan üye ya da soyut olmayan temel sınıf olması
    • Varsayılan üye başlatıcısı olmayan bir const statik olmayan üye olması ve bu üyenin const-default-constructible olmaması
  • Kullanıcı doğrudan bir oluşturucu sağlarsa derleyici örtük varsayılan oluşturucuyu tanımlamaya çalışmaz ve deleted oluşturucu da üretmez
  • Bu koşullar olsa bile C++ başlatma kuralları genel olarak çok izin verici davranır

Aggregate initialization'ın devreye girdiği an

  • struct A { const int x; }; A a{}; varsayılan oluşturucusu silineceği için derlenmeyecekmiş gibi görünür, ancak gerçekte derlenir ve a.x, 0 olur
  • Kilit nokta A'nın aggregate olmasıdır
  • Aggregate, bir dizi ya da aşağıdaki koşulları sağlayan bir sınıftır
    • user-declared veya inherited oluşturucusu yoktur
    • private/protected doğrudan statik olmayan veri üyesi yoktur
    • private/protected doğrudan temel sınıfı yoktur
    • virtual fonksiyonu veya virtual temel sınıfı yoktur
  • Bir aggregate üzerinde list-initialization yapıldığında, belirli istisnalar dışında aggregate initialization uygulanır
    • Başlatma listesindeki her öğe, aggregate'in her öğesini sırayla başlatır
    • Liste öğesi sayısı yetersizse kalan öğeler, varsayılan üye başlatıcısı varsa onunla başlatılır
    • Varsayılan üye başlatıcısı yoksa ve başvuru değilse boş başlatma listesiyle copy-initialized edilir
  • Bu nedenle A a{}; bir oluşturucu çağrısı değil; a.x'in boş listeyle copy-list-initialized edilip value-initialization üzerinden zero-initialization'a uğradığı bir örnektir

Süslü parantezle başlatmanın ek kuralları

  • T t{...} ve T t = {...} gibi süslü parantez tabanlı sözdizimleri genel olarak list-initializationdır
    • T t{...} direct-list-initialization'dır
    • T t = {...} copy-list-initialization'dır
  • Aggregate olmayan sınıf türlerinde başlatma listesi boşsa ve varsayılan oluşturucu varsa value-initialization yapılır
  • Diğer durumlarda genel overload resolution ile oluşturucular değerlendirilir ve std::initializer_list oluşturucu overload'ları öncelik kazanır
  • Süslü parantezle başlatma, tek öğeyle başlatırken narrowing conversion'a izin vermez
struct A {
    const int x;
};

A b{4};      // mümkün
A c{4.0f};   // narrowing conversion hatası

Parantezle başlatma daha permissive'dir

  • T t(a, b, c); gibi parantezle başlatma direct-non-list-initialization çağırır; direct-list-initialization'a benzer ama farklı kurallara sahiptir
  • T t();, nesne oluşturma değil; argümansız ve dönüş tipi T olan bir fonksiyon bildirimi olarak yorumlanan most vexing parse sorununa sahiptir
  • C++20'den itibaren aggregate'ler için de parantezle başlatma kullanılabilir
  • Parantez tabanlı aggregate initialization, süslü parantez tabanlı başlatmadan farklı çalışır
    • narrowing conversion'a izin verir
    • Kalan öğeler boş listeyle başlatılmak yerine doğrudan value-initialized olur
    • Başvuruya bağlanan geçici nesnenin ömrünü uzatmaz
struct T {
    const int& r;
};
T t(42);
  • Yukarıdaki kodda t.r bir dangling reference olur ve okunursa undefined behaviour'dır

std::initializer_list, kopya oluşturucu, elision

  • Parantezle başlatma, std::initializer_list<T> oluşturucu overload'ı olsa bile T'nin kopya oluşturucusunu çağırabilir
  • Buna karşılık süslü parantezle başlatmada std::initializer_list oluşturucusu öncelik kazanabilir
struct T {
    T(std::initializer_list<T>) {
        std::cout << "list" << std::endl;
    }
    T(const T&) {
        std::cout << "copy" << std::endl;
    }
};

T t{};    // list
T s{t};   // list
T r(t);   // copy
T q(T{}); // list, kopya yok
  • T q(T{}); içinde kopya gerçekleşmemesi, direct- ve copy-initialization'da initializer T türünde bir prvalue ise nesnenin doğrudan o ifadeyle başlatılacağı kuralından kaynaklanır
  • Bu davranış genellikle copy elision olarak bilinir, ancak standart bunu açıkça elision olarak adlandırmaz
  • List-initialization'da aynı elision'ın mümkün olup olmadığına dair standart açıklaması yetersizdir ve CWG issue 2311 bununla ilgilidir
    • GCC ve Clang çoğu durumda elision yapar
    • std::initializer_list<T> oluşturucu overload'ı varsa GCC elision yerine bu oluşturucuyu kullanır

Değerlendirme sırası ve pratik sonuç

  • Parantezle başlatma listesindeki öğelerin değerlendirme sırası garanti edilmez
  • Süslü parantezle başlatma listesi öğeleri kesin olarak soldan sağa değerlendirir
  • Statik değişken başlatma ve constant initialization gibi özel kurallar da vardır; ancak yalnızca sıradan nesne başlatma bile kuralların yeterince karmaşık olmasına yol açar
  • Pratikte, örtük varsayılan oluşturucuya ve başlatma kurallarına güvenmek yerine, başlatma niyetini açıkça belirtmek için doğrudan oluşturucular yazmak daha güvenlidir

1 yorum

 
GN⁺ 2024-07-06
Hacker News görüşleri
  • T için value initialization yapıldığında sonucun 0 olduğu açıklaması doğruydu; ilk başta gözden kaçmıştı ama yazar gerçekten de x için value initialization yapıyordu.
    Genel olarak C++ başlatma kuralları 40 yıl boyunca genişletilip yamalandıkça anlaşılması zor ve sağduyuya aykırı köşeler kazanmış olsa da, yaklaşık %99,9 oranında beklenen şekilde çalıştığını düşünüyorum.
    Büyük iyileştirme, default initialization’ı açık hâle getirmek ve diğer her durumda her zaman value initialization yapmak olurdu diye düşünüyorum. Büyük dizileri 0 ile doldurmanın maliyetinden kaçınmak istendiğinde bunu std::array = void; gibi bir söz dizimiyle açıkça belirtmek daha iyi olurdu.

    • Bir örneği gerçekten başlatırsanız sorun yaşamazsınız; bir constructor çağırmak istiyorsanız da constructor tanımlarsınız.
      Fazla zeki davranıp sonra bunun başa çıkması zor olduğundan şikâyet etmeye benziyor.
  • Tüm yaklaşım yanlış. Bir struct içine const reference koymayın; gerçekten gerekiyorsa std::reference_wrapper kullanmak daha doğru.
    Yanıt biraz ters olsa da asıl nokta, yazının sonucunun tamamen yanlış olması. Elle constructor yazmayın, 5/3/0 kuralını izleyin; const reference tutmanız gerekiyorsa rvalue geçici nesne geçirip geçirmediğinizi kontrol etmelisiniz. Bu o kadar korkutucu bir sorun değil.

    • 5/3/0 kuralı destructor, move constructor ve copy constructor ile ilgilidir. Son örnek hariç, yazı çoğunlukla standart constructor denebilecek şeyin tuhaf davranışlarını ele alıyor.
    • Ben de aynı fikirdeyim. Yazarın bazı çok temel ilke ve yönergeleri bilerek görmezden gelip şikâyet edecek bir şey bulduğu hissi güçlü.
      Sadece temel eğitimlerden geçmiş biri bile böyle yapay senaryolardan kaçınır.
    • std::reference_wrapper’ı hiç kullanmadım ve çalıştığım çeşitli C++ kod tabanlarında da görmedim.
      Derin ve karmaşık template kodlarının bir yerlerinde kullanılıyordur; doğru olabilir ama benim deneyimime göre bu genel geçer bilgi değil.
  • I Have No Mouth, and I Must Scream (1967) metninin aslına buradan ulaşılabilir: https://talesofmytery.blogspot.com/2018/10/harlan-ellison-i-...

    • Harlan Ellison ile bir kez telefonda konuşmuştum. 17 yaşındayken gece 2’de eve telefon geldi; 30 fitlik telefon kablosunu odamın içine çekip kapıyı kapattığım dönemlerdi.
      Martin H Greenberg’in ailesi bizim evde kalıyordu ve o Martin’i arıyordu. Bilimkurgu sevdiğim için kitaplarından birkaçını da okumuştum; adını duyduktan sonra konuşmanın geri kalanını neredeyse bulanık hatırlıyorum. Sonunda Martin’i uyandırıp telefonu ona verdim ve sonrasında uyumakta zorlandım.
  • unless kelimesinin kalın yazıldığı kısımla ilgili iğneleyici yorum olmamasına şaşırdım.
    T t{v0}; ve T t{v0, v1}; gibi güzel bir söz dizimi var ama tek elemanlı başlatma, iki veya daha fazla elemanlı durumlar kadar güvenilir çalışmıyor.
    Parametre paketiyle struct oluşturmayı kolaylaştırma yönüne giden ve bu şekilde başlatılabilen değişken uzunluklu dizi benzeri şeyleri de destekleyen bir dilde bu fark tuhaf. Tür elbette template de olabilir.
    Elle constructor yazabilirsiniz ve yalnızca bir eleman vererek tuple veya array başlatabilirsiniz; ancak özel bazı durumlarda yanlış constructor çağrılabilir. C++11 initializer list’ler yeni çıktığında bunu fark etmiş ve delilik olduğunu düşünmüştüm.

    • Initializer list’ler pek önemli değil. Bazı standart container’ların kullanması dışında neredeyse kimse kullanmıyor; o yüzden o tuhaf özelliği görmezden gelebilirsiniz.
  • C++’ın daha fazla tuhaflığını görmek için C++ FQA’yı öneririm: https://yosefk.com/c++fqa/
    Artık yaklaşık 15 yıllık ama C++ eski özellikleri veya davranışları neredeyse hiç kaldırmadığı için aynı zamanda eskimiyor.

  • Blog teması gerçekten çok güzel. DEC dönemi bilgisayarlarından ilham aldığı açıkça belli oluyor; buna rağmen temiz ve minimalist olduğu için ferah duruyor.

    • Başlığın sağındaki çizgilerin sayfa genişliğine ve satır sayısına tepki verme biçimini sevdim.
  • Berbat. C++’ın korkutucu yanlarından biri. Güzel özellikleri de olan ve çok yetkin insanların üzerinde çalıştığı bir dil olduğu için daha da üzücü.
    Herb’ün C++ syntax 2 gibi bir şeyinin, benim gibi sıradan insanların da kullanabileceği bir dil hâline getirmesini umuyorum.

    • Yazılım geliştirmeyle azıcık uğraşmış biri için pek sorun olmayacak şeyler hakkında fazla abartılı şikâyet ediliyor gibi.
      Yazıdaki örnekler, programcının kendisinin yazmamayı seçtiği özel üye fonksiyonlarını dilin istisnai olarak otomatik üretmesi özelliğinin köşe durumlarına özellikle dokunacak şekilde hazırlanmış.
      C++’ın “kullandığın kadar öde” tasarım hedefi var ve 3 kuralı/5 kuralı C++’a giriş düzeyinde temel bilgidir. Bu özel üye fonksiyonları derleyici yalnızca belirli koşullarda ürettiğinden, koşullar sağlanmıyorsa otomatik üretilmemeleri de temel bilgi sayılır.
      Sonuçta kullanılacak constructor’ı kendiniz ayarlamanız gerektiğini bilmek bu kadar akıl almaz bir şey mi, emin değilim. Bir örnek oluştururken onu başlatmak gerektiği de şaşılacak bir şey değil. Gerçek hayatta insanlar büyük tantana çıkarmadan bu şekilde iş yapıyor.
  • Bunu okuyunca başım dönüyor. Java constructor’larını ve nesne başlatmayı anlamaya çalıştığım zamanları hatırlatıyor
    En azından şimdiye kadarki deneyimime göre, Go ve Rust gibi özel constructor’ı olmayan bir tercih birçok şeyi basitleştiriyor
    Constructor’ı olmayan dillere geçtikten sonra constructor’ları özleyen biri var mı merak ediyorum

    • Yerinde çalışan constructor’ların mümkün kıldığı, biraz anlaşılması zor büyü gibi bazı durumlar var. Örneğin Rust’ta hâlâ garantili placement new türü bir davranış yok
      Elbette MaybeUninit içine ptr::write ile alanları tek tek elle yazabilirsiniz, ama ergonomik değil ve struct’ın buna açıkça izin vermesi gerekiyor
    • Derleyicinin hiçbir şey yapmamasının “gerçekten basitleştirdiği” sözünü pek anlayamıyorum
      Sonuçta bu, tüm constructor’ları açıkça bildirmek ve tanımlamak gerektiği anlamına geliyor; C++ da en başından beri bu seçeneği sunuyor. Özel üye fonksiyonlarının otomatik üretilmesi, tekrar eden koddan kaçınmak isteyenler için yalnızca çok belirli durumlarda çalışacak şekilde eklenmiş bir özellik
      İsterseniz kendi constructor’ınızı ekleyip normal şekilde yaşayabilirsiniz. Özel üye fonksiyonları yüzünden basit şeylerin karmaşıklaştığından şikâyet etmek, sözlükte zor kelimeler var diye İngilizcenin basit olmadığından şikâyet etmeye benziyor
    • Java constructor’ları kolay sayılır; bu C++ tarafı ise gerçekten kara büyü gibi
      Benim anladığım kadarıyla Rust constructor’ları temelde Java’ya benzemiyor mu?
    • Go’nun sıfır başlatması da bazen dil kurallarını kurcalamayı gerektirebiliyor
      https://codefibershq.com/blog/golang-why-nil-is-not-always-n...
  • Arka planda gerçekleşen tüm örtük davranışları ekleyen ya da gösteren bir C++ aracı var mı merak ediyorum
    Örneğin otomatik eklenen constructor’ları, örtük kopyalama constructor’larını ve başka şaşırtıcı şeyleri gösteren bir araçtan bahsediyorum

    • Gerçekçi olarak Godbolt ile cppinsights kombinasyonu en iyi seçenek gibi
  • T::T() = default; durumunda çıktının 0 olmasını beklersiniz ama çöp değer olacağını gösteren örnek aslında o kadar da garip değil
    İlgili bağlantı: https://consteval.ca/2024/07/03/initialization/#:~:text=You%...
    Çünkü farklı yapılsaydı, kütüphanenin herhangi bir kullanıcısı kütüphane davranışını değiştirebilir hale gelirdi

    • Alternatifin mantıksız olması, bu çözümün otomatik olarak mantıklı olduğu anlamına gelmez. Aksine, C++ tasarımının kendini ne kadar çok köşeye sıkıştırdığını gösteriyor