C++ ile nesne yönelimli programlama yaparken kurucu içinde sanal bir fonksiyon çağırmak ilk bakışta gayet mantıklı görünebilir. Sonuçta fonksiyon virtual olarak tanımlanmıştır; nesne türetilmiş sınıfa aitse onun sürümü çalışmalıdır, değil mi? Ne yazık ki C++ burada bilinçli biçimde farklı davranır. Kurucu çalışırken sanal çağrı, o anda kurulmakta olan sınıfın uygulamasına bağlanır; henüz hazır olmayan türetilmiş sınıfa gönderilmez.
``
Temel örnek
Aşağıdaki kod, sık karşılaşılan beklenti ile gerçek davranış arasındaki farkı gösterir:
#include <iostream>
class Base {
public:
Base() {
std::cout << "Base kuruluyor\n";
initialize();
}
virtual void initialize() {
std::cout << "Base::initialize\n";
}
virtual ~Base() = default;
};
class Derived : public Base {
public:
Derived() : value(42) {
std::cout << "Derived kuruluyor\n";
}
void initialize() override {
std::cout << "Derived::initialize, value = "
<< value << '\n';
}
private:
int value;
};
int main() {
Derived object;
}
Programın önemli çıktısı şöyledir:
Base kuruluyor
Base::initialize
Derived kuruluyor
object, Derived türünde olmasına rağmen Base kurucusundaki çağrı Derived::initialize() fonksiyonuna gitmez. Bu bir derleyici hatası veya polimorfizm arızası değil, nesnenin yaşam döngüsünü koruyan dil kuralıdır.
Nesne hangi sırayla oluşur?
Bir Derived nesnesinin kurulumu basitleştirilmiş biçimde şu sırayı izler:
- Bellek ayrılır.
Basealt nesnesi kurulur.Derivedüyeleri başlatılır.Derivedkurucusunun gövdesi çalışır.- Nesne tam bir
Derivedolarak kullanılabilir.
Kurulum ilerlemesini yaklaşık olarak $P = c / n$ ile düşünebiliriz. Burada $c$ tamamlanan aşama, $n$ toplam aşama sayısıdır. Base kurucusu çalışırken $P < 1$ olduğundan, türetilmiş bölüm henüz geçerli durumuna ulaşmamıştır.
Eğer çağrı Derived::initialize() fonksiyonuna yönlendirilseydi, bu fonksiyon henüz başlatılmamış value üyesini okuyabilirdi. Böyle bir erişim tanımsız veya anlamsal açıdan bozuk davranışlara kapı açardı. C++ bu nedenle kurulum sırasında nesnenin dinamik türünü geçici olarak kurulmakta olan sınıfla sınırlar.
| Çağrının yapıldığı yer | Seçilen uygulama | Türemiş bölüm hazır mı? |
|---|---|---|
Base kurucusu |
Base::initialize() |
Hayır |
Derived kurucusu gövdesi |
Derived::initialize() |
Büyük ölçüde evet |
| Kurulum tamamlandıktan sonra | Dinamik türe göre | Evet |
Base yıkıcısı |
Base sürümü |
Hayır; türemiş bölüm yıkılmıştır |
Aynı kural yıkıcılar için de geçerlidir. Yıkım ters sırada gerçekleştiği için temel sınıf yıkıcısı çalışırken türetilmiş bölüm artık yaşamını tamamlamıştır.
Daha güvenli tasarım: iki aşamalı başlatma
Polimorfik davranış gerekiyorsa sanal fonksiyonu nesne tamamen kurulduktan sonra çağırabilirsiniz:
class Base {
public:
virtual void initialize() = 0;
virtual ~Base() = default;
};
class Derived : public Base {
public:
void initialize() override {
// Tüm üyeler artık güvenle kullanılabilir.
}
};
int main() {
Derived object;
object.initialize();
}
Daha kontrollü bir seçenek fabrika fonksiyonudur. Fabrika önce nesneyi oluşturur, ardından sanal başlatmayı gerçekleştirir:
#include <memory>
std::unique_ptr<Base> createDerived() {
auto object = std::make_unique<Derived>();
object->initialize();
return object;
}
| Yaklaşım | Avantaj | Risk |
|---|---|---|
| Kurucuda sanal çağrı | Kısa görünür | Beklenen override çalışmaz |
Açık initialize() çağrısı |
Basit ve anlaşılır | Çağırmayı unutmak mümkündür |
| Fabrika fonksiyonu | Kurulum adımlarını merkezileştirir | Tasarıma ek katman getirir |
| Kurucuya bağımlılık verme | Nesneyi hemen geçerli kılar | Daha fazla tasarım planlaması ister |
Özetle, kurucu içindeki sanal çağrı polimorfizmi tamamen kapatmaz; çağrıyı nesnenin güvenli biçimde kurulmuş bölümüne sınırlar. Gerçek dinamik gönderim gerekiyorsa işlemi kurulum sonrasına taşımak veya fabrika deseni kullanmak en temiz çözümdür.
Yorumlar