Elmas Problemi

Elmas problemi, çoklu miras yöntemi yanlış kullanılırsa ortaya çıkabilecek bir problemdir. Aşağıdaki şema ile konuyu anlamaya çalışalım:

Sağdaki şemada D sınıfı B ve C sınıfından, B ve C sınıfları ise A sınıfından miras almaktadır. Aslında bu şemada her şey yolunda gibi gözüküyor fakat programlamada bu şema aşağıdaki şema gibi işlenir:

Derleme işleminde soldaki şemadaki gibi derleme yapılır. Gördüğünüz gibi ilk şemadakinden farklı olarak iki tane A sınıfı vardır ve B ve C sınıfları bu A sınıflarından miras almaktadır. Bunun nedeni ise B ve C sınıfları A sınıfının nesnesinin kopyasını alır (Az sonra bu ifadeye açıklık getireceğim). Buna bağlı olarak iki tane A sınıfı oluşur. İlk şemada D sınıfında miras alınan nesne sayısı 3 tane iken bu şemada D sınıfının miras aldığı nesne sayısı 4 tanedir. Bunun nedeni ise bir sınıf bir başka sınıftan miras alırken miras aldığı sınıfın nesnesi kopyalanır ve miras alan sınıfa aktarılır. Fakat elmas problemindeki asıl sorun nesnelerin sayısı değildir.

Konuyu yazdığımız kodlar ile anlamaya çalışalım:

#include <iostream>
using namespace std;

class A {

public:
	A() { cout << "A sinifi cagirildi" << endl; }

};

class B : public A {

public:
	B() {

		cout << "B sinifi cagirildi" << endl;

	}

};

class C : public A {

public:
	C() {

		cout << "C sinifi cagirildi" << endl;

	}

};

class D : public B, public C {

public:
	D() {

		cout << "D sinifi cagirildi" << endl;

	}

};

int main() {

	A nesne1;
	cout << endl;
	B nesne2;
	cout << endl;
	C nesne3;
	cout << endl;
	D nesne4;
	cout << endl;

	return 0;
}

Bu yazdığımız kodlarda ilk şemanın ikinci şemaya nasıl dönüştüğünü anlıyoruz.

Ekrana verilen çıktı:

A sinifi cagirildi

A sinifi cagirildi
B sinifi cagirildi

A sinifi cagirildi
C sinifi cagirildi

A sinifi cagirildi
B sinifi cagirildi
A sinifi cagirildi
C sinifi cagirildi
D sinifi cagirildi

Ekrana verilen çıktıda D sınıfına ait iki tane A sınıfından nesne olduğunu görüyoruz. Gördüğünüz gibi bu noktada hiçbir sıkıntı yok.

Bir sonraki kodlar ile konuya devam edelim:

#include <iostream>
using namespace std;

class A {

	int sayi;

public:
	A() { sayi = 10; } /* varsiylan bir deger atama */

	A(int n) { 

		/* sinifin cagirildigina dair cikti verir ve sayi degiskenine
		deger atamasi yapar */

		cout << "A sinifi cagirildi" << endl; 
		sayi = n;
	
	}

	int getsayi() { return sayi; } /* sayi degiskenini dondurur */

};

class B : public A {

public:
	B(int n) : A(n) {

		/* sinifin cagirildigina dair cikti verir */

		cout << "B sinifi cagirildi" << endl;

	}

};

class C : public A {

public:
	C(int n) : A(n) {

		/* sinifin cagirildigina dair cikti verir */

		cout << "C sinifi cagirildi" << endl;

	}

};

class D : public B, public C {

public:
	D(int n1, int n2) : B(n1), C(n2) {

		/* sinifin cagirildigina dair cikti verir */

		cout << "D sinifi cagirildi" << endl;

	}

};

int main() {

	D nesne = D(5, 10); /* nesne atamasi */

	cout << nesne.B::getsayi() << endl; /* nesnenin B sinifindaki sayisini
	dondurur */
	cout << nesne.C::getsayi() << endl; /* nesnenin C sinifindaki sayisini
	dondurur */

	return 0;
}

Bu yazdığımız kodlarda ise A sınıfında bir “sayi” değişkeni tanımladık. Daha sonra B ve C sınıfları A sınıfından miras aldılar ve en sonunda D sınıfı B ve C sınıflarından miras aldı. Son olarak D sınıfının miras almış olduğu fonksiyonlara “main()” fonksiyonları içerisinde değerler atayıp B ve C sınıflarındaki nesnelere atanmış sayıları ekrana yazdırdık.

Ekrana verilen çıktı:

A sinifi cagirildi
B sinifi cagirildi
A sinifi cagirildi
C sinifi cagirildi
D sinifi cagirildi
5
10

Gördüğünüz gibi buraya kadar hiçbir problem ile karşılaşmadık. Şimdi işlerin kötüye gittiği kısma geliyoruz:

#include <iostream>
using namespace std;

class A {

	int sayi;

public:
	A() { sayi = 10; } /* varsiylan bir deger atama */

	A(int n) { 

		/* sinifin cagirildigina dair cikti verir ve sayi degiskenine
		deger atamasi yapar */

		cout << "A sinifi cagirildi" << endl; 
		sayi = n;
	
	}

	int getsayi() { return sayi; } /* sayi degiskenini dondurur */

};

class B : public A {

public:
	B(int n) : A(n) {

		/* sinifin cagirildigina dair cikti verir */

		cout << "B sinifi cagirildi" << endl;

	}

};

class C : public A {

public:
	C(int n) : A(n) {

		/* sinifin cagirildigina dair cikti verir */

		cout << "C sinifi cagirildi" << endl;

	}

};

class D : public B, public C {

public:
	D(int n1, int n2) : B(n1), C(n2) {

		/* sinifin cagirildigina dair cikti verir */

		cout << "D sinifi cagirildi" << endl;

	}

};

int main() {

	D nesne = D(5, 10); /* nesne atamasi */

	cout << nesne.getsayi() << endl;  /* programin calismasi 
	derleyici tarafindan engelleniyor */

	return 0;
}

Yazdığımız kodlar bir önceki kodlar ile neredeyse aynı. Tek fark “main()” fonksiyonundaki çıktıda. Bu çıktıda D sınıfındaki nesneye ait sayı ekrana verilmesi gerekir fakat program bunu yapamaz. Bunun nedeni ise B ve C sınıflarının ikinci şemadaki gibi iki farklı A sınıfından miras almasıdır. Buna bağlı olarak D sınıfı B ve C sınıfından “sayi” değişkeni ile farklı miraslar alır. Bunun sonucunda program ekrana hangi sayıyı çıktı olarak vereceğini bilemez ve program başlatılamaz.

Problemin Çözümü (Virtual Inheritance) (Sanal Miras)

Bu problemin çözümü “virtual” anahtar kelimesidir. Sanal kalıtım, D sınıfının iki farklı A sınıfından miras almasını önler. Bunun sonucunda D sınıfı bir tane A sınıfından miras alır. Yazdığımız kodlar ile konuyu anlamaya çalışalım:

#include <iostream>
using namespace std;

class A {

	int sayi;

public:
	A() { sayi = 10; } /* varsiylan bir deger atama */

	A(int n) { 

		/* sinifin cagirildigina dair cikti verir ve sayi degiskenine
		deger atamasi yapar */

		cout << "A sinifi cagirildi" << endl; 
		sayi = n;
	
	}

	int getsayi() { return sayi; } /* sayi degiskenini dondurur */

};

class B : virtual public A {

public:
	B(int n) : A(n) {

		/* sinifin cagirildigina dair cikti verir */

		cout << "B sinifi cagirildi" << endl;

	}

};

class C : virtual public A {

public:
	C(int n) : A(n) {

		/* sinifin cagirildigina dair cikti verir */

		cout << "C sinifi cagirildi" << endl;

	}

};

class D : public B, public C {

public:
	D(int n1, int n2, int n3) : B(n1), C(n2), A(n3) {

		/* sinifin cagirildigina dair cikti verir */

		cout << "D sinifi cagirildi" << endl;

	}

};

int main() {

	D nesne = D(5, 10, 20); /* nesne atamasi */

	cout << nesne.getsayi() << endl;  /* sanal miras cozumu ile D sinifindaki
	sayi belli olur ve ekrana cikti verilir */

	return 0;
}

Ekrana verilen çıktı:

A sinifi cagirildi
B sinifi cagirildi
C sinifi cagirildi
D sinifi cagirildi
20

Sonucu iyi bir şekilde anlamak adına alttaki görselden yararlanalım:

Sağ altta da görüldüğü üzere kodun çıktısı 20 şeklinde. Peki bu kod neden 20 ye eşitlendi:

Aslında D sınıfının içerisinde kahverengi 5, mavi 10 ve turuncu 20 değerleri girdi olarak alınd;
5, B sınıfına gönderildi. B sınıfının amacı ise A sınıfını çağırarak “sayi” değerini 5’e eşitlemekti fakat bu işlemi sanal olarak gerçekleştirdi ve D sınıfımızın içindeki “sayi” değişkeninin değeri 5’e eşitlenmedi.
Aynı işlem C sınıfı için de gerçekleşti ve 10 değeri sanal olarak atandı.

“sayi” değerini asıl 20’ye eşitleyen yer D sınıfının içinde çağırdığımız A sınıfı oldu. Yeşil ile işaretlenen yerde olduğu gibi sayımız 20’ye eşitlendi.

Peki D sınıfının tanım yerinde A sınıfı hiç çağırılmasaydı ne olurdu ?

D sınıfını incelerseniz kodun değiştiğini ve artık “A(n3)” ün çağrılmadığını görebilirsiniz.
Artık A sınıfı çağrılmıyor . B sınıfı ile C sınıfının da sanal bir A sınıfı üzerinde işlem gerçekleştiriyor. Bu yüzden D sınıfı oluşurken bir “sayi” değişkeni oluştu fakat herhangi bir şeye eşitlenmedi.
İşte tam burada A sınıfının içine yazdığımız kod (A(){sayi = 10 ;}) işlev gösterecek ve “sayi” değişkeni otomatik olarak 10’ a eşitlenecektir.

Eğer varsayılan değer atamasaydık ne olurdu ? Sistem “sayi” değişkenini oluşturacak fakat neye eşitleyeceğini bilemediği için hata verecekti.

Elmas problemi gerçekten yazılımda önemli bir yer kaplar. Eğer elmas şemasında (yazının ilk şemasındaki gibi) bir yapı yapmayı planlıyorsanız çok dikkatli olmalı ve her olasılığı gözden geçirmelisiniz. Umarız bu yazı elmas problemini ve çözümünü anlamanıza katkı sağlamıştır.

Bu yazı www.cogrencisi.com ve www.gibitek.com iş birliği ile hazırlandı. Yazının diğer versiyonuna buradan ulaşabilirsiniz.

Bir cevap yazın

E-posta hesabınız yayımlanmayacak.