- 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
Hacker News görüşleri
Tiç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 dexiç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.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_wrapperkullanmak 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.
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-...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.
unlesskelimesinin kalın yazıldığı kısımla ilgili iğneleyici yorum olmamasına şaşırdım.T t{v0};veT 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.
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.
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ı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
Elbette
MaybeUninitiçineptr::writeile alanları tek tek elle yazabilirsiniz, ama ergonomik değil ve struct’ın buna açıkça izin vermesi gerekiyorSonuç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
Benim anladığım kadarıyla Rust constructor’ları temelde Java’ya benzemiyor mu?
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
cppinsightskombinasyonu en iyi seçenek gibiT::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