Birkaç yıl önce C++ dilinin yaratıcısı Bjarne Stroustrup'un yazdığı "A Tour of C++" kitabını okurken çok güzel bir örnekle karşılaşmıştım. İki objeyi birbirine eşitlerken -ne yaptığımızı bilmiyorsak- çok büyük problemlere yol açacak bir hata yapıyor olabiliriz. Bu rehberde obje kopyalamanın incelikleri üzerine yazacağım.
Elimizde primitif bir "a" objesi -sözgelimi, int tipinde- olsun. Bunu yeni bir "b" objesine kopyalamak neredeyse her programlama dilinde aynı syntax'a sahip ve oldukça basit.
Açıklamaya gerek duymadığım bu kod bir integer tipinde bir a nesnesi yaratıyor, ona 5 değerini atıyor, yeni bir b nesnesi yaratıyor ve değerini A'ya eşitliyor. Ama kopyalama her zaman bu kadar basit çalışmayabilir. Şimdi bir A sınıfına sahip olduğumuzu varsayalım, bu integer değerini A sınıfı bir özellik olarak taşısın.
Yine oldukça basit bir kod, A sınıfına ait bir a objemiz var, bunun m_x özelliğini 5 e eşitledikten sonra b objesine kopyaladık. (Artık b objesinin de m_x değeri 5 oldu!), ardından a objesinin m_x değerini 7'ye eşitledik. Tabii ki b objesi bundan etkilenmedi, onun m_x değeri aynı kaldı. Programımızın çıktısı da zaten bu yönde olacaktır. Peki ya A sınıfımız bir integer değerden daha fazla özellik taşısaydı, mesela bir string pointer'a sahip olsaydı? A objemiz bir string pointer'a sahip olsun, bu pointerı başlangıç string'ine eşitleyelim ve b objesine kopyalayalım, ardından a objesinin pointerının gösterdiği değeri güncelleyelim ve a ve b objelerinin string değerlerini ekrana yazdıralım.
a objesinin m_str değerinin Turkhacks olmasını bekleriz, doğal olarak. Peki ya b objesi? Programı çalıştırdığımızda alacağımız sonuç şudur.
Ama nasıl olur? Biz b objesinin hiçbir değerini değiştirmedik ki!? Bunun sebebi C++ dilindeki varsayılan (default) kopyalama stilinin yüzeysel kopyalama (shallow copy) olmasıdır. Yüzeysel kopyalama, eşitlik operatörünün sağ tarafında bulunan objenin tüm değerlerini alır, birebir olarak sol tarafa yazar; ve eşitlik operatörünün sağ tarafındaki a nesnesi bir string nesnesi tutmuyor, bir string nesnesinin adresini tutuyordu. Grafik üzerinde gösterecek olursak, A sınıfı şu şekilde çizilebilir.
Dolayısıyla oluşturulan a objesi, dışarıdan verilen bu string nesnesinin "adresine" sahipti -örnek olarak 0x0001 olsun- ve tüm değerler birebir kopyalanınca b nesnesi de aynı nesnenin adresine sahip olmuş oldu. (b nesnesi için de m_str değeri 0x0001 oldu). Dolayısı ile hafızadaki 0x0001 adresinde yaptığımız tüm değişikliklerden ortak olarak b nesnesi de etkilendi, böyle istemesek dahi. Bunu çözmek için "Deep Copy" denen metodu kullanmalıyız.
Deep copy, objenin ait olduğu sınıfın kullandığı kaynakları birebir olarak yazmaktansa, önce kaynak nesneleri kopyalar ve kopyaladığı nesneleri eşitlik operatörünün sol tarafına yazar. Deep copy işlemini gerçekleştirmek için, objenin kopyalanırken hangi işlemlerden geçeceğini derleyiciye biz belirtmeliyiz, bunun için copy constructor'ı ve copy operator overloadını düzenleyelim.
Bu kodumuzun çıktısı beklendiği üzere;
Olacaktır. Bunun sebebi yeni implementasyonumuzda eğer A sınıfına ait herhangi bir obje = operatörü üzerinden ya da direkt referans yoluyla "A b{a}" gibi kopyalanmaya çalışılırsa önce a objesinin kullandığı string kaynağı kopyalanacak; ardından yeni kaynağın adresi yeni oluşturulan b nesnesine verilecektir. Eski ve yeni durumu az önceki diyagrama benzer şekilde nesneler üzerinden gösterecek olursak
Kısa ve basit bir örnekle C++ dilinde obje kopyalamanın potansiyel bir riskine değinmeye çalıştım, buraya kadar okuduysanız teşekkür ederim.
Elimizde primitif bir "a" objesi -sözgelimi, int tipinde- olsun. Bunu yeni bir "b" objesine kopyalamak neredeyse her programlama dilinde aynı syntax'a sahip ve oldukça basit.
C++:
int main()
{
int a;
a = 5;
int b;
b = a;
std::cout << b << "\n";
}
Açıklamaya gerek duymadığım bu kod bir integer tipinde bir a nesnesi yaratıyor, ona 5 değerini atıyor, yeni bir b nesnesi yaratıyor ve değerini A'ya eşitliyor. Ama kopyalama her zaman bu kadar basit çalışmayabilir. Şimdi bir A sınıfına sahip olduğumuzu varsayalım, bu integer değerini A sınıfı bir özellik olarak taşısın.
C++:
#include <iostream>
class A{
public:
int m_x;
};
int main()
{
A a;
a.m_x = 5;
A b;
b = a;
a.m_x = 7;
std::cout << "b.m_x: " << b.m_x << "\n"; // 5
std::cout << "a.m_x: " << a.m_x << "\n"; //7
}
Yine oldukça basit bir kod, A sınıfına ait bir a objemiz var, bunun m_x özelliğini 5 e eşitledikten sonra b objesine kopyaladık. (Artık b objesinin de m_x değeri 5 oldu!), ardından a objesinin m_x değerini 7'ye eşitledik. Tabii ki b objesi bundan etkilenmedi, onun m_x değeri aynı kaldı. Programımızın çıktısı da zaten bu yönde olacaktır. Peki ya A sınıfımız bir integer değerden daha fazla özellik taşısaydı, mesela bir string pointer'a sahip olsaydı? A objemiz bir string pointer'a sahip olsun, bu pointerı başlangıç string'ine eşitleyelim ve b objesine kopyalayalım, ardından a objesinin pointerının gösterdiği değeri güncelleyelim ve a ve b objelerinin string değerlerini ekrana yazdıralım.
C++:
#include <iostream>
#include <string>
class A{
public:
A(std::string str) : m_pStr{&str} {}
int m_x;
std::string* m_pStr;
};
int main()
{
A a{"xs0n3r"};
a.m_x = 5;
A b = a;
std::string new_string{"Turkhacks"};
*(a.m_pStr) = new_string;
std::cout << "*b.m_pStr: " << *(b.m_pStr) << "\n"; //????
std::cout << "*a.m_pStr: " << *(a.m_pStr) << "\n"; // Turkhacks
}
a objesinin m_str değerinin Turkhacks olmasını bekleriz, doğal olarak. Peki ya b objesi? Programı çalıştırdığımızda alacağımız sonuç şudur.
C++:
PS C:\Users\xs0n3r\ths> .\copy_example.exe
b.m_str: Turkhacks
a.m_str: Turkhacks
Ama nasıl olur? Biz b objesinin hiçbir değerini değiştirmedik ki!? Bunun sebebi C++ dilindeki varsayılan (default) kopyalama stilinin yüzeysel kopyalama (shallow copy) olmasıdır. Yüzeysel kopyalama, eşitlik operatörünün sağ tarafında bulunan objenin tüm değerlerini alır, birebir olarak sol tarafa yazar; ve eşitlik operatörünün sağ tarafındaki a nesnesi bir string nesnesi tutmuyor, bir string nesnesinin adresini tutuyordu. Grafik üzerinde gösterecek olursak, A sınıfı şu şekilde çizilebilir.
Dolayısıyla oluşturulan a objesi, dışarıdan verilen bu string nesnesinin "adresine" sahipti -örnek olarak 0x0001 olsun- ve tüm değerler birebir kopyalanınca b nesnesi de aynı nesnenin adresine sahip olmuş oldu. (b nesnesi için de m_str değeri 0x0001 oldu). Dolayısı ile hafızadaki 0x0001 adresinde yaptığımız tüm değişikliklerden ortak olarak b nesnesi de etkilendi, böyle istemesek dahi. Bunu çözmek için "Deep Copy" denen metodu kullanmalıyız.
Deep copy, objenin ait olduğu sınıfın kullandığı kaynakları birebir olarak yazmaktansa, önce kaynak nesneleri kopyalar ve kopyaladığı nesneleri eşitlik operatörünün sol tarafına yazar. Deep copy işlemini gerçekleştirmek için, objenin kopyalanırken hangi işlemlerden geçeceğini derleyiciye biz belirtmeliyiz, bunun için copy constructor'ı ve copy operator overloadını düzenleyelim.
C++:
#include <iostream>
#include <string>
class A{
public:
//parametric constructor
A(std::string str) : m_str{&str} {}
//copy constructor and copy operator
A(const A& rh)
{
m_x = rh.m_x;
std::string* my_new_string = new std::string;
*my_new_string = *(rh.m_str);
m_str = my_new_string;
}
A& operator=(const A& rh)
{
if (this != &lh)
{
m_x = rh.m_x;
std::string* my_new_string = new std::string;
*my_new_string = *(rh.m_str);
m_str = my_new_string;
}
return *this;
}
//attributes
int m_x;
std::string* m_str;
};
int main()
{
A a{"xs0n3r"};
a.m_x = 5;
A b = a;
std::string new_string{"Turkhacks"};
*(a.m_str) = new_string;
std::cout << "b.m_str: " << *(b.m_str) << "\n"; //????
std::cout << "a.m_str: " << *(a.m_str) << "\n"; // Turkhacks
}
Bu kodumuzun çıktısı beklendiği üzere;
C++:
PS C:\Users\xs0n3r\ths> .\copy_example.exe
b.m_str: xs0n3r
a.m_str: Turkhacks
Olacaktır. Bunun sebebi yeni implementasyonumuzda eğer A sınıfına ait herhangi bir obje = operatörü üzerinden ya da direkt referans yoluyla "A b{a}" gibi kopyalanmaya çalışılırsa önce a objesinin kullandığı string kaynağı kopyalanacak; ardından yeni kaynağın adresi yeni oluşturulan b nesnesine verilecektir. Eski ve yeni durumu az önceki diyagrama benzer şekilde nesneler üzerinden gösterecek olursak
Kısa ve basit bir örnekle C++ dilinde obje kopyalamanın potansiyel bir riskine değinmeye çalıştım, buraya kadar okuduysanız teşekkür ederim.



