C++ Dilinde reference qualifier
- Yusuf Hançar
- Jul 20, 2024
- 7 min read
C++ dilinde modern standartlarla eklenen özellikleri incelerken sıkça reference kavramından bahsettik. Bununla beraber l value, r value, taşıma semantiği, geçici nesneler kavramlarını derinlemesine inceledik ve ilişkilerini pekiştirdik. Tasarımımızdaki ihtiyaca göre kopyalanması veya taşınması gereken nesneler için reference niteleyicisi kullandık, std::move kullandık, const ...& kullandık. Tasarımlar büyüyüp ihtiyaçlar arttıkça yeni niteleyiciler, yeni anahtar kelimeler hayatımıza girmeye devam etmektedir.
Reference qualifier kavramı da bunlardan biridir. C++11 ile standartlara eklenerek sınıfımızdaki üye fonksiyonları lvalue(T&) veya rvalue(T&&) olarak çağrılmasını sınırlandırmak için kullanılan bir niteleyicidir. Kullanım amacı temelde nesne ömrünü ve kaynak yönetimini daha iyi kontrol etmektir.
std::vector<std::string> create_svec()
{
return {"Smart", "Coding", "Rules"};
}
int main()
{
// Legal: iterate over strings in the vector
for (std::string str : create_svec())
{
std::cout << str << "\n";
}
// Undefined behavior: trying to access char in a temporary object
for (char chr : create_svec().at(0)) // UB: temporary object
{
std::cout << chr << "\n";
}
// Undefined behavior: trying to access char in a temporary object
for (char chr : create_svec()[0]) // UB: temporary object
{
std::cout << chr << "\n";
}
// Undefined behavior: trying to access char in a temporary object
for (char chr : create_svec().front()) // UB: temporary object
{
std::cout << chr << "\n";
}
return 0;
}
yukarıda neden undefined behavior olan kodlar vardır ?
std::vector<std::string> create_svec()
{
return {"Smart", "Coding", "Rules"};
}
int main()
{
// burada life-extension var
const auto& r1 = create_svec();
/* syntax error. geri dönüş değeri ifadesi L value reference'a bağlanmış.
sağ taraf değeri sol taraf değerine bağlanmaz. */
auto& r2 = create_svec();
// burada life-extension var
vector<string>&& r3 = create_svec();
}
int main()
{
const auto& r = create_svec().at(0); // legal ancak life-extension yok
}
class Data {
public :
~Data()
{
cout << "dtor called\n";
}
private :
vector<int> ivec{ 1, 2, 3, 4 };
};
Data func()
{
return Data{};
}
int main ()
{
{
const auto& rval = func();
cout << "assigned func()\n";
}
cout << "life extension state\n";
}
******************************
CEVAP :
assigned func()
dtor called
life extension state
******************************
AÇIKLAMA : life extension.
******************************
Data& rval = func(); // Geçici nesne ömrü uzatılmaz, hata verir
******************************
AÇIKLAMA : Geçici bir nesne, non-const bir referansa bağlandığında ömrü uzatılmaz. Bu durumda, geçici nesne, referansa bağlandığı kapsamın sonunda yok olur.
******************************
{
const auto& rval = func(); // Geçici nesne burada ömrü uzatılır
} // rval'in kapsamı burada sona erer, geçici nesnenin ömrü de burada sona erer
******************************
AÇIKLAMA : Referansın kapsamı bittiğinde, ona bağlı geçici nesnenin ömrü sona erer.
******************************
cout << func().ivec.size() << endl; // func() çağrısı sonrası geçici nesne hemen yok olur
******************************
AÇIKLAMA : Geçici nesne, tanımlandığı ifade sona erdiğinde hemen yok olur.
******************************
auto tmp1 = func(); // İlk geçici nesne
auto tmp2 = func(); // İkinci geçici nesne
******************************
AÇIKLAMA : Bir geçici nesne başka bir geçici nesneye atanırsa, atama işlemi bittikten sonra her iki geçici nesne de ayrı ayrı ömre sahip olur.
******************************
class Data {
public :
~Data()
{
cout << "dtor called\n";
}
private :
vector<int> ivec{ 1, 2, 3, 4 };
};
Data func()
{
return Data{};
}
int main ()
{
{
Data&& r = func(); // auto&& yazsaydık aynı olurdu.
cout << "assign func()\n";
}
cout << "life extension state\n";
}
******************************
CEVAP :
assign func()
dtor called
life extension state
******************************
AÇIKLAMA : life extension görüldü.
******************************
class Data {
public :
~Data()
{
cout << "dtor called\n";
}
vector<int> get() const
{
return ivec;
}
private :
vector<int> ivec{ 1, 2, 3, 4 };
};
Data func()
{
return Data{};
}
int main ()
{
const auto& rval = func().get();
cout << "assigned func()\n";
cout << rval[0] << "\n"; // tanımsız davranış
}
******************************
CEVAP :
dtor called
assigned func()
???
******************************
AÇIKLAMA : life extension yok.
const auto& rval = func().get_vec();
Bu satırda func() fonksiyonu bir geçici Data nesnesi döndürmektedir. Bu geçici nesne üzerinde get() fonksiyonu çağrılır ve bu fonksiyon vector<int> türünde bir değer döndürmektedir. Ancak, get() fonksiyonu const olarak işaretlenmiş olmasına rağmen, döndürülen vector<int> nesnesi bir geçici nesne olduğu için bu referansa const bir şekilde bağlanmamaktadır.
Dolayısıyla, "const auto& rval" referansı geçici bir nesneye bağlandığı için, bu durum C++ dilinde tanımsız davranışa yol açar. Çünkü geçici nesnenin ömrü, bu ifadenin sonunda sona erer, bu da rval referansının geçersiz bir bellek alanına işaret etmesine neden olabilir.
Doğru bir yaklaşım, func() fonksiyonundan dönen geçici Data nesnesine direkt olarak erişim sağlamaktır, böylece geçici nesnenin ömrü boyunca geçerli bir referans elde edilmiş olur:
******************************
class Data {
public :
std::string get() const
{
// m_str kopyalanır ve çağıran fonksiyona geri döner
return m_str;
}
private :
std::string m_str;
};
int main()
{
Data val;
// ..
val.get();
}
******************************
CEVAP :
******************************
AÇIKLAMA : bu şekilde bir get fonksiyonu yazıldığında kopyalamaya neden olur.
******************************
class Data {
public :
const std::string& get() const
{
return m_str;
}
private :
std::string m_str;
};
int main()
{
Data val;
// ..
const auto& str = val.get();
}
******************************
CEVAP :
******************************
AÇIKLAMA : m_str nesnesinin bir referansını döndürür. Referans döndürmek, kopyalama işlemi yapmaz ve hafızada sadece bir işaretçiyi tutar. Bu da daha hızlı ve hafızayı daha verimli kullanır.
const ise döndürülen std::string nesnesinin değiştirilemez olduğunu garanti ederek güvenlik sağlar ve fonksiyonun yan etkisiz olduğunu belirtir.
******************************
class Data {
public :
const std::string& get() const
{
return m_str;
}
private :
std::string m_str;
};
Data create_data();
int main()
{
for (auto chr : create_data().get())
{
// create_data() tarafından döndürülen geçici nesne burada yok olur.
// get() fonksiyonundan dönen referans geçersiz hale gelir.
}
}
******************************
CEVAP :
******************************
AÇIKLAMA : create_data() fonksiyonu çağrıldığında, geçici bir Data nesnesi oluşturulur. create_data().get() ifadesi ise bu geçici nesnenin m_str üyesine bir referans döner. Ancak, geçici Data nesnesi for döngüsü başladığında yok olur ve bu nedenle get() tarafından döndürülen referans artık geçersiz hale gelir("dangling reference"). Bu durum bellek hatalarına ve tanımsız davranışlara neden olabilir.
******************************
int main()
{
Data data = create_data(); // Geçici nesnenin ömrünü uzatıldı
// data.get() tarafından döndürülen referans geçerlidir
for (auto chr : data.get())
{
}
}
Bu problemleri çözebilmek için reference qualifier kullanılmaktadır.
Geçici Data nesnesi (rvalue) üzerinde çağrılan get() fonksiyonunun farklı bir davranış sergilemesini sağlayabiliriz. Örneğin, geçici nesne durumunda, get() fonksiyonunun bir std::string kopyası döndürmesini sağlayabiliriz. Bu, dangling reference probleminden kaçınmamızı sağlar.
class Data {
public :
const std::string& get() const & // lvalue referans için
{
return m_str;
}
std::string get() && // rvalue referans için
{
return std::move(m_str);
}
private :
std::string m_str;
};
Data create_data();
int main()
{
// Geçici nesne (rvalue) icin
for (auto chr : create_data().get()) // rvalue olan get() çağrılır
{
}
Data data;
// Kalıcı nesne (lvalue) icin
for (auto chr : data.get()) // lvalue olan get() çağrılır
{
}
}
class Data {
public:
};
Data func()
{
return Data{};
}
int main ()
{
func() = func();
Data{} = Data();
}
******************************
CEVAP :
******************************
AÇIKLAMA : atamaların hiç biri syntax error değil. geçici nesneye yapılan atama geçerli.
******************************
class Data {
public:
void func(){}
};
int main ()
{
Data{}.func();
}
******************************
AÇIKLAMA : legal
******************************
class Data {
public:
void func(){}
};
int main ()
{
Data val;
val.func();
Data{}.func();
std::move(val).func();
}
******************************
CEVAP :
******************************
AÇIKLAMA : legal
******************************
reference qualifier ile...
class Data {
public:
void func()&
{}
};
int main ()
{
Data val;
val.func();
Data{}.func(); // error
std::move(val).func(); // error
}
******************************
CEVAP :
******************************
AÇIKLAMA : fonksiyonun sadece L value değer kategorisindeki nesnelerle çağrılmasını, R value değer kategorisindeki ifadelerle çağırılmamasını istersek reference declaratoru koyarız. Buna reference qualifier denir. yani L value reference qualifier.
******************************
class Data {
public:
void func()&&
{}
};
int main ()
{
Data val;
val.func(); // error
Data{}.func(); // legal
std::move(val).func(); // legal
}
class Data {
public:
void func()&;
{}
};
int main ()
{
const Data c_val;
c_val.func(); // error
}
class Data {
public:
void func()const &
{}
};
int main ()
{
const Data cv;
cv.func(); // legal
}
void func(bool)
{
cout << "func(bool)\n";
}
void func(std::string)
{
cout << "func(std::string)\n";
}
std::string get()
{
return "yusuf";
}
int main()
{
if (get() == "smartcode")
{
}
}
******************************
CEVAP : legal
******************************
int main()
{
func(get() == "smartcode");
}
******************************
CEVAP : func(bool)
******************************
AÇIKLAMA :
******************************
int main()
{
func(get() = "smartcode"); //func(get().operator=("smartcode"));
}
******************************
CEVAP : func(string)
******************************
AÇIKLAMA : çağırılan fonksiyon = operator fonksiyonudur.
atama operator fonksiyonu reference qualifier değildir.
sağ taraf değeri olan bir sınıf nesnesi için atama operator fonksiyonunun çağırılmasında engel yoktur.
atama operator fonksiyonunun geri dönüş türü sınıf türünden referanstır. (*this döndürürler)
******************************
class Data {
public :
void func(){}
};
int main()
{
Data val;
val.func();
Data{}.func();
Data{} = val;
}
******************************
CEVAP : legal
******************************
class Data {
public :
Data(const Data&) = default;
Data(Data&&) = default;
Data& operator=(const Data& str)& = default;
Data& operator=(Data&& str)& = default;
};
Data func()
{
return Data{};
}
int main()
{
func() = func();
}
******************************
CEVAP : error
******************************
AÇIKLAMA : sınıfın atama operator fonksiyonları L value reference qualifier yani
sağ taraf değeri olan sınıf nesnelerine atama yapıldığında geçersiz olur.
atama operator fonksiyonlarını yazdığımız için move members bildirilmemiş olur.
******************************
class Data {
public :
Data(const string& name) : m_name(name) {}
string get_name()&& // R value ref. qualified
{
cout << "get_name&&" << endl;
return {};
}
const string& get_name() const& // const L value ref. qualified
{
cout << "get_name() const& " << endl;
return m_name;
}
const string& get_name()& // non-const L value ref. qualified
{
cout << "get_name()& " << endl;
return m_name;
}
private :
string m_name;
};
template <typename T>
void foo(T&& x) // universal ref parametreli
{
// forward burda value category'i korudu ve get_name çağırısyla da değer kategorisine // göre overload olanlardan biri çağırılır.
auto name = std::forward<T>(x).get_name();
cout << name << endl;
}
int main()
{
Data p{ "smart" };
const Data cp{ "code" };
foo(p); // L value elde ettik ve non-const L value çağırıldı. 3.
foo(cp); // const L ref 2.
foo(Data{ "cpp" });
}
******************************
CEVAP :
get_name()&
smart
get_name() const&
code
get_name&&
******************************
AÇIKLAMA : kopyalamadan kaçınmak için fonksiyonların geri dönüş değeri const& yapılır. burada kopylamadan kaçınmak için const string& overload'ı kullanılacak.
ancak range based for loop'un problem oluşturduğu yerde ise string get_name()&& overload'ı çalışacaktır.
******************************
Comments