> C++ dilinde temel kavramlar : >> "Expression" : Sabitlerin ve değişkenlerin, operatörlerle bir araya gelen birimlere ifade denir. Dilin bir çok kuralı ifadeler ile ilgilidir. C++ standartları ise bu konuda kısaca şöyle söylemektedir: "Each C++ expression, (an operator with its operands, a literal, a variable name etc.) is characterized by two independent properties: a type and a value category. Each expression has some non-reference type, and each expression belongs to exactly one of the three primary value categories: prvalue, xvalue and lvalue." Şimdi de detaylarını incelemeye başlayalım: >>> İfadenin Türü("Value of Expression") : "Function Overloading" sırasında, şablonların derleme sırasında açılmasında vb. bir çok yerde etkili olan bir kavramdır. Örneğin, "int", "int*" gibi şeyler bir ifadenin türü olabilir. Fakat burada ifadenin türü referans OLAMAZ. Fakat bu durum göstericiler için geçerli değildir. Özetle dilin bir çok aracı için ifadelerin türü belirleyici bir faktördür. * Örnek 1, #include int main() { int x = 6; int& rx = x; // Burada 'rx' ifadesinin türü "int&" DEĞİLDİR. Burada ifadenin türü "int" biçimindedir. rx = 66; int* px = &x; // Fakat bu durum göstericiler için geçerli değildir. Buradaki "px" in türü "int*" biçimindedir. *px = 99; } >>> İfadenin Değer Kategorisi("Value Category of Expression") : İfadelerin türü kadar bu kavram da çok önemlidir. Değişkenlerin DEĞER KATEGORİSİ YOKTUR. İFADENİN DEĞER KATEGORİSİ VARDIR. Değişkenlerin türü olur. İfadelerin değer kategorileri ayrıca iki temel özelliği de beraberinde getirmektedir: Bir ifadenin kimliğinin olması ve o ifadenin kaynaklarının başkaları tarafından devir alınıp alınamaması. >>>> Bir ifadenin kimliğinin olması: Bellekte bir yere karşılık gelmesi anlamındadır. Bir diğer deyişle "address of" operatörünün operandı olabilmesidir. Burada ifadenin bir isimle temsil edilmesi zorunluluk değildir. >>>> Bir ifadenin kaynaklarının çalınabilir olması: Bir ifadenin kaynaklarının başka şahsa devir edilmesi durumudur. Öte yandan ifadelerin değer kategorisi toplamda beş adettir. Bunlardan üç tanesi "Primary Value Category", diğer iki tanesi ise "Combined Value Category". >>>> "Primary Value Category" : "PR-Value", "L-Value" ve "X-Value" değer kategorilerinden oluşmaktadır. Bir ifade bir "t" anında bu üç kategoriden birisine ilişkin olmak zorundadır. >>>>> "PR-Value" : Kimliği olmayan fakat kaynakları çalınabilen ifadelerdir. >>>>> "L-Value" : Kimliği olan fakat kaynakları çalınamayan ifadelerdir. >>>>> "X-Value" : Kimliği olan ve aynı zamanda kaynakları çalınabilen ifadelerdir. >>>> "Combined Value Category" : "GL-Value" ve "R-Value" değer kategorilerinden oluşmaktadır. >>>>> "GL-Value" : Bir ifadenin kategorisinin o anda "L-Value" ya da "X-Value" olması durumudur. Yani bir kimliğe sahip olan ifadelerin değer kategorisi budur. >>>>> "R-Value" : Bir ifadenin kategorisinin o anda "PR-Value" ya da "X-Value" olması durumudur. Burada yazılanları da şu şekilde gösterebiliriz: Has ID Has no ID Can Steeal (Resources) X-Value PR-Value => R-Value Cannot Steal(Resources) L-Value XXX || GL-Value Şimdi bizler bir ifadenin değer kategorisini nasıl tespit edebiliriz? Fakat öncesinde "decltype" anahtar sözcüğünü hatırlamamız gerekmektedir. Şöyleki; bu operatörün operandının bir isim olması veya bir ifade olması durumunda elde edilecek sonuç değişecektir. >>>> Operandın bir isim olması durumu: Bu isim ne şekilde deklare edilmişse, operatörün sonucu da o türe karşılık gelecektir. * Örnek 1, struct Myclass { int a; }; int main() { int x = 10; int& r = x; const int& cr = x; int* ptr = &x; decltype(x) a; // Burada "decltype(x)" ifadesinin karşılığı "int" türüdür. "int" yazabileceğimiz çoğu yere // "decltype(x)" yazabiliriz. decltype(r) aa; // Burada "decltype(r)" ifadesinin karşılığı "int&" türüdür. Referanslara ilk değer vermek // zorunlu olduğu için, böylesi bir kullanım sentaks hatası oluşturmaktadır. decltype(cr) aaa; // Burada "decltype(cr)" ifadesinin karşılığı "const int&" türüdür. "const reference" lara // ilk değer vermek zorunlu olduğu için, böylesi bir kullanım sentaks hatası oluşturmaktadır. Myclass m{}; decltype(m.a) q; // Burada "decltype(m.a)" ifadesinin karşılığı "int" türüdür. Myclass* pm = &m; decltype(pm->a) qq; // Burada "decltype(pm->a)" ifadesinin karşılığı "int" türüdür. int&& b = 5; decltype(b) bb; // Burada "decltype(b)" ifadesinin türü "int&&" türüdür. } >>>> Operandın bir ifade olması durumu: Burada ifadenin türü rol oynamaktadır. Bunu da aşağıdaki tablo ile özetleyebiliriz: Expression Type PR-Value T L-Value T& X-Value T&& * Örnek 1, int&& foo(); struct Neco { int x{}; }; int main() { decltype(10) a; // Operant olan ifade "PR-Value" olmasından dolayı "decltype(10)" ifadesinin türü "int" türüdür. int x = 3; decltype(x + 5) aa; // Operant olan ifade "PR-Value" olmasından dolayı "decltype(x + 5)" ifadesinin türü "int" türüdür. int* px = &x; decltype(*px) aaa; // Operant olan ifade "L-Value" olmasından dolayı "decltype(*px)" ifadesinin türü "int&" türüdür. decltype((x)) b; // Operant olan ifade "L-Value" olmasından dolayı "decltype((x))" ifadesinin türü "int&" türüdür. decltype(foo()) c; // Operant olan ifade "X-Value" olmasından dolayı "decltype(foo())" ifadesinin türü "int&&" türüdür. decltype(Neco{}.x) d; // Dilin kuralları gereği "R-Value" olan sınıf nesnelerinin elemanlarına erişme ifadelerinin değer // kategorilari "X-Value" şeklindedir. Operant olan ifadenin "X-Value" olmasından dolayı // "decltype(Neco{}.x)" ifadesinin türü "int&&" türüdür. } İşte aşağıdaki kod parçası, ifadenin değer kategorisini söylemektedir: * Örnek 1, #include /* Primariy Variable Template */ template constexpr const char* p = "PR_Value"; /* Partial Specialization of Primary Variable Template*/ template constexpr const char* p = "L_Value"; /* Partial Specialization of Primary Variable Template*/ template constexpr const char* p = "X_Value"; #define print_val_category(e) std::cout << "Value category of '" << #e << "' is " << p << '\n' /* Function Prototypes */ void moo(); int foo(); int& bar(); int&& baz(); /* Class Definitons */ struct Nec { int x{}; static int sx; }; int main() { std::cout << "\n----------------------------------------------------\n"; std::cout << p << "\n"; // "PR_Value" std::cout << p << "\n"; // "L_Value" std::cout << p << "\n"; // "X_Value" std::cout << p << "\n"; // "PR_Value" print_val_category(10); // Value category of '10' is PR_Value int x = 100; std::cout << p << "\n"; // "PR_Value" print_val_category(x); // Value category of 'x' is L_Value std::cout << p << "\n"; // "L_Value" print_val_category((x)); // Value category of '(x)' is L_Value int* ptr = &x; std::cout << p << "\n"; // "L_Value" print_val_category(*ptr); // Value category of '*ptr' is L_Value std::cout << p << "\n"; // "L_Value" print_val_category((*ptr)); // Value category of '(*ptr)' is L_Value int y = 100; print_val_category(y); // Value category of 'y' is L_Value std::cout << "\n----------------------------------------------------\n"; int z = 100; print_val_category(z + 5); // Value category of 'z + 5' is PR_Value print_val_category(x++); // Value category of 'x++' is PR_Value print_val_category(x--); // Value category of 'x++' is PR_Value print_val_category(++x); // Value category of '++x' is L_Value print_val_category(--x); // Value category of '++x' is L_Value std::cout << "\n----------------------------------------------------\n"; print_val_category(moo()); // Value category of 'moo()' is PR_Value print_val_category(foo()); // Value category of 'foo()' is PR_Value print_val_category(bar()); // Value category of 'bar()' is L_Value print_val_category(baz()); // Value category of 'baz()' is X_Value std::cout << "\n----------------------------------------------------\n"; print_val_category(100); // Value category of '100' is PR_Value print_val_category(100.001); // Value category of '100.001' is PR_Value print_val_category('A'); // Value category of ''A'' is PR_Value print_val_category("Ahmet"); // Value category of '"Ahmet"' is L_Value std::cout << "\n----------------------------------------------------\n"; print_val_category(moo); // Value category of 'moo' is L_Value print_val_category(foo); // Value category of 'foo' is L_Value print_val_category(bar); // Value category of 'bar' is L_Value print_val_category(baz); // Value category of 'baz' is L_Value std::cout << "\n----------------------------------------------------\n"; int q = 100; print_val_category(q); // Value category of 'q' is L_Value print_val_category(std::move(q)); // Value category of 'std::move(q)' is X_Value print_val_category(static_cast(q)); // Value category of 'static_cast(q)' is X_Value /* * Fonksiyonlar hangi türe / referans türe dönüştürülürse dönüştürülsün, her zaman "L_Value" olurlar. * Dolayısıyla "MSVC" derleyicisi bu konuda bir "bug" a sahiptir. "GCC" derleyicisi ile test edebiliriz. */ print_val_category(std::move(foo)); // Value category of 'std::move(foo)' is X_Value std::cout << "\n----------------------------------------------------\n"; print_val_category(nullptr); // Value category of 'nullptr' is PR_Value std::cout << "\n----------------------------------------------------\n"; print_val_category(Nec{}); // Value category of 'Nec{}' is PR_Value Nec myNec; print_val_category(myNec); // Value category of 'myNec' is L_Value auto mymyNec = myNec; print_val_category(mymyNec); // Value category of 'mymyNec' is L_Value auto& mymymyNec = myNec; print_val_category(mymymyNec); // Value category of 'mymymyNec' is L_Value auto&& mymymymyNec = Nec{}; print_val_category(mymymymyNec); // Value category of 'mymymymyNec' is L_Value std::cout << "\n----------------------------------------------------\n"; Nec neco; print_val_category(neco.x); // Value category of 'neco.x' is L_Value print_val_category(Nec{}.x); // Value category of 'Nec{}.x' is X_Value print_val_category(std::move(neco).x); // Value category of 'std::move(neco).x' is X_Value print_val_category(std::move(neco).sx); // Value category of 'std::move(neco).sx' is L_Value print_val_category(std::move(neco).sx); // Value category of 'std::move(neco).sx' is L_Value /* * Yine "R-Value" değer kategorisindeki bir ifadenin "non-static" veri elemanı bir referans olsa bile, * bu veri elemanına erişim ifadesi de bir "L-Value". */ std::cout << "\n----------------------------------------------------\n"; print_val_category([]() { return 31; }); // Value category of '[]() { return 31; }' is PR_Value std::cout << "\n----------------------------------------------------\n"; int i{}, j{}, k{}; print_val_category(i < 100 ? j : k); // Value category of 'i < 100 ? j : k' is L_Value print_val_category(i < 100 ? 100 : k); // Value category of 'i < 100 ? 100 : k' is PR_Value print_val_category(i < 100 ? j : 100); // Value category of 'i < 100 ? j : 100' is PR_Value std::cout << "\n----------------------------------------------------\n"; } * Örnek 2, #include #include template void func() { if constexpr (std::is_rvalue_reference_v) std::cout << "X-Value\n"; else if constexpr (std::is_lvalue_reference_v) std::cout << "L-Value\n"; else if constexpr (!std::is_reference_v) std::cout << "PR-Value\n"; } #define exp(e) decltype((e)) int main() { func(); // PR-Value int x = 43; func(); // L-Value } Pekiyi bu değer kategorilerine biz nerelerde ihtiyaç duyacağız? El-cevap; Referansların kullanımı sırasında, "Type Deduction", "Function Overload Resolution" sırasında. >>>> Referansların Kullanımı Sırasında: Anımsayacağınız üzere C++ dilinde üç farkı referans vardır. Bunlar "L-Value Reference", "R-Value Reference", "Forwarding Reference / Universal Reference" referanslarıdır. >>>>> "L-Value Reference" : Tek "&" deklaratörünün kullanıldığı referanslardır. "const" olmayan bir "L-Value" referanslara sadece ve sadece "const" olmayan "L_Value" ifadeler bağlanabilir. Eğer referansımız "const" olursa; -> "const" olmayan "L-Value" bir ifade bağlanabilir. -> "const" bir "L-Value" ifade bağlanabilir. -> "R-Value" bir ifade bağlanabilir. -> "const" bir "R-Value" ifade bağlanabilir. >>>>> "R-Value Reference" : Çift "&" deklaratörünün kullanıldığı referanslardır. "L-Value" bir ifade kesinlikle bağlayamıyoruz. Eğer referansımız "const" değilse, "const" olmayan bir "R-Value" ifade bağlanabilir. Eğer "const" ise "const" ve "const" olmayan bir "R-Value Reference" bağlanabilir. >>>> "Function Overload Resolution" sırasında: Aşağıdaki örneği inceleyelim: * Örnek 1, #include #include /* T& const T& T&& const T&& L-Value 1 2 X X const L-Value X 1 X X R-Value X 3 1 2 const R-Value X 2 X 1 */ struct S {}; void foo(S&) { std::cout << "S&\n"; } void foo(const S&) { std::cout << "const S&\n"; } void foo(S&&) { std::cout << "S&&\n"; } void foo(const S&&) { std::cout << "const S&&\n"; } int main() { S x; foo(x); // L-Value const S cx; foo(cx); // const L-Value foo(S{}); // R-Value foo(std::move(cx)); // const R-Value } İşte bu örnekten de görüleceği üzere "const T&" olanlar, "const" olsun veya olmasın, hem "L-Value" hem "R-Value" değer kategorisindeki ifadelere bağlanırken, "T&&" olanlar sadece "const" olmayan "R-Value" değer kategorisindeki ifadelere bağlanır. Buradan hareketle kaynağını üzerimize alacağımız, bir diğer deyişle çalacağımız, ifadeler için "T&&", diğer ifadeler için ise "const T&" kullanılır. Yani "L-Value" ifadeleri "const T&" parametreli, "R-Value" ifadeleri ise "T&&" parametreli fonksiyonlar alacaktır. * Örnek 1, class Myclass {}; /* * Böylesi bir parametreye sahip fonksiyon, büyük ihtimal ile gövdesine * ya "Copy Ctor" ya da "Copy Assignment" çağıracaktır. Bu yönde bir izlenim * vermektedir. */ void foo(const Myclass& r) { //... Myclass m = r; // Copy Ctor. //... Myclass mm; mm = r; // Copy Assignment //... } /* * Böylesi bir parametreye sahip fonksiyon, büyük ihtimal ile gövdesine * ya "Move Ctor" ya da "Move Assignment" çağıracaktır. Bu yönde bir izlenim * vermektedir. */ void foo(Myclass&& r) { //... Myclass m = std::move(r); // Move Ctor. //... Myclass mm; mm = std::move(m); // Move Assignment } int main() { //... } >>>> "Type Deduction" sırasında: Şimdi burada "std::move" fonksiyonu devreye girmektedir. Anımsayacağınız üzere "Move, does not move". Yani "L-Value" olarak gelen "R-Value" olarak çıkar. "R-Value" gelen yine "R-Value" olarak çıkar. Bu fonksiyonun yaptığını da "static_cast" ile de yapabilirdik. Yani "static_cast(exp)" ifadesi ile "std::move(exp)" ifadesi arasında bir fark YOKTUR. Dolayısıyla aşağıdaki örneği inceleyelim: * Örnek 1, #include #include template constexpr void Move(T&&) noexcept { //... } template void func(T&& r) { /* * Buradaki "T&&" ifadesi "Universal Referance" demektir. Böylesi bir referansta ise kurallar şu * şekildedir: * Bu fonksiyona "L-Value" ifade geçilirse, fonksiyonun parametresi "L-Value Reference" biçiminde * olacaktır. Çünkü "T" türüne karşılık "L-Value Reference" gelmiştir. "Reference Collapsing" kuralı * gereği fonksiyonun parametresi "L-Value Reference" olmuştur. Bu fonksiyona "R-Value" ifade * geçilirse, fonksiyonun parametresi "R-Value Reference" biçiminde olacaktır. Çünkü "T" türüne * karşılık referans olmayan bir tür gelmiştir. "Reference Collapsing" kuralı gereği fonksiyonun * parametresi "R-Value Reference" olmuştur. */ } int main() { } * Örnek 2, "T&&" doğrudan yalın biçimde bulunmadıkça "Universal Reference" değildir. //... /* Aşağıdaki fonksiyon şablonu bildiriminde, "r" bir "universal reference" biçimindedir. */ template void func(T&& r); /* Aşağıdaki fonksiyon şablonu bildiriminde, "r" bir "universal reference" biçiminde DEĞİLDİR. */ template void func(const T&& r); /* Aşağıdaki fonksiyon şablonu bildiriminde, "r" bir "universal reference" biçiminde DEĞİLDİR. */ template void foo(std::vector&& r); /* Aşağıdaki "foo" fonksiyon bildiriminde, "r" bir "universal reference" biçiminde DEĞİLDİR. */ template class Myclass { public: void foo(T&& r); }; /* Aşağıdaki fonksiyon şablonu bildiriminde, "r" bir "universal reference" biçimindedir. */ template class Myclasss { public: template void foo(U&& r); }; template class Nec { public: struct MyStruct { }; }; /* Aşağıdaki "myfunc" fonksiyon bildiriminde, "r" bir "universal reference" biçiminde DEĞİLDİR. */ template void myfunc(Nec::MyStruct&& r); //... Normal şartlarda C++ dilinde "Reference to Reference" mevcut değildir. Fakat bazı durumlarda böylesi bir durum gerçekleşmektedir. İşte bu durum gerçekleştiğinde de "Reference Collapsing" mekanizması devreye girmektedir. Pekiyi "Reference Collapsing" mekanizması nasıl işler? Sağ taraf referansı ile sağ taraf referansı karşılaşırsa, sonuçta sağ taraf referansı; sol taraf referansı ile herhangi bir referans karşılaşırsa sonuçta sol taraf referansı ortaya çıkar. * Örnek 1.0, Aşağıdaki gibi parametresinin "Universal Reference" olması durumunda "Referance Collapsing" devreye girmektedir. //... template void func(T&& r); //... * Örnek 1.1, Yine "auto&&" kullanılması durumunda. * Örnek 1.2, Aşağıdaki durumda da "Referance Collapsing" olacaktır. template void func(T& t) { /* * "T" yerine "int&&" gelecektir. "T&& &" sonucu "T&" olacağı için, * "a" değişkeni bir "L-Value Referance" olacaktır. */ /* * "T" yerine "int&&" gelecektir. */ T a = 100; // int&& a = 100; /* * "T" yerine "int&&" gelecektir. "T&& &&" sonucu "T&&" olacağı için, * "b" değişkeni bir "R-Value Referance" olacaktır. */ T&& b = 45; // int&& b = 45; } int main() { int x{}; func(x); } * Örnek 2, Aşağıdaki gibi "using" bildirimlerinin kullanıldığı yerlerde de "Reference Collapsing" devreye girer. //... class Myclass {}; using LREF = Myclass&; using RREF = Myclass&&; int main() { { /* "Reference Collapsing" gereği "r" değişkeni bir "L-Value Reference" biçimindedir. */ LREF& r = Myclass{}; // T& & => T& Myclass m; /* "Reference Collapsing" gereği "r" değişkeni bir "L-Value Reference" biçimindedir. */ LREF& rr = m; // T& & => T& /* "Reference Collapsing" gereği "r" değişkeni bir "L-Value Reference" biçimindedir. */ LREF&& rrr = Myclass{}; // T& && => T& /* "Reference Collapsing" gereği "r" değişkeni bir "L-Value Reference" biçimindedir. */ LREF&& rrrr = m; // T& && => T& } { /* "Reference Collapsing" gereği "r" değişkeni bir "L-Value Reference" biçimindedir. */ RREF& r = Myclass{}; // T&& & => T& Myclass m; /* "Reference Collapsing" gereği "r" değişkeni bir "L-Value Reference" biçimindedir. */ RREF& rr = m; // T&& & => T& /* "Reference Collapsing" gereği "r" değişkeni bir "R-Value Reference" biçimindedir. */ RREF&& rrr = Myclass{}; // T&& && => T&& /* "Reference Collapsing" gereği "r" değişkeni bir "R-Value Reference" biçimindedir. */ RREF&& rrrr = m; // T&& && => T& } } * Örnek 3.0, "decltype" kullanımı sırasında da "Referance Collapsing" devreye girmektedir. //... int main() { int x{ 234 }; /* "x" is an int. */ int* ptr{ &x }; /* "ptr" is a pointer to "x". */ decltype(*ptr)& r = x; /* "r" is an "L-Value Reference" to "x". */ /* int& & r = x; // T& & => T& */ /* int& r = x; */ } * Örnek 3.1, //... int&& foo(); int main() { decltype(foo())&& i = 100; /* "r" is an "R-Value Reference" to "x". */ /* int&& && i = 100; // T&& && => T&& */ /* int&& i = 100; */ } Pekiyi bu "std::move" fonksiyonu tam olarak ne işe yaramaktadır? İşin özünde bu fonksiyon bir nesnenin taşınabilir olmasını, bir diğer deyişle kaynaklarının çalınabilir olmasını sağlamaktadır. Böylelikle ilgili nesnenin kaynaklarını başka bir nesneye kopyalamak yerine direkt olarak taşıyoruz. Artık bizim nesnemizin kaynakları taşındığı için kendisi "Moved From State" halindedir. C++ standartları ise bu haldeki nesneler için şu garantiyi vermektedir; Değeri bilinmeyen fakat geçerli nesneler. Bu durumu fonksiyonların parametre değişkenlerine benzetebiliriz. O parametreye hangi değerin geçileceğini önceden bilmek mümkün değildir fakat o parametre geçerlidir. Burada dikkat etmemiz gereken, "Moved From State" halindeki bir nesneyi kullanmamız gerekiyor ise kullanmalıyız fakat kendi değeri ile kullanmamalıyız. * Örnek 1, #include #include int main() { std::string str(10000, 'A'); std::cout << "size of 'str' : " << str.size() << "\n"; // OUTPUT : size of 'str' : 10000 auto s = std::move(str); std::cout << "size of 'str': " << str.size() << "\n"; // OUTPUT : size of 'str': 0 std::cout << "size of 's': " << s.size() << "\n"; // OUTPUT : size of 's': 10000 s = std::string(10000, 'A'); std::cout << "size of 's': " << s.size() << "\n"; // OUTPUT : size of 's': 10000 } Öte yandan "Moved From State" halindeki bir nesne ile "Default Init." kullanılarak hayata getirilen nesnenin aynı olması C++ ile garanti altına alınmamıştır. Benzer şekilde üçüncü parti kütüphaneleri kullanırken de ilgili kütüphanenin bu konuyla ilgili dökümanlarına bakılması gerekmektedir. Şimdi de bu fonksiyonu bizler yazalım: * Örnek 1, #include #include template struct RemoveReference { using type = T; }; /* Partial Specialization for L-Value References */ template struct RemoveReference { using type = T; /* Şablon argümanı "int&" ise "T" nin kendisi "int" olacaktır. */ }; /* Partial Specialization for R-Value References */ template struct RemoveReference { using type = T; /* Şablon argümanı "int&&" ise "T" nin kendisi "int" olacaktır. */ }; template using RemoveReference_t = typename RemoveReference::type; // Until C++20 // using RemoveReference_t = RemoveReference::type; // Since C++20 template RemoveReference_t&& MyMove(T&& t) { return static_cast&&>(t); } int main() { RemoveReference::type x{}; // int x{}; RemoveReference_t y{}; // int y{}; //... } * Örnek 2, //... template std::remove_reference_t&& MyMove(T&& t) { return static_cast &&(t); } int main() { //... } Şimdi de C++17 ile birlikte gelen bir özelliği inceleyelim: "PR-Value Expression" ın tanımı. C++17 öncesinde "PR-Value" ifadeler bir nesne belirtmekteydi. Fakat artık bir nesne belirtmekten ziyade artık bir yol gösterici konumundalar. Aşağıdaki örneği inceleyelim: * Örnek 1, #include #include class Myclass {}; Myclass foo() { return Myclass{}; } void bar(Myclass) {} int main() { /* * "=" operatörünün sağ tarafındaki ifade C++17 öncesinde bir nesneyi temsil etmekteydi. Sentaks * hatasının olmaması için ilgili sınıfın "Copy Ctor." / "Move Ctor." fonksiyonlarının olması * gerekebilmektedir. Bir takım "compiler" optimizasyonları ile bu gerekliliğin önüne geçilmektedir. * Fakat standart değildir. Artık C++17 ile birlikte "PR-Value" ifadeler artık bir nesneyi temsil * ETMEMEKTEDİR. Artık belirli koşullar sağlandığı taktirde "result object" oluşturan ifadelerdir. Bu * şartlar ise ya "initializer" olarak bir nesneyi "init." etmesi, fonksiyonun parametre değişkeni, * fonksiyonun geri dönüş değerini tutması, ifadenin "discard" edilmesi veya ilgili ifadenin * "const L-Value Reference" veya "R-Value Reference" a bağlanması sonucunda "result object" oluşur. Bu * durum kopyalama veya taşıma ile ilgili değildir. */ Myclass m = Myclass{}; /* * Bu fonksiyona argüman olan ifade yine bir nesneyi temsil etmekteydi. */ bar(Myclass{}); /* Geçici nesne artık "result object" olacaktır. */ const Myclass& r{ Myclass{} }; Myclass&& rr{ Myclass{} }; } * Örnek 2, #include #include class Myclass { public: Myclass(); Myclass(int); }; void bar(Myclass) {} int main() { /* * Derleyici optimizasyonundan bağımsız olarak * sadece bir defa "int" parametreli "Ctor." fonksiyonu * çağrılacaktır. Tabii C++17 sonrasında. */ bar(Myclass{12}); } * Örnek 3, #include #include class Myclass { public: Myclass(int) { std::cout << "Myclass(int)\n"; } Myclass(const Myclass&) = delete; }; Myclass bar(int x) { return Myclass{x}; } int main() { /* Yine sadece ve sadece "int" parametreli "Ctor." fonksiyonu çağrılacaktır. */ Myclass mx{ bar(31) }; // Myclass(int) auto my{ Myclass{Myclass{bar(31)}} }; // Myclass(int) } İşte burada yapılanları anlatan şu akronimlerde mevcuttur; "RVO". >>>> "RVO" : İki farklı tipte bulunur. Bunlar, "Unnamed Return Value Optimization" ve "Named Return Value Optimization" ismindedirler. >>>>> "Unnamed Return Value Optimization" : Artık C++ dilinde optimizasyon olarak görülmemektedir. Burada "Mandotary Copy Ellision" devreye girmektedir. >>>>> "Named Return Value Optimization" : Halen C++ dilinde optimizasyon olarak görülmektedir. > "Copy Ellision" : Bu terim hayata gelmiş olan iki nesneyi birbirine atarkenki durum için kullanılmaz. Her ne kadar bu atama işlemi için sınıfın "Move" fonksiyonlarını kullanmak maliyeti düşürsede, ortada hayata gelmiş nesne olduğu için burada "Copy Ellision" söz konusu değildir. Pekiyi nerelerde görülür? Şu noktalarda "Copy Ellision" dan söz edebiliriz: Bir objeyi hayata getirirken, "return" deyimlerinde, "throw" ifadelerinde ve hatanın "catch" edildip işlendiği "catch" blok içerisinde. "Copy Ellision" ise kendi içerisinde birkaç farklı ifade barındırır. Bunlar, "Mandotary Copy Ellision" ve "Non-Mandotary Copy Ellision". şeklindedir. Şimdi de bunları irdeleyelim: >> "Mandotary Copy Ellision" : Anımsayacağınız üzere C++17 ile birlikte "PR_Value" değer kategorisi bir nesneye karşılık değil, bir nesne oluştururken kullanılacak reçete konumundadır. Ne zamanki bir "Matererialization" gerçekleşir, işte o zaman "PR_Value to X_Value" dönüşümü meydana gelir ve bir "Result Object" oluşur, işte o zaman bir nesneye karşılık gelir. Eğer iş bu "Matererialization" gerçekleşmezse, doğrudan bir nesneden bahsetmemiz mümkün değildir. İşte bu duruma da "Mandotary Copy Ellision" denmektedir. Bu durumda ilgili sınıfın "Move Ctor.", "Copy Ctor." veya her ikisinin de çağrılabilir olması önemli değildir. C++17 öncesinde bu fonksiyonların çağrılması sentaks hatası oluştururken, artık SENTAKS HATASI OLUŞTURMAYACAKTIR. Çünkü artık bu bir optimizasyon DEĞİLDİR. "Mandotary Copy Ellision" da bir kaç yerde görülmektedir. Bunlar, "Return Value Optimization" ve "Unmaterialized Object Passing" yerleridir. Şimdi de bunları irdeleyelim: >>> "Return Value Optimization" : Bu ise kendi içerisinde ikiye ayrılır. Bunlar, "Unnamed Return Value Optimization" ve "Named Return Value Optimization". biçimindedir. Bunlardan, >>>> "Unnamed Return Value Optimization" : Böylesi yerler artık bir "Mandotary Copy Ellision" olarak geçmektedir. >>>> "Named Return Value Optimization" : Böylesi yerler hala derleyiciler açısından optimizasyon olarak kabul edilmektedir. Dolayısıyla bu tip yerlerde ilgili sınıfın "Move Ctor.", "Copy Ctor." veya her ikisinin de çağrılabilir OLMAMASI sentaks hatasına yol açacaktır. Bir diğer deyişle derleyici böylesi bir optimizasyonu yapmak gibi bir zorunluluğu yoktur. Hatta yapabilmek gibi bir zorunluluğu da yoktur. Dolayısıyla bu durum derleyiciye bağlıdır. Ek olarak bizim "Copy Ctor." ve "Move Ctor." fonksiyonlarımız yan etkiye sahipse, örneğin "global" isim alanındaki bir nesnenin değerini değiştiriyorsa, bu yan etkinin gerçekleşeceğin veya gerçekleşmeyeceğini varsayarak kod YAZMAMALIYIZ. Burada derleyici bu yan etkiyi önemsemeyecektir. Yani derleyicinin böylesi bir optimizasyonu yapacağını varsayarak yukarıdaki gibi bir yan etki oluşturmamalıyız veya tam tersi. >>> "Unmaterialized Object Passing" : Kabaca isimsiz nesne kullanımıdır. Zaman zaman isimsiz nesne kullanmak bizim faydamıza olabilir. Şimdi de aşağıdaki pekiştirici örnekleri inceleyelim: * Örnek 1, C++17 ile birlikte artık sadece bir defa "Default Ctor." çağrılacaktır. #include class Myclass { public: Myclass() { std::cout << "Default Ctor.\n"; } Myclass(int) { std::cout << "Myclass(int)\n"; } Myclass& operator=(const Myclass&) { std::cout << "Copy Assignment\n"; return *this; } Myclass& operator=(Myclass&&){ std::cout << "Move Assignment\n"; return *this; } ~Myclass() { std::cout << "Destructor\n"; } private: }; void foo(Myclass x) { } int main() { /* # OUTPUT #
Default Ctor. Destructor
*/ std::cout << "
\n"; { foo(Myclass{}); } std::cout << "
\n"; } * Örnek 2, C++17 ile birlikte artık "m" nesnesi için sadece bir defa "Default Ctor." çağrılacaktır. #include class Myclass { public: Myclass() { std::cout << "Default Ctor.\n"; } Myclass(int) { std::cout << "Myclass(int)\n"; } Myclass& operator=(const Myclass&) { std::cout << "Copy Assignment\n"; return *this; } Myclass& operator=(Myclass&&){ std::cout << "Move Assignment\n"; return *this; } ~Myclass() { std::cout << "Destructor\n"; } private: }; int main() { /* # OUTPUT #
Default Ctor. Destructor
*/ std::cout << "
\n"; { Myclass m{ Myclass{Myclass{Myclass{}}} }; } std::cout << "
\n"; } * Örnek 3, C++17 ile birlikte artık "m" nesnesi için sadece bir defa "Ctor." çağrılacaktır. Yani "Matererialization" gerçekleşmiştir. #include class Myclass { public: Myclass() { std::cout << "Default Ctor.\n"; } Myclass(int) { std::cout << "Myclass(int)\n"; } Myclass& operator=(const Myclass&) { std::cout << "Copy Assignment\n"; return *this; } Myclass& operator=(Myclass&&){ std::cout << "Move Assignment\n"; return *this; } ~Myclass() { std::cout << "Destructor\n"; } private: }; Myclass foo(int x) { return Myclass{ x }; } int main() { std::cout << "
\n"; { /* # OUTPUT #
Myclass(int) Destructor
*/ auto m{foo(123)}; } std::cout << "
\n"; } * Örnek 4, Aşağıdaki örnekte artık bir kopyalama söz konusu değildir. Referans, geçici bir nesneye bağlanmaktadır. Yani "Matererialization" gerçekleşmiştir. #include class Myclass { public: Myclass() { std::cout << "Default Ctor.\n"; } Myclass(int) { std::cout << "Myclass(int)\n"; } Myclass& operator=(const Myclass&) { std::cout << "Copy Assignment\n"; return *this; } Myclass& operator=(Myclass&&){ std::cout << "Move Assignment\n"; return *this; } ~Myclass() { std::cout << "Destructor\n"; } private: }; void foo(const Myclass&) {} int main() { std::cout << "
\n"; { /* # OUTPUT #
Myclass(int) Destructor
*/ foo(Myclass{31}); } std::cout << "
\n"; } * Örnek 5, Artık aşağıdaki örneklerde "Mandotary Copy Ellision" dan söz edemeyiz çünkü isimlendirilmiş nesne kullanıyoruz. Artık sınıfın "Copy Ctor." ve "Move Ctor." fonksiyonlarının çağrılabilir olması gerekmektedir. #include class Myclass { public: Myclass() { std::cout << "Default Ctor.\n"; } Myclass(int) { std::cout << "Myclass(int)\n"; } Myclass(const Myclass&) { std::cout << "Copy Ctor.\n"; } Myclass(Myclass&&) { std::cout << "Move Ctor.\n"; } Myclass& operator=(const Myclass&) { std::cout << "Copy Assignment\n"; return *this; } Myclass& operator=(Myclass&&){ std::cout << "Move Assignment\n"; return *this; } ~Myclass() { std::cout << "Destructor\n"; } private: }; void foo(Myclass) {} int main() { std::cout << "
\n"; { /* # OUTPUT #
Default Ctor. Copy Ctor. Destructor Destructor
*/ Myclass m; foo(m); } { /* # OUTPUT #
Default Ctor. Move Ctor. Destructor Destructor
*/ Myclass m; foo(std::move(m)); } std::cout << "
\n"; } * Örnek 6, #include class Myclass { public: Myclass() { std::cout << "Default Ctor.\n"; } Myclass(int) { std::cout << "Myclass(int)\n"; } Myclass& operator=(const Myclass&) { std::cout << "Copy Assignment\n"; return *this; } Myclass& operator=(Myclass&&){ std::cout << "Move Assignment\n"; return *this; } ~Myclass() { std::cout << "Destructor\n"; } int foo() { std::cout << "Myclass::foo()\n"; return 31; } private: }; int main() { std::cout << "
\n"; puts("---------------"); { /* # OUTPUT #
Default Ctor. Destructor
*/ Myclass{}; } puts("---------------"); { /* # OUTPUT #
Default Ctor. Destructor
*/ const Myclass& r = Myclass{}; // Burada "Copy Ellision" dan bahsemeyiz. Çünkü "Matererialization" // gerçekleşmiştir. } puts("---------------"); { /* # OUTPUT #
Default Ctor. Destructor
*/ Myclass&& r = Myclass{}; // Burada "Copy Ellision" dan bahsemeyiz. Çünkü "Matererialization" // gerçekleşmiştir. } puts("---------------"); { /* # OUTPUT # Default Ctor. Myclass::foo() Destructor */ auto value{ Myclass{}.foo() }; // Burada "Copy Ellision" dan bahsemeyiz. Çünkü "Matererialization" // gerçekleşmiştir. } puts("---------------"); std::cout << "
\n"; } * Örnek 7, "Named Return Value Optimization" yapabilmemiz için sınıfın ilgili fonksiyonlarının çağrılabilir olması gerekmektedir. #include class Myclass { public: Myclass() { std::cout << "Default Ctor.\n"; } Myclass(int) { std::cout << "Myclass(int)\n"; } Myclass(const Myclass&) { std::cout << "Copy Ctor.\n"; } Myclass(Myclass&&) { std::cout << "Move Ctor.\n"; } Myclass& operator=(const Myclass&) { std::cout << "Copy Assignment\n"; return *this; } Myclass& operator=(Myclass&&){ std::cout << "Move Assignment\n"; return *this; } ~Myclass() { std::cout << "Destructor\n"; } private: }; Myclass foo() { Myclass m; /* * "Named Return Value Optimization" gerçekleştirebilmek için sınıfın * "Move Ctor." ve "Copy Ctor." fonksiyonlarının ÇAĞRILABİLİR OLMASI * gerekmektedir. Aksi halde sentaks hatası alacağız. */ return m; } int main() { std::cout << "
\n"; foo(); std::cout << "
\n"; } * Örnek 8, "Named Return Value Optimization" yapabilmemiz için sınıfın ilgili fonksiyonlarının çağrılabilir olması gerekmektedir. #include class Myclass { public: Myclass() { std::cout << "Default Ctor.\n"; } Myclass(int) { std::cout << "Myclass(int)\n"; } Myclass(const Myclass&) { std::cout << "Copy Ctor.\n"; } Myclass(Myclass&&) { std::cout << "Move Ctor.\n"; } Myclass& operator=(const Myclass&) { std::cout << "Copy Assignment\n"; return *this; } Myclass& operator=(Myclass&&) { std::cout << "Move Assignment\n"; return *this; } ~Myclass() { std::cout << "Destructor\n"; } private: }; /* * "foo" fonksiyonunu çağıran kod, aynı zamanda "return" * değerinin yazılacağı adresi de bu fonksiyona geçiyor. * Dolayısıyla "mx" nesnesi o adreste hayata geliyor. İşte * bu "Named Return Value Optimization" oluyor. */ Myclass foo(int value) { std::cout << "<>\n"; Myclass mx(value); std::cout << "&mx : " << &mx << "\n"; std::cout << "<>\n"; return mx; } int main() { /* # OUTPUT #
<> Myclass(int) &mx : 000000F7580FF694 <> &mx : 000000F7580FF694 Destructor
*/ std::cout << "
\n"; { Myclass mx = foo(123); std::cout << "&mx : " << &mx << "\n"; } std::cout << "
\n"; } * Örnek 9.1, Aşağıdaki örnekte ise "Copy Ctor." fonksiyonu olmamasına rağmen "Move Ctor." olmasından dolayı herhangi bir sentaks hatası almamış olduk. Dolayısıyla geri dönüş değerini direkt olarak bir sınıf olarak da belirleyebiliriz. Böylelikle hem kod yazma hem de kod okuma daha rahat olacaktır. Çünkü burada "mx" otomatik ömürlü. #include class Myclass { public: Myclass() { std::cout << "Default Ctor.\n"; } Myclass(int) { std::cout << "Myclass(int)\n"; } Myclass(Myclass&&) { std::cout << "Move Ctor.\n"; } Myclass& operator=(const Myclass&) { std::cout << "Copy Assignment\n"; return *this; } Myclass& operator=(Myclass&&) { std::cout << "Move Assignment\n"; return *this; } ~Myclass() { std::cout << "Destructor\n"; } private: }; Myclass foo(int value) { std::cout << "<>\n"; Myclass mx(value); std::cout << "&mx : " << &mx << "\n"; std::cout << "<>\n"; return mx; } int main() { /* # OUTPUT #
<> Myclass(int) &mx : 000000F7580FF694 <> &mx : 000000F7580FF694 Destructor
*/ std::cout << "
\n"; { Myclass mx = foo(123); std::cout << "&mx : " << &mx << "\n"; } std::cout << "
\n"; } * Örnek 9.2, Benzer örnek aşağıdada vardır. #include #include #include auto foo() { return std::make_unique("ulya yuruk"); } int main() { /* # OUTPUT #
ulya yuruk
*/ std::cout << "
\n"; { auto mx = foo(); std::cout << *mx << "\n"; } std::cout << "
\n"; } * Örnek 9.3, Aşağıdaki örnekte de "Named Return Value Optimization" gerçekleşmemiştir. #include #include #include class Myclass { public: Myclass() { std::cout << "Default Ctor.\n"; } Myclass(int) { std::cout << "Myclass(int)\n"; } Myclass(const Myclass&) { std::cout << "Copy Ctor.\n"; } Myclass(Myclass&&) { std::cout << "Move Ctor.\n"; } Myclass& operator=(const Myclass&) { std::cout << "Copy Assignment\n"; return *this; } Myclass& operator=(Myclass&&) { std::cout << "Move Assignment\n"; return *this; } ~Myclass() { std::cout << "Destructor\n"; } private: }; /* * Burada artık "Named Return Value Optimization" söz konusu değildir. * Çünkü "x" in adresi zaten belli. Dolayısıyla bu fonksiyonu çağıran * kodun gönderdiği ve geri dönüş değerinin yazılacağı adres bilgisini * kullanamıyoruz. Çünkü "x" değişkeni halihazırda bir adrese sahip. */ Myclass foo(Myclass x) { return x; } // (2) Copy Ctor. for "x". int main() { /* # OUTPUT #
Default Ctor. Copy Ctor. Move Ctor. Destructor Destructor Destructor
*/ std::cout << "
\n"; { Myclass mx{}; // (1) Default Ctor. Myclass my{foo(mx)}; // (3) Move Ctor. for "my" } std::cout << "
\n"; } * Örnek 9.4, Aşağıdaki kodların bazılarında "NRVO" yapılamamıştır. Çünkü farklı durumlarda farklı nesneler "return" edilmek istenmiştir. Fakat bu durumu bazı derleyiciler gidermiştir. Özellikle "f4" ve "f5" fonksiyonlarına bakabiliriz. #include #include #include class Myclass { public: Myclass() { std::cout << "Default Ctor.\n"; } Myclass(int) { std::cout << "Myclass(int)\n"; } Myclass(const Myclass&) { std::cout << "Copy Ctor.\n"; } Myclass(Myclass&&) { std::cout << "Move Ctor.\n"; } Myclass& operator=(const Myclass&) { std::cout << "Copy Assignment\n"; return *this; } Myclass& operator=(Myclass&&) { std::cout << "Move Assignment\n"; return *this; } ~Myclass() { std::cout << "Destructor\n"; } private: }; void foo() {} void bar() {} Myclass f1(int x) { Myclass m{x}; foo(); /* * Bu operatörün operandları "L_Value" olduğu için derleyici * "NRVO" gerçekleştiremiyor. Hatta "Move Ctor." bile * çağrılmayacaktır. */ return x > 10 ? m : m; /* * Bu durumda artık yine "NRVO" gerçekleşmeyecektir. Fakat artık * "Move Ctor." çağrılacaktır. */ // if(x > 10) return m; else return m; /* * Bu durumda da yine "NRVO" gerçekleşmeyecektir. Fakat ilgili * operatörün operandları "R_Value" olması hasebiyle "Move Ctor." * çağrılacaktır. */ // return x > 10 ? Myclass{3} : Myclass{2}; } Myclass f2(int x) { /* * Bu durumda da yine "NRVO" gerçekleşmeyecektir. Fakat ilgili * sınıfın "Move Ctor." fonksiyonu çağrılacaktır. */ auto m = Myclass(x); if (x > 10) return m; return Myclass{x+5}; } Myclass f3(int x) { /* * Bu durumda da yine "NRVO" gerçekleşmeyecektir. Fakat ilgili * sınıfın "Move Ctor." fonksiyonu çağrılacaktır. Fakat "clang" * derleyici BURADA "RVO" GERÇEKLEŞTİRMİŞTİR. İLGİLİ DERLEYİCİDE * ÇALIŞTIRILDIĞINDA SADECE BİR ADET "Ctor." ve "Dtor." ÇAĞRISI * OLDUĞU GÖRÜLECEKTİR. */ if (x > 10) { Myclass mx(x); foo(); return mx; } else { return Myclass{x+3}; } } Myclass f4(int x) { /* * Aşağıdaki kod için bazı derleyiciler "NRVO" gerçekleştirirken, * bazıları gerçekleştirememektedir. Gerçekleşmesi halinde sadece * bir adet "Ctor." ve "Dtor." çağrılırken, gerçekleşmemesi halinde * ilgili "Ctor.", "Move Ctor." çağrısı ve "Dtor." çağrıları * yapılacaktır. */ if (x > 10) { Myclass mx(x); foo(); return mx; } else { Myclass my(x+5); bar(); return my; } } Myclass f5_a(int x) { Myclass mx(x); foo(); return mx; } Myclass f5_b(int x) { Myclass mx(x + 5); bar(); return mx; } Myclass f5(int x) { /* Bu fonksiyon çağrısı sonrasında da "NRVO" SAĞLANMIŞTIR. */ if (x > 10) return f5_a(x); else return f5_b(x); } int main() { std::cout << "
\n"; { /* # OUTPUT #
Myclass(int) Copy Ctor. Destructor Destructor
*/ auto mx = f1(123); } puts("------------------------------------"); { /* # OUTPUT #
Myclass(int) Move Ctor. Destructor Destructor
*/ auto mx = f2(123); } puts("------------------------------------"); { /* # OUTPUT #
Myclass(int) Move Ctor. Destructor Destructor
*/ auto mx = f3(123); } puts("------------------------------------"); { /* # OUTPUT #
Myclass(int) Destructor
*/ auto mx = f4(123); } puts("------------------------------------"); { /* # OUTPUT #
Myclass(int) Destructor
*/ auto mx = f5(123); } puts("------------------------------------"); std::cout << "
\n"; } * Örnek 10, Eğer bir yerde "RVO" nun yapılıp yapılmadığından emin olmak istiyorsak, aşağıdaki koddan yararlanabiliriz: #include #include #include struct force_rvo { force_rvo(int) { std::cout << "force_rvo(int)\n"; } force_rvo(const force_rvo&); // Do not implement. force_rvo(force_rvo&&); // Do not implement. }; force_rvo foo() { //... } int main() { std::cout << "
\n"; { /* # OUTPUT # */ /* * Eğer "Copy Ellision" gerçekleşirse, "Copy Ctor." ve "Move Ctor." * fonksiyonları çağrılmayağı için bir sorun olmayaktı. * Fakat gerçekleşmemesi durumunda ilgili fonksiyonlara çağrı * yapılacaktır. Her iki fonksiyon da bildirildiği için sentaks * hatası almayacağız. Fakat tanımları olmadığı için "linker" * hatası alacağız. İşte böylelikle "Copy Ellision" yapılıp * yapılmadığını da test edebiliriz. */ } std::cout << "
\n"; } Son kez özetlersek; -> Parametre değişkenini "return" ediyorsak, burada kimse "Return Value Optimization" YAPAMAZ. Çünkü ilgili fonksiyonu çağıran kod, ilgili fonksiyona ayrıca "return" deyiminin yazılacağı adresi de geçmektedir. Parametre değişkeni halihazırda bir adrese sahip olduğundan, fonksiyona geçilen bu adres değeri KULLANILMAYACAKTIR. -> Belirgin şekilde iki farklı nesne varsa, fonksiyonun parametre değişkeni olmayan ve otomatik ömürlü, ve bir şarta bağlı olarak ikisinden birini döndürüyorsak, yine kimse "Return Value Optimization" YAPAMAZ. Çünkü hangi nesnenin ilgili fonksiyona geçilen ve "return" deyiminin yazılacağı adresi kullanacağı belli değil. -> Kalıtım uygulandığında ve "dynamic type" söz konusu ise yine "Return Value Optimization" yapma ihtimali kalmamaktadır. -> Atama yaparken "Copy Ellision" dan söz edemeyiz. Sadece "kopyalayan atama operaötrü" yerine "taşıyan atama operatörünün" kullanılması fayda sağlayabilir. > Taşıma Semantiği: Artık kullanılmayacak bir nesne söz konusu olduğunda, o nesneyi başka bir yere kopyalamak yerine o nesnenin kaynağını çalmaktır. Taşıma Semantiği şu senaryolarda devre dışı kalacaktır; >> Fonksiyonun geri dönüş değeri "const T" olması durumunda devreye girmeyecektir. Çünkü ekseriyetle taşıma semantiği için tür "T&&" türüdür. Bu tür de "const T" türüne bağlanamayacağı için, "const T&" türüne bağlanacaktır. Dolayısıyla "Copy Ctor." / "Copy Assignment" fonksiyonu çağrılacaktır. * Örnek 1, Eğer aşağıdaki örnekte "const T&" parametreli fonksiyona ilaveten "const T&&" parametreli bir fonksiyon yazsaydık, işte o fonksiyon çağrılacaktır. #include #include #include #include #include #include class Myclass { public: Myclass() { std::cout << "Default Ctor.\n"; } Myclass(int) { std::cout << "Myclass(int)\n"; } Myclass(const Myclass&) { std::cout << "Copy Ctor.\n"; } Myclass(Myclass&&) { std::cout << "Move Ctor.\n"; } Myclass& operator=(const Myclass&) { std::cout << "Copy Assignment\n"; return *this; } Myclass& operator=(Myclass&&) { std::cout << "Move Assignment\n"; return *this; } ~Myclass() { std::cout << "Destructor\n"; } private: }; const Myclass foo(int x) { Myclass mx(x); return mx; } int main() { std::cout << "
\n"; { /* # OUTPUT #
Default Ctor. Myclass(int) Copy Assignment Destructor Destructor
*/ Myclass mx; mx = foo(123); } std::cout << "
\n"; } >> Sınıf türünden geri dönüş değeri olan bir fonksiyonun "return" ifadesinde "std::move" çağrısı yapmak. İngilizce'de "Pessimistic Move" denmektedir. Çünkü bu durumda "Copy Ellision" olabilecekken "Move Ctor." çağrılmaktadır. * Örnek 1, #include #include #include #include #include #include class Myclass { public: Myclass() { std::cout << "Default Ctor.\n"; } Myclass(int) { std::cout << "Myclass(int)\n"; } Myclass(const Myclass&) { std::cout << "Copy Ctor.\n"; } Myclass(Myclass&&) { std::cout << "Move Ctor.\n"; } Myclass& operator=(const Myclass&) { std::cout << "Copy Assignment\n"; return *this; } Myclass& operator=(Myclass&&) { std::cout << "Move Assignment\n"; return *this; } ~Myclass() { std::cout << "Destructor\n"; } private: }; Myclass foo(int x) { Myclass mx(x); return std::move(mx); } Myclass bar(int x) { Myclass mx(x); return mx;; } Myclass far() { Myclass mx; return mx; } int main() { std::cout << "
\n"; puts("-----------------------"); { /* # OUTPUT # Myclass(int) Move Ctor. Destructor Destructor */ Myclass mx = foo(123); } puts("-----------------------"); { /* # OUTPUT # Myclass(int) Destructor */ Myclass mx = bar(123); } puts("-----------------------"); { /* # OUTPUT # Default Ctor. Destructor */ Myclass mx = far(); } puts("-----------------------"); std::cout << "
\n"; } Öte yandan bazı senaryolarda bu mekanizmadan faydalanmamız bizim karımızadır. * Örnek 1, Aşağıdaki örnekte "Move" işleminin "Copy" işleminden daha karlı olacağı varsayılarak "Nec" sınıfına "T" parametreli bir "Copy Ctor." yazılmıştır. Normal şartlarda "Copy Ctor." fonksiyonu için "const T&" parametrik yapısı seçilir. #include #include #include #include #include #include class Myclass { public: Myclass() { std::cout << "Default Ctor.\n"; } Myclass(int) { std::cout << "Myclass(int)\n"; } Myclass(const Myclass&) { std::cout << "Copy Ctor.\n"; } Myclass(Myclass&&) { std::cout << "Move Ctor.\n"; } Myclass& operator=(const Myclass&) { std::cout << "Copy Assignment\n"; return *this; } Myclass& operator=(Myclass&&) { std::cout << "Move Assignment\n"; return *this; } ~Myclass() { std::cout << "Destructor\n"; } private: }; class Nec { public: /* Eski C++ da böyle bir kod asla yazılmazdı. C++17 itibariyle bize kazanç sağlayabilir. */ Nec(Myclass m) : mx{std::move(m)} {} private: Myclass mx; }; int main() { std::cout << "
\n"; puts("-----------------------"); { /* # OUTPUT # Myclass(int) Move Ctor. Destructor Destructor */ Nec mynec{ Myclass{5} }; } puts("-----------------------"); std::cout << "
\n"; } * Örnek 2, Pekiştirici bir örnek: #include #include #include #include #include #include class Myclass { public: Myclass() { std::cout << "Default Ctor.\n"; } Myclass(int) { std::cout << "Myclass(int)\n"; } Myclass(const Myclass&) { std::cout << "Copy Ctor.\n"; } Myclass(Myclass&&) { std::cout << "Move Ctor.\n"; } Myclass& operator=(const Myclass&) { std::cout << "Copy Assignment\n"; return *this; } Myclass& operator=(Myclass&&) { std::cout << "Move Assignment\n"; return *this; } ~Myclass() { std::cout << "Destructor\n"; } private: }; class Nec { public: /* Eski C++ da böyle bir kod asla yazılmazdı. C++17 itibariyle bize kazanç sağlayabilir. */ Nec(Myclass m) : mx{std::move(m)} {} private: Myclass mx; }; int main() { std::cout << "
\n"; puts("-----------------------"); { /* # OUTPUT # Myclass(int) Move Ctor. Destructor Destructor */ Nec mynec{ Myclass{5} }; } puts("-----------------------"); { /* # OUTPUT # Default Ctor. Copy Ctor. Move Ctor. Destructor Destructor Destructor */ Myclass m; Nec mynec{ m }; } puts("-----------------------"); { /* # OUTPUT # Default Ctor. Move Ctor. Move Ctor. Destructor Destructor Destructor */ Myclass m; Nec mynec{ std::move(m) }; } puts("-----------------------"); std::cout << "
\n"; } * Örnek 3, Aşağıdaki "Nec" sınıfının "Ctor." fonksiyonları için üç alternafiften hangisi bize uygun ise onu seçmeliyiz. Şu an için tavsiye edilen birinci yoldur. #include #include #include #include #include #include class Myclass { public: Myclass() { std::cout << "Default Ctor.\n"; } Myclass(int) { std::cout << "Myclass(int)\n"; } Myclass(const Myclass&) { std::cout << "Copy Ctor.\n"; } Myclass(Myclass&&) { std::cout << "Move Ctor.\n"; } Myclass& operator=(const Myclass&) { std::cout << "Copy Assignment\n"; return *this; } Myclass& operator=(Myclass&&) { std::cout << "Move Assignment\n"; return *this; } ~Myclass() { std::cout << "Destructor\n"; } private: }; class Nec { public: // WAY - I Nec(Myclass m) : mx{std::move(m)} {} // WAY - II // Nec(const Myclass& m) : mx(m) {} // Nec(Myclass&& m) : mx(std::move(m)) {} // WAY - III // Nec(const Myclass& m) : mx(m) {} private: Myclass mx; }; int main() { //... } Buradan hareketle bazı sınıflar "Move Only" olacak biçimde tasarlanmışlardır. Yani "Move Ctor." ve "Move Assignment" fonksiyonları kullanıcı tarafından tarafından bildirmemiz halinde "Copy" fonksiyonları derleyici tarafından "delete" edilecektir. Fakat böyle yapmak yerine "Copy" fonksiyonları direkt olarak "delete" etmek daha iyi olabilir. Tabii bu noktada "Move" fonksiyonların tanımını programcının mı yoksa derleyicinin mi yaptığınının bir önemi de yoktur. * Örnek 1, class Myclass{ public: Myclass() = default; Myclass(const Myclass& other) = delete; Myclass& operator=(const Myclass& other) = delete; Myclass(Myclass&& other) = default; Myclass& operator=(Myclass&& other) = default; }; void func(Myclass) { } int main() { func(Myclass{}); // OK Myclass m; // func(m); // Sentaks Hatası: use of deleted function ‘Myclass::Myclass(const Myclass&)’ func(std::move(m)); // OK } Bu sınıflar "STL" içerisindeki bazı yerlerde kullanıldığında sentaks hatasına yol açarken, bazı "STL" öğeleri de arka planda taşıma işlemi gerçekleştirmektedir. Şöyleki; >> "std::initializer_list" sınıfı arka planda kopyalama yaptığı için "Move Only" sınıfların kullanılması sentaks hatasına yol açacaktır. >> "algorithm" başlık dosyası içerisindeki "remove", "remove_if" ve "unique" isimli fonksiyonlar gerçek silme yerine bir taşıma gerçekleştirmektedir. Dolayısıyla "Container" içerisindeki toplam eleman sayısı değişmemekte, sadece silinmek istenen öğeler ilgili "Container" içerisinde ötelenmektedir. "remove" ve "remove_if" fonksiyonlarının temsili implementasyonu ise şu şekildedir: template ForIt Remove(ForIt first, ForIt last, const T& value){ first = std::find(first, last, value); if(first != last) for(ForIt i = first; ++i != last;) if(!(*i == value)) *first++ = std::move(*i); return first; } template ForIt Remove_If(ForIt first, ForIt last, UnPred p){ first = std::find_if(first, last, p); if(first != last) for(ForIt i = first; ++i != last;) if(!p(*i)) *first++ = std::move(*i); return first; } Bu iki implementasyondan da görüleceği üzere "std::move(*i)" çağrısından dolayı ilgili nesneler "Moved From State" halini alabilmektedir. * Örnek 1, Aşağıdaki örnekten de görüleceği üzere, içerisinde "i" karakterini barındıranlar "s_vec" in sonuna ötelenmiştir fakat bu ötele işlemi sonucunda artık "Moved From State" halindedirler. #include #include #include #include int main() { /* # OUTPUT # 1 ali 2 fehmi 3 */ std::vector s_vec{ "onur", "inci", "esen", "ali", "fehmi", "turfan" }; auto iter = remove_if(s_vec.begin(), s_vec.end(), [](const auto& s){ return s.find('i') != std::string::npos; } ); int cnt{}; while(iter != s_vec.end()) std::cout << ++cnt << ' ' << *iter++ << '\n'; } Diğer yandan Taşıma Semantiği ile ilgili bir diğer önemli konu ise ilgili fonksiyonların "noexcept" garantisinin vermesidir. Şöyleki; * Örnek 1, Derleyicinin yazdığı özel fonksiyonlar "noexcept" garantisi vermektedir. #include #include class Nec {}; int main() { /* # OUTPUT #
Is Default Ctor. noexcept? : true Is Dtor. noexcept? : true Is Copy Ctor. noexcept? : true Is Move Ctor. noexcept? : true Is Copy Assign. noexcept? : true Is Move Assign. noexcept? : true
*/ using namespace std; std::cout << "
\n"; cout.setf(ios::boolalpha); cout << "Is Default Ctor. noexcept? : " << is_nothrow_default_constructible_v << "\n"; cout << "Is Dtor. noexcept? : " << is_nothrow_destructible_v << "\n"; cout << "Is Copy Ctor. noexcept? : " << is_nothrow_copy_constructible_v << "\n"; cout << "Is Move Ctor. noexcept? : " << is_nothrow_move_constructible_v << "\n"; cout << "Is Copy Assign. noexcept? : " << is_nothrow_copy_assignable_v << "\n"; cout << "Is Move Assign. noexcept? : " << is_nothrow_move_assignable_v << "\n"; std::cout << "
\n"; } * Örnek 2, Sınıfın veri elemanları "noexcept" garantisi vermiyorsa, bizim sınıfımız da vermeyecektir. #include #include #include class Nec { std::string m_name; }; int main() { /* # OUTPUT #
Is Default Ctor. noexcept? : true Is Dtor. noexcept? : true Is Copy Ctor. noexcept? : false Is Move Ctor. noexcept? : true Is Copy Assign. noexcept? : false Is Move Assign. noexcept? : true
*/ using namespace std; std::cout << "
\n"; cout.setf(ios::boolalpha); cout << "Is Default Ctor. noexcept? : " << is_nothrow_default_constructible_v << "\n"; cout << "Is Dtor. noexcept? : " << is_nothrow_destructible_v << "\n"; cout << "Is Copy Ctor. noexcept? : " << is_nothrow_copy_constructible_v << "\n"; cout << "Is Move Ctor. noexcept? : " << is_nothrow_move_constructible_v << "\n"; cout << "Is Copy Assign. noexcept? : " << is_nothrow_copy_assignable_v << "\n"; cout << "Is Move Assign. noexcept? : " << is_nothrow_move_assignable_v << "\n"; std::cout << "
\n"; } * Örnek 3, "Dtor." fonksiyonlar için "noexcept" garantisi verilmemesi abes bir durumdur. #include #include #include #include #include #include class Myclass { public: Myclass() { std::cout << "Default Ctor.\n"; } Myclass(int) { std::cout << "Myclass(int)\n"; } Myclass(const Myclass&) { std::cout << "Copy Ctor.\n"; } Myclass(Myclass&&) { std::cout << "Move Ctor.\n"; } Myclass& operator=(const Myclass&) { std::cout << "Copy Assignment\n"; return *this; } Myclass& operator=(Myclass&&) { std::cout << "Move Assignment\n"; return *this; } ~Myclass() { std::cout << "Destructor\n"; } private: }; class Nec { // Static assert is failed. "Copy Ctor." should be an "noexcept". static_assert(std::is_nothrow_copy_constructible_v); Myclass m_x; }; int main() { /* # OUTPUT #
Is Default Ctor. noexcept? : false Is Dtor. noexcept? : true Is Copy Ctor. noexcept? : false Is Move Ctor. noexcept? : false Is Copy Assign. noexcept? : false Is Move Assign. noexcept? : false
*/ using namespace std; std::cout << "
\n"; cout.setf(ios::boolalpha); cout << "Is Default Ctor. noexcept? : " << is_nothrow_default_constructible_v << "\n"; cout << "Is Dtor. noexcept? : " << is_nothrow_destructible_v << "\n"; cout << "Is Copy Ctor. noexcept? : " << is_nothrow_copy_constructible_v << "\n"; cout << "Is Move Ctor. noexcept? : " << is_nothrow_move_constructible_v << "\n"; cout << "Is Copy Assign. noexcept? : " << is_nothrow_copy_assignable_v << "\n"; cout << "Is Move Assign. noexcept? : " << is_nothrow_move_assignable_v << "\n"; std::cout << "
\n"; } Bütün bunlara ek olarak sınıfların sunacağı bir takım "exception" garanti etme seviyeleri vardır. Bunlar "basic guarantee", "strong guarantee" ve "noexcept". >> "Basic Guarantee" : Bir kaynak sızıntısı mevcut değil ve programın "state" i halen geçerli durumdadır. Bütün fonksiyonların bunu garanti etmesi gerekmektedir. Yani "exception" ile fonksiyonun akışından çıkıldığında, yukarıdaki şartların sağlanmış olması gerekmektedir. * Örnek 1, void foo() { auto p = new std::string{"Ahmet"}; /* * Eğer bu noktada bir "exception" gönderilirse, * programın akışı "foo" fonksiyonundan çıkacağı * için bir kaynak sızıntısı meydana gelecektir. * Dolayısıyla böylesi bir kod yazmaktan kaçınmalıyız. */ delete p; } >> "Strong Guarantee" : "Basic Guarantee" nin sunduğu garantileri de veriyor. Ek olarak bir "exception" ile fonksiyonun bünyesinden çıkıldığında program "state" inde bir değişiklik olmayacağı garantisi vermektedir. "Commit or Rollback" de denir. Ya işimi yaparım, işimi yapamazsam da önceki "state" i korurum. >> "noexcept" : Benden bir "exception" beklemeyin demektir. Eğer bir şekilde "exception" gönderilirse, "std::terminate" fonksiyonu çağrılacaktır. Şimdi de "Move Semantics" fonksiyonları ile "noexcept" garantisi arasındaki ilişkiyi irdeleyelim: * Örnek 1, "Student" sınıfının "Move Ctor." fonksiyonu "noexcept" olmadığı için ve çağrılan ".push_back" fonksiyonu "Strong Guarantee" verdiği için sunduğu için "reallocation" sonrasında ilgili isimler "Copy Ctor." ile çağrılmıştır. Bunun da nedeni şudur: "Copy Ctor." çağrısı sırasında bir "exception" gönderilirse, orjinal bilgiler hala değişmediği için "Commit or Rollback" mevzusu olacaktır. Fakat "Move Ctor." çağrısı sırasında kaynak çalındığı için bir "exception" gönderildiğinde "Commit or Rollback" mevzusu olmayacaktır. #include #include #include class Student { public: Student(const char* name) : name_{name} {} std::string getName() const { return name_; } Student(const Student& other) : name_{ other.name_ } { std::cout << "COPY: " << name_ << "\n"; } Student(Student&& other) : name_{ std::move(other.name_)} { std::cout << "MOVE: " << name_ << "\n"; } private: std::string name_; }; int main() { /* # OUTPUT # COPY: Omer Faruk Yesiltepe COPY: Seyfullah Karamehmetoglu COPY: Muzaffer Karabasan Capacity: 3 MOVE: Ulya Yuruk COPY: Omer Faruk Yesiltepe COPY: Seyfullah Karamehmetoglu COPY: Muzaffer Karabasan */ std::vector svec{ "Omer Faruk Yesiltepe", "Seyfullah Karamehmetoglu", "Muzaffer Karabasan" }; std::cout << "Capacity: " << svec.capacity() << "\n"; svec.push_back("Ulya Yuruk"); } * Örnek 2, İşte şimdi "Move Ctor." fonksiyonumuz "noexcept" olduğu için artık kopyalama yerine taşıma gerçekleşecektir. #include #include #include class Student { public: Student(const char* name) : name_{name} {} std::string getName() const { return name_; } Student(const Student& other) : name_{ other.name_ } { std::cout << "COPY: " << name_ << "\n"; } Student(Student&& other) noexcept : name_{ std::move(other.name_)} { std::cout << "MOVE: " << name_ << "\n"; } private: std::string name_; }; int main() { /* # OUTPUT # COPY: Omer Faruk Yesiltepe COPY: Seyfullah Karamehmetoglu COPY: Muzaffer Karabasan Capacity: 3 MOVE: Ulya Yuruk MOVE: Omer Faruk Yesiltepe MOVE: Seyfullah Karamehmetoglu MOVE: Muzaffer Karabasan */ std::vector svec{ "Omer Faruk Yesiltepe", "Seyfullah Karamehmetoglu", "Muzaffer Karabasan" }; std::cout << "Capacity: " << svec.capacity() << "\n"; svec.push_back("Ulya Yuruk"); } Bu iki örnekte de görüldüğü üzere "Move Ctor." fonksiyonunu "noexcept" olarak nitelemeliyiz, mümkün olduğu sürece. Anımsayacağınız üzere "Dtor." fonksiyonları "noexcept" garantisi vermektedir eğer derleyici yazarsa. Bizler yazarken bile "noexcept" garantisi verecekmiş gibi yazmalıyız. "Dtor." fonksiyonlarının akışı sırasında bir "exception" fırlatılması hiç istenmeyecek bir şeydir çünkü. Şimdi de "Move Ctor." fonksiyonlarında "noexcept" anahtar sözcüğünün kullanılmasının önemine ilişkin şu örneği inceleyelim: * Örnek 1, Aşağıdaki örnekteki süreyi de incelersek, "noexcept" anahtar kelimesinin ne kadar önemli olduğunu da göreceğiz: #include #include #include #include class Mystr { public: Mystr() : m_str(500, 'A') {} // 1000 adet "a" karakteri. Mystr(const Mystr& other) = default; Mystr(Mystr&& other) : m_str{std::move(other.m_str)} {} // "Move Ctor." şimdilik "noexcept" değil. private: std::string m_str; }; int main() { /* # OUTPUT # Capacity : 1000000 Duration : 1583.61ms Capacity : 1000001 */ using namespace std; using namespace std::chrono; std::vector vec(1'000'000); // 1000000 adet öğe. std::cout << "Capacity : " << vec.capacity() << "\n"; auto tp_start = steady_clock::now(); vec.reserve(vec.capacity() + 1); // 1000001 adet öğe. auto tp_end = steady_clock::now(); std::cout << "Duration : " << duration(tp_end - tp_start) << "\n"; // Since C++20 std::cout << "Capacity : " << vec.capacity() << "\n"; } * Örnek 2, Aşağıdaki örnekte ise "Move Ctor." fonksiyonunun "noexcept" olması durumundaki süreyi göstermektedir. #include #include #include #include class Mystr { public: Mystr() : m_str(500, 'A') {} // 1000 adet "a" karakteri. Mystr(const Mystr& other) = default; Mystr(Mystr&& other) noexcept: m_str{std::move(other.m_str)} {} // "Move Ctor." artık "noexcept". private: std::string m_str; }; int main() { /* # OUTPUT # Capacity : 1000000 Duration : 644.156ms Capacity : 1000001 */ using namespace std; using namespace std::chrono; std::vector vec(1'000'000); // 1000000 adet öğe. std::cout << "Capacity : " << vec.capacity() << "\n"; auto tp_start = steady_clock::now(); vec.reserve(vec.capacity() + 1); // 1000001 adet öğe. auto tp_end = steady_clock::now(); std::cout << "Duration : " << duration(tp_end - tp_start) << "\n"; // Since C++20 std::cout << "Capacity : " << vec.capacity() << "\n"; } * Örnek 3, #include #include #include #include class Mystr { public: // 1000 adet "a" karakteri. Mystr() : m_str(500, 'A') {} Mystr(const Mystr& other) = default; // "Move Ctor." eğer "std::string" sınıfının "Move Ctor." u "noexcept" ise "noexcept". Mystr(Mystr&& other) = default; private: std::string m_str; }; int main() { /* # OUTPUT # Capacity : 1000000 Duration : 622.303ms Capacity : 1000001 */ using namespace std; using namespace std::chrono; std::vector vec(1'000'000); // 1000000 adet öğe. std::cout << "Capacity : " << vec.capacity() << "\n"; auto tp_start = steady_clock::now(); vec.reserve(vec.capacity() + 1); // 1000001 adet öğe. auto tp_end = steady_clock::now(); std::cout << "Duration : " << duration(tp_end - tp_start) << "\n"; // Since C++20 std::cout << "Capacity : " << vec.capacity() << "\n"; } * Örnek 4, #include #include #include #include class Mystr { public: Mystr() : m_str(500, 'A') {} // 1000 adet "a" karakteri. Mystr(const Mystr& other) = default; Mystr(Mystr&& other)noexcept(false) = default; // "Move Ctor." artık "noexcept" değil. Since C++20 private: std::string m_str; }; int main() { /* # OUTPUT # Capacity : 1000000 Duration : 1558.27ms Capacity : 1000001 */ using namespace std; using namespace std::chrono; std::vector vec(1'000'000); // 1000000 adet öğe. std::cout << "Capacity : " << vec.capacity() << "\n"; auto tp_start = steady_clock::now(); vec.reserve(vec.capacity() + 1); // 1000001 adet öğe. auto tp_end = steady_clock::now(); std::cout << "Duration : " << duration(tp_end - tp_start) << "\n"; // Since C++20 std::cout << "Capacity : " << vec.capacity() << "\n"; } Buraya kadarkileri özetlersek; Eğer "Move Ctor." fonksiyonu "Implicitly Declared" durumda ise "noexcept" olup olmayacağına derleyicinin kendisi karar vermektedir. Burada bizler "static_assert" kullanarak bir takım önlemler alabiliriz. Eğer "Move Ctor." fonksiyonunu bizler yazacaksak, "noexcept" olup olmamasını bir koşula bağlamalıyız. Şimdi de taşıma semantiğindeki bir diğer kavram olan "Moved From State" durumunu, yani taşıma işlemi sonrasında geride kalan nesnenin durumunu inceleyelim. Anımsayacağınız üzere taşıma semantiği ömrü bitmek üzere olan nesnelerin kaynağını çalmak için geliştirilen bir semantiktir. Fakat öyle durumlar vardır ki kaynağı çalınmış olsa bile ömrü henüz bitmediği için başka kodlar tarafından tekrardan kullanılacaktır. Fakat tekrardan kullanılmadan evvel bir takım garantileri de veriyor olması gerekmektedir. Bu garantiler "Dtor." fonksiyonunun çağrılabilir olması ve çağrıldığı zaman da başka komplikasyonlara yol açmaması gerekmektedir ki bu duruma İngilizce "destructable" da denir, "unspecified, but valid state" durumda olmasıdır. Pekiyi buradaki "unspecified, but valid state" ile kastedilen şey nedir? Burada kastedilen şey şudur: nesnenin kendisi "valid" durumda fakat tuttuğu değer bilinmiyor. Bu da demektir ki bu nesneye yeni bir değer atayarak onu tekrardan kullanıma sokabiliriz. Standart kütüphanedekiler bu garantiyi vermektedir. Fakat üçüncü parti kütüphaneler için bir kesinlik yoktur. Şimdi de aşağıdaki örnekleri inceleyelim: * Örnek 1, Aşağıdaki örnekte "Card" sınıfının "Move Ctor." fonksiyonu derleyici tarafından yazıldığı için sınıfın veri elemanı "m_val", "Moved from State" durumu söz konusu olduğunda boş bir "string" tutacaktır. #include #include /* * "Card" sınıfının "Move Ctor." fonksiyonunu derleyici yazdı. * "Moved from State" durumuna geldiğinde "m_val" değişkeni boş bir yazı tutacaktır. * Bunun nedeni ise "std::string" sınıfının "Move" fonksiyonlarından dolayıdır. * Bu örnek nezdinde bu durum İSTENMEMEKTEDİR. Artık "get_value()" fonksiyonu çağrısı * sonrasında ekrana hiç bir şey yazdırılmayacak. Çözüm olarak ya taşıma semantiği komple * devre dışı bırakılmalı ya bizler bu fonksiyonları yazmalıyız ya da sınıfın üye fonksiyonları * "Moved from State" durumu göz önüne alarak yazılmalıdır. Örneğin, "m_val" değişkeni boş bir yazı * tutuyorken "get_value" fonksiyonunun geri dönüş değerinin başka bir değer olması gibi. Son çare * olarak da "Moved from State" için beklenen davranışı sergilemeyecek fonksiyonların çağrılmamasını * dökümante etmeliyiz. */ class Card { public: Card(const std::string& val) : m_val{val} { // } std::string get_value() const { return m_val; } private: std::string m_val; }; int main() { /* # OUTPUT # */ Card c1{ "king_of_hearts" }; auto c2 = std::move(c1); std::cout << c1.get_value() << '\n'; // Use of a moved from object. } * Örnek 2, Aşağıdaki örnekte derleyici tarafından yazılan "Move Assignment" fonksiyonu, sınıfın veri elemanları arasındaki ilişkiyi bozduğu görülmektedir. #include #include /* * "InString" sınıfının "Move" fonksiyonlarını derleyici yazmıştır. Aşağıdaki "Move Assignment". * çağrısı sonrasında "m_sval" değişkeni yine boş bir yazı tutarken "m_val" için değerinin sıfır * olacağı söylenemez. İşte bu da istenmeyen bir durumdur, yine bu örnek nezdinde. Burada gösterilmek * istenen şey, derleyicinin yazdığı ilgili fonksiyonun sınıfın veri elemanları arasındaki ilişkiyi * bozmasıdır. Bu problemi de gidermek için yukarıdaki örnekte belirtilen çözümlerden birini uygulayabiliriz. */ class InString { public: InString(int val = 0) : m_val{val}, m_sval{std::to_string(val)} {} void set_value(int i) { m_val = i; m_sval = std::to_string(i); } void print()const { std::cout << "[" << m_val << ", " << m_sval << "]"; } private: int m_val; std::string m_sval; }; int main() { /* # OUTPUT # (before move), isx and isy => [42, 42][0, 0] (after move), isx and isy => [42, ][42, 42] */ InString isx{ 42 }; InString isy; std::cout << "(before move), isx and isy => "; isx.print(); isy.print(); puts(""); isy = std::move(isx); std::cout << "(after move), isx and isy => "; isx.print(); isy.print(); puts(""); } * Örnek 3, Aşağıdaki örnekte ise derleyicinin yazdığı "Move" fonksiyonundan dolayı ilgili veri elemanının değerinin bizim istemediğimiz bir değer almasından dolayı meydana gelen sorun gösterilmiştir. #include #include #include /* * "SharedInt" sınıfının veri elemanı, "Moved from State" oluştuğunda boş bir gösterici * halini alacaktır. İşte bu istenmeyen bir durumdur. Çünkü derleyicinin yazdığı "Move" * fonksiyonları buna yol açmıştır. Pekiyi bu durumu nasıl giderebilirdik? Yöntemlerden * birisi "as_string" isimli üye fonksiyonun, "m_sp" değişkeninin değerine göre farklı * değer döndürmesi olabilir. */ class SharedInt { public: explicit SharedInt(int val) : m_sp{ std::make_shared(val) } {} std::string as_string() const { return std::to_string(*m_sp); } private: std::shared_ptr m_sp; }; int main() { /* # OUTPUT # 20 20 */ SharedInt x{ 20 }; SharedInt y{ x }; std::cout << x.as_string() << '\n'; std::cout << y.as_string() << '\n'; SharedInt a{ 42 }; SharedInt b{ std::move(a) }; // "a" değişkeni "Moved from State" durumunda. Dolayısıyla değeri boş bir göstericidir. std::cout << a.as_string() << '\n'; } > Hatırlatıcı Notlar: >> C++ dilinde "R-Value Reference" olanlar "const" olabilirler. Fakat bu tip referanslar %98 ihtimalle "non-const" olarak kullanılırlar. >> "std::move" ve "std::copy": * Örnek 1, //... template OutIter Copy(InIter beg, InIter end, OutIter destbeg) { while (beg != end) *destbeg++ = *beg++; return destbeg; } template OutIter Move(InIter beg, InIter end, OutIter destbeg) { while (beg != end) *destbeg++ = std::move(*beg++); return destbeg; } //... >> Taşıma yapmak her zaman için kopyalama yapmaktan daha faydalıdır, biçimindeki bir cümle yanlış olabilir. Çünkü bazı sınıflar arka planda dinamik bellek yönetimi yaparken bazıları yapmamaktadır. * Örnek 1, #include #include class Nec { public: std::vector vx; std::array ax; }; int main() { /* * Burada "Nec" sınıfının "vx" isimli veri elemanını kopyalamak * gerçekten de maliyet gerektiren bir aksiyondur. Fakat "ax" isimli * veri elemanı için bundan söz edemeyebiliriz. Sonuçta "ax" isimli veri * elemanı arka planda dinamik bellek yönetmemektedir. Dolayısıyla "vx" için * taşımak fayda sağlayabilirken "ax" için sağlamayabilir. İş bu sebepten dolayıdır ki * taşımak her zaman için faydalıdır diyemeyiz. * */ } * Örnek 2, Benzer bir örnek "std::string" sınıfı için de verilebilir. Her ne kadar C++ dili bunu zorunlu kılmasa da, ilgili sınıfın içerisinde belli bir büyüklüğe sahip "buffer" alan tutuyorlar. Eğer yazının büyüklüğü bu "buffer" alandan küçükse dinamik bellek yönetimine BAŞVURULMAMAKTADIR. Aksi halde başvurulmaktadır. Buna da "Small String Optimization" denmektedir. * Örnek 3, Fakat en güzeli "Copy Ellision". Yani kopyalamanın elimine edilmesi, bir diğer deyişle kopyalamanın hiç olmaması. >> "typename" anahtar sözcüğünün iki anlamı vardır. >>> Şablon tür parametrelerinde "class" anahtar sözcüğünün yerine kullanılabilir olmasıdır. * Örnek 1, template void func(T&& r); template void foo(T&& r); >>> Bir şablon kod içerisinde nitelenmiş isim olarak kullanılan bir "identifier" lar iki anlama gelmektedir. Bunlardan ilki sınıfın "static" veri elemanı, bir diğer deyişle "nested type", biçimindeyken diğeri ise bir tür belirtmek amacı ile. C++20'ye kadar varsayılan durumlarda ilk anlam kullanılırken, ikinci anlamı kastetmek için "typename" anahtar sözcüğünü kullanmak gerekiyordu. Fakat artık bazı yerlerde bu anahtar sözcüğü kullanmaya gerek kalmadı. Örneğin, "T::x" ifadesinde kastedilen şey nedir? Burada dilin kuralları gereği çıkartılacak ilk anlam, "x" in bir "nested type" veya "static" veri elemanı olmasıdır. Eğer "x" in bir tür bilgisi olduğunu kastediyorsak, C++20 öncesinde, "typename T::x" biçiminde kullanmalıyız. >> Sınıfın "Special Member Functions" : "non-static" üye fonksiyonlardır. Özel olmalarının sebebi kodlarının derleyi tarafından yazılabiliyor olmasıdır. Toplamda 6 adettir. Bunlar, "Default Ctor.", "Dtor.", "Copy Ctor.", "Move Ctor.", "Copy Assignment", "Move Assignment" fonksiyonlarıdır. Eğer derleyicinin yazmış olduğu versiyon bizim isteklerimizi karşılıyorsa, bu fonksiyonların yazımını derleyiciye bırakmalıyız. Buna da "Rule of Zero" denmektedir. İşte derleyicinin bir şekilde bu fonksiyonları yazmasına ise ilgili fonksiyonun "default" edilmesi denir. Örneğin, "Defaulting the Default Ctor.". Öte yandan bu altı fonksiyon şu üç halden birisindedir; "Not Declared", "User Declared", "Implicitly Declared". Şimdi de bunları inceleyelim; >>> "Not Declared" : İlgili FONKSİYONUN OLMADIĞI ANLAMINA GELMEKTEDİR. Bildirilen fakat "delete" edilen fonksiyonlar, bu KATEGORİDE DEĞİLDİR. * Örnek 1, Aşağıdaki sınıfın "Default Ctor." fonksiyonu "Not Declared" hükmündedir çünkü bizler parametreli bir "Ctor." fonksiyonu bildirdiğimiz için. Eğer bir sınıfın "Default Ctor." fonksiyonunun OLMAYIŞI, o sınıfın kullanım olanaklarını büyük ölçüde kısıtlayacaktır. Artık "Default Init." gerçekleşen yerlerde sentaks hatası oluşacaktır. struct Myclass{ Myclass(int); }; * Örnek 2, Aşağıdaki sınıfın "Move" fonksiyonları "Not Declared" hükmündedir. Çünkü "Dtor." veya "Copy" fonksiyonlarından birinin bildirilmesi durumunda "Move" fonksiyonları "Not Declared" olacaktır. struct Myclass{ Myclass(); Myclass(const Myclass&); }; >>> "User Declared" : Bildirimin programcı tarafından yapılmasıdır. Üç farklı biçimde bir programcı bildirebilir ki bunlar şu şekildedir: Fonksiyonun bildirir ve kendisi tanımlar, fonksiyonu bildirir fakat derleyicinin tanımlamasını ister, fonksiyonu bildirir fakat "delete" eder. * Örnek 1, struct Myclass{ Myclass() { } }; * Örnek 2, struct Myclass{ Myclass() = default; }; * Örnek 3, struct Myclass{ Myclass() = delete; /* Bu fonksiyon da "Function Overload Resolution" a dahil edilmektedir. */ }; * Örnek 4, Aşağıdaki örnekte sınıfın "Copy" fonksiyonları "delete" edildiği için "Move" fonksiyonları da "Not Declared" hükmündedir. "Copy" fonksiyonlarına çağrı da sentaks hatası olacaktır. Dolayısıyla bu sınıfımız artık "Non-Copyable" ve "Non-Moveable" durumdadır. struct Myclass{ Myclass(const Myclass&) = delete; Myclass& operator=(const Myclass&) = delete; }; * Örnek 5, Aşağıdaki örnekte sınıfın "Move" fonksiyonları bildirildiği için "Copy" fonksiyonları "delete" EDİLMİŞTİR. Artık sınıfımız "Moveable, but Non-Copyable" durumdadır. BURADA UNUTMAMAMIZ GEREKEN ŞEY "Move" FONKSİYONLARINI HİÇ BİR ZAMAN "delete" ETMEMELİYİZ. struct Myclass{ Myclass(Myclass&&); Myclass& operator=(Myclass&&); }; >>> "Implicitly Declared" : Derleyicinin durumdan vazife çıkartarak bu altı fonksiyonu kendisinin bildirmesi ve tanımlamasıdır. Burada da iki alt kategori vardır; "defaulted" ve "deleted" olması. >>>> "defaulted" : Derleyicinin, fonksiyonu olması gerektiği gibi bildirmesi ve tanımlamasıdır. >>>> "deleted" : Derleyicinin, fonksiyonu olması gerektiği gibi bildirmesi ve tanımlaması sırasında bir sentaks hatası getirecekse, ilgili fonksiyonu "delete" etmesidir. * Örnek 1, class Myclass{ public: private: const int mx; }; int main() { Myclass m; // "Default Ctor. is deleted." Çünkü "const" nesnelere ilk değer vermek bir zorunluluktur. } * Örnek 2, class Member{ public: Member(int); }; class Myclass{ private: Member mx; }; int main() { Myclass m; // "Default Ctor. is deleted." } Şimdi de bunları bir çatı altında özetleyelim; -> Eğer sınıfa bu altı fonksiyondan hiç birisini bildirmezsek, derleyici tarafından "default" edilecektir. -> "Default Ctor." haricinde herhangi bir "Ctor." bildirirsek, "Default Ctor." artık "Not Declared" durumdadır. Fakat geri kalan beş tanesi yine derleyici tarafından "default" edilecektir. -> Eğer sınıfa "Default Ctor." bildirirsek, geri kalan beş fonksiyon da derleyici tarafından "default" edilecektir. -> Eğer sınıfa "Dtor." bildirirsek, "Rule of Three" kapsamında olan "Default Ctor.", "Copy Ctor." ve "Copy Assignment" fonksiyonları derleyici tarafından "default" edilecektir. "Move" fonksiyonları ise "Not Declared" olacaktır. -> Eğer sınıfa "Copy Ctor." bildirirsek, "Dtor." ve "Copy Assignment" fonksiyonları derleyici tarafından "default" edilecektir. Bir "Ctor." bildirdiğimiz için "Default Ctor." artık "Not Declared" olacaktır. "Move" fonksiyonları ise "Not Declared" olacaktır. -> Eğer sınıfa "Copy Assignment" bildirirsek, "Default Ctor.", "Dtor." ve "Copy Ctor." fonksiyonları derleyici tarafından "default" edilecektir. "Move" fonksiyonları ise "Not Declared" olacaktır. -> "Move" fonksiyonlarından herhangi birini bile bildirsek, "Copy" fonksiyonlarının ikisi de derleyici tarafından "delete" edilmektedir. Buradan da görüleceği üzere; -> "Default Ctor." haricinde herhangi bir "Ctor." bildirilmesi durumunda, "Default Ctor." artık "Not Declared" olur. -> Bir sınıfın "Dtor." fonksiyonu her zaman için mevcuttur. Ama derleyici yazar ama biz yazarız. -> "Rule of Three" kapsamında olan "Dtor.", "Copy Ctor." ve "Copy Assignment" fonksiyonlarından herhangi birisini bildirirsek, "Move" fonksiyonlar "Not Declared" olurlar. -> "Move" fonksiyonlardan birisini bildirirsek, "Copy" fonksiyonların ikisi de "delete" edilirler. Şimdi de aşağıdaki örnekleri inceleyelim: * Örnek 1, Aşağıdaki örnekte sınıfın "Default Ctor." fonksiyonu derleyici tarafından "default" edildiği için veri elemanları da aynı işleme maruz kaldı. #include #include class Myclass { public: void print() const { std::cout << max << " " << name << "\n"; } private: /* * "In-class Init." || "Default Member Init." * Eğer sınıfın "Ctor." fonksiyonları içerisinde aşağıdaki veri * elemanlarına herhangi bir değer atamazsam, bu küme parantezi * içerisindeki değerler ile hayata gelecekler. Benzer şekilde * eğer derleyici "Default Ctor." fonksiyonunu kendisi yazarsa * bile yine veri elemanları aşağıdaki değerleri alacaktır. */ int max{}; std::string name{"no_name_yet"}; }; int main() { Myclass m; m.print(); // OUTPUT => 0 no_name_yet } * Örnek 2, #include #include class Myclass { public: void print() const { std::cout << max << " " << name << "\n"; } private: int max; std::string name; }; int main() { /* * Sınıfın derleyici tarafından yazılan "Default Ctor." fonksiyonu veri elemanlarını da * "default" hayata getirdiği için "primitive" türler çöp değer ile hayata geldiler. * Dolayısıyla bu programda "Tanımsız Davranış" vardır. "std::string" sınıfının * "Default Ctor." fonksiyonu çağrıldı. */ Myclass m; m.print(); // OUTPUT => -858993460 /* * Aşağıda "mm" nesnesi "Value Init." ile hayata gelmiştir. Dolayısıyla ilk önce "Zero Init." * uygulanmıştır. */ Myclass mm{}; mm.print(); // OUTPUT => 0 } >> "noexcept" kelimesininin kullanım yeri: * Örnek 1, #include #include class Nec { Nec() noexcept; }; class Erg { Erg() noexcept; }; class Myclass { public: Myclass() noexcept; private: Nec nx; Erg ex; }; int main() { /* * "Myclass" sınıfının "Default Ctor." fonksiyonu "exception" * GÖNDERMEME GARANTİSİ VARSA, sonuç "true" olacaktır. Aksi * halde "false" olacaktır. */ constexpr auto b{ std::is_nothrow_default_constructible_v }; // true } * Örnek 2, class Nec {}; class Erg {}; class Myclass { private: Nec nx; Erg ex; }; int main() { /* * "Myclass" sınıfının "Default Ctor." fonksiyonu "exception" * GÖNDERMEME GARANTİSİ VARSA, sonuç "true" olacaktır. Aksi * halde "false" olacaktır. */ constexpr auto b{ std::is_nothrow_default_constructible_v }; // true } * Örnek 3, #include #include /* * "T" türünden iki nesne toplama operatörünün operandı olduğunda yapılacak * TOPLAMA işleminin "exception" GÖNDERMEME GARANTİSİ VARSA "func" FONKSİYONU * DA AYNI GARANTİYİ VERİYOR. */ template void func(T x)noexcept(noexcept(x + x)); // void func(T x)noexcept(true); /* => */ // void func(T x)noexcept; // void func(T x)noexcept(false); /* => */ // void func(T x); int main() { /* * Operand olan "std::cout << 1" ifadesinin yürütülmesinde * "exception" GÖNDERMEME GARANTİSİ VARSA, sonuç "true" olacaktır. * Aksi halde "false" olacaktır. */ constexpr auto c = noexcept(std::cout << 1); // false constexpr auto d = noexcept(10 + 20); // true } * Örnek 4, #include #include template void func(T x)noexcept(noexcept(x + x)); // ^ ^ // | "noexcept" operatörü // "noexcept" specifier. struct Neco { Neco operator+(const Neco&) const noexcept; }; int main() { constexpr bool a = noexcept(func(12)); // True std::string str; constexpr bool s = noexcept(func(s)); // False Neco myNec; constexpr bool n = noexcept(func(myNec)); // True } >> "Unevaluated Context" : "sizeof" ve "noexcept" operatörleri örnek olarak verilebilir. >> Aşağıdaki örnekleri inceleyelim: * Örnek 1, int main() { /* * "x" için; * Bir ifade oluşturmadığı için "Value Category" den bahsedilemez. * Fakat türü "int" biçimindedir. */ int x = 31; int b; /* * "x" için; * Bir ifade oluşturduğu için "Value Category" den bahsedilebilir: "L-Value". * Fakat türü: "int". */ b = x; /* * "+x" için; * Bir ifade oluşturduğu için "Value Category" den bahsedilebilir: "PR-Value". * Fakat türü: "int". */ b = +x; /* * "x + 5" için; * Bir ifade oluşturduğu için "Value Category" den bahsedilebilir: "PR-Value". * Fakat türü: "int". */ b = x + 5; /* * "x + 10" için; * Bir ifade oluşturduğu için "Value Category" den bahsedilebilir: "PR-Value". * Fakat türü: "int". */ auto y = x + 10; /* * "x + 10.05" için; * Bir ifade oluşturduğu için "Value Category" den bahsedilebilir: "PR-Value". * Fakat türü: "double". */ auto z = x + 10.05; /* * "c" için; * Bir ifade oluşturmadığı için "Value Category" den bahsedilemez. * Fakat türü: "char". */ char c = 'A'; char ccc; /* * "c" için; * Bir ifade oluşturduğu için "Value Category" den bahsedilebilir: "L-Value". * Fakat türü: "char". */ ccc = c; /* * "+c" için; * Bir ifade oluşturduğu için "Value Category" den bahsedilebilir: "PR-Value". * Fakat türü: "int". */ auto d = +c; /* * "rr" için; * Bir ifade oluşturmadığı için "Value Category" den bahsedilemez. * Fakat türü: "int&&". */ int&& rr = 10; /* * "rr" için; * Bir ifade oluşturduğu için "Value Category" den bahsedilebilir: "L-Value". * Fakat türü: "int". Çünkü ifadelerin türü olarak REFERANS OLAMAZ. */ +rr; /* * "p" için; * Bir ifade oluşturmadığı için "Value Category" den bahsedilemez. * Fakat türü: "int*". */ int* p{}; /* * "p" için; * Bir ifade oluşturduğu için "Value Category" den bahsedilebilir: "L-Value". * Fakat türü: "int*". İfadelerin türü referans OLAMAZ fakat gösterici OLABİLİR. */ p; } >> Sınıfın beri elemanı bir "handle" değilse, derleyicinin yazdığı "Move Ctor." ve "Move Assign." fonksiyonları bizim işimizi görecektir. Unutmayınız ki "Rule of Zero". Buradaki "handle" ile kastedilen bir gösterici, referans vs. olması. Yani derleyicinin yazdığı şey işimizi bozmuyor ise ellemeyelim. >> "noexcept" : Modern C++ ile dile eklendi. Aşağıdaki fonksiyon bildirimine göre, ilgili fonksiyon "exception" gönderebilir. void foo(); Öte yandan aşağıdaki gibi bir fonksiyon bildiriminde ise ilgili fonksiyonun "exception" göndermemesi garanti altındadır. void foo()noexcept; Diğer yandan aşağıdaki gibi bir fonksiyon bildiriminde ise ilgili fonksiyon derleme zamanındaki duruma göre "exception" göndermeme garantisi verebilir veya vermeyebilir. void foo()noexcept(false); Artık bu fonksiyon bildirimi ile "void foo();" biçimindeki bildirim aynıdır. Eğer bildirim, void foo()noexcept(true); şeklinde olsaydı, "void foo()noexcept;" biçimindeki bildirim gibi olacaktı. Örneğin, void foo()noexcept(sizeof(int) == 4); biçimindeki bir bildirime göre ilgili sistemde "int" türü dört bayt ise "exception" göndermeme garantisi vermektedir. Aksi halde "exception" gönderebilir demektir. Fakat bu şekildeki kullanım ekseriyetle şablonlarda çok kullanılmaktadır. Örneğin, template void func()noexcept(std::is_nothrow_constructible_v); ile "T" türünün "Copy Ctor." fonksiyonu "exception" göndermeme garantisi veriyorsa, ben de veriyorum. Öte yandan "noexcept", fonksiyonun imzasının bir parçasıdır fakat "Function Overload Resolution" a dahil edilmemektedir. Dolayısıyla "noexcept" garantisi veren ve vermeyen iki fonksiyonu birbirinin "overload" ı haline getiremeyiz. Fakat fonksiyon göstericisi işler biraz karışmaktadır. Şöyleki; * Örnek 1, //... void foo(int)noexcept {} int main(void) { void(*fp)(int) = foo; constexpr auto a = noexcept(fp(12)); // "a" is "false". void(*fpe)(int)noexcept = foo; constexpr auto b = noexcept(fpe(12)); // "b" is "true". } * Örnek 2, void foo(int) {} int main(void) { void(*fpe)(int)noexcept = foo; // Sentaks hatası. } * Örnek 3, void foo(int)noexcept{} int main(void) { auto fp = foo; constexpr bool a = noexcept(fp(12)); // "a" is "true". } * Örnek 4, class Base{ public: virtual void func(int); }; class Der : public Base{ public: virtual void func(int)noexcept override; // It is OK. }; int main() {} * Örnek 5, class Base{ public: virtual void func(int)noexcept; }; class Der : public Base{ public: virtual void func(int) override; // SENTAKS HATASI. }; int main() {} * Örnek 6, Aşağıdaki durumda "virtual override" söz konusu OLMADIĞI için herhangi bir sentaks hatası da oluşmayacaktır. class Base{ public: void foo(int) noexcept; }; class Der : public Base{ public: void foo(int); // It is OK. }; >> Anımsayacağınız üzere üç farklı referans türü vardı. Sol taraf referans, sağ taraf referans ve "universal reference". >>> "Universal Reference" : İki farklı yerde kullanılır. Bunlardan ilki "auto" anahtar kelimesi ile birlikte kullanım ve şablon parametresi ile birlikte kullanım. "const" ya da "non-const" olan her türlü "Value Category" değerine sahip ifadeleri bu tip referanslara bağlayabiliriz. * Örnek 1, template void foo(T&&) {} void func(auto&& x) {} // Since C++20 int main() { auto&& r = 31; } Burada dikkat etmemiz gereken şey her "&&" atomu "Universal Reference" DEMEK DEĞİLDİR. Aynı atomu kullanarak da yine "R_Value Reference" için de kullanılmaktadır. Öte yandan parametresi "T&&" olan fonksiyona gönderebildiğimiz bütün ifadeleri, parametresi "const T&" olan fonksiyonlara da gönderebiliriz. Öyleyse bu iki fonksiyonun arasındaki fark nedir? Parametresi "T&&" olan fonksiyonun gövdesinde, bu fonksiyona geçilen argümanın değer kategorisi ve "constness" bilgigisi bilinir haldeyken parametresi "const T&" olan fonksiyonun gövdesinde bu bilgiler bilinir değildir, KAYBOLMAKTADIR. Şimdi de böylesi referanslara gönderilen ifadelere göre şablon parametresi ve fonksiyon parametresinin durumlarına bakalım: * Örnek 1, template void foo(T&& arg) {} //... int main() { /* * T : Myclass&, (void foo(Myclass& &&arg) {}) ("Referance Collapsing" öncesinde.) * * arg: Myclass&, (void foo(Myclass& arg) {}) ("Referance Collapsing" sonrasında.) */ Myclass m; foo(m); /* * T : Myclass, (void foo(Myclass &&arg) {}) ("Referance Collapsing" öncesinde.) * * arg: Myclass&&, (void foo(Myclass&& arg) {}) ("Referance Collapsing" sonrasında.) */ foo(Myclass{}); /* * T : const Myclass& , (void foo(const Myclass& &&arg) {}) ("Referance Collapsing" öncesinde.) * * arg: const Myclass&, (void foo(const Myclass& arg) {}) ("Referance Collapsing" sonrasında.) */ const Myclass cm; foo(cm); /* * T : const Myclass , (void foo(const Myclass &&arg) {}) ("Referance Collapsing" öncesinde.) * * arg: const Myclass&&, (void foo(const Myclass&& arg) {}) ("Referance Collapsing" sonrasında.) */ foo(std::move(cm)); } Buradaki "T" ve "arg" değişkenlerine bakarak "foo" fonksiyonlarına gönderilen ifadenin "constness" bilgisine ve "Value Category" bilgisine ulaşabiliriz. Pekiyi bu bilgi bizim nerede işimize yarayacaktır? Bu bilgileri koruyarak ilgili değişkenleri başka fonksiyonlara gönderebiliriz. İşte burada "std::forward" fonksiyonu devreye girmektedir. Bu senaryoya da "Perfect Forwarding" denir. * Örnek 1, //... template void foo(T&& arg) { /* * "arg" değişkeninin yukarıdaki özellikleri korunarak * "bar" fonksiyonuna argüman olarak geçilmiştir. İşte * bu işleme de "Perfect Forwarding" denir. Bizler direkt * olarak ilgili argümanlar ile "bar" fonksiyonunu da * çağırabiliriz. "foo" fonksiyonu üzerinden de. * İkisi arasında bir fark olmamalı. */ bar(std::forward(arg)); } "Perfect Forwarding" ile amaçlanan şey bir fonksiyonu direkt olarak argümanlar ile çağırmak yerine o argümanları önce aracı bir fonksiyona göndermek ve aracı olan fonksiyonun esas fonksiyonu çağırmasını sağlatmaktadır. Bir diğer deyişle esas fonksiyonu direkt olarak çağırmakla aracı kullanarak çağırmak arasında bir farkın olmamasıdır. Anımsayacağınız üzere "container" sınıfların "emplace" isimli fonksiyonları bu mekanizmadan faydalanmaktadır. Anımsayacağınız üzere "empalce" fonksiyonuna bizler argüman olarak, "container" içerisinde tutulan sınıfın "Ctor." fonksiyonlarına geçilecek argümanları geçmekteyiz. Böylelikle "emplace" fonksiyonu aldığı parametreler ile ilgili sınıf türünden nesneyi hayata getirmektedir. Benzer mekanizma "std::make_unique" fonksiyonunda da işletilmektedir. Bizler bu fonksiyona ilgili sınıfın "Ctor." fonksiyonuna geçilecek olan argümanları geçmekteyiz. * Örnek 1, Aşağıda "Perfect Forwarding" için kara düzen bir mekanizma geliştirilmiştir. Eğer "foo" fonksiyonu birden fazla parametreye sahip olsaydı, her version için ayrı bir "overload" yazmamız gerekecekti. Yine "call_foo" için de aynısı gerekecekti. #include class Myclass {}; void foo(Myclass&) { std::cout << "Myclass&\n"; } void foo(Myclass&&) { std::cout << "Myclass&&\n"; } void foo(const Myclass&) { std::cout << "const Myclass&\n"; } void foo(const Myclass&&) { std::cout << "const Myclass&&\n"; } void call_foo(Myclass& m) { foo(m); } void call_foo(Myclass&& m) { foo(std::move(m)); } void call_foo(const Myclass& m) { foo(m); } void call_foo(const Myclass&& m) { foo(std::move(m)); } int main() { /* # OUTPUT # */ Myclass m; const Myclass cm; foo(m); // Myclass& call_foo(m); // Myclass& puts(""); foo(cm); // const Myclass& call_foo(cm); // const Myclass& puts(""); foo(Myclass{}); // Myclass&& call_foo(Myclass{}); // Myclass&& puts(""); foo(std::move(m)); // Myclass&& call_foo(std::move(m)); // Myclass&& puts(""); foo(std::move(cm)); // const Myclass&& call_foo(std::move(cm));// const Myclass&& puts(""); } * Örnek 2.0, İşte yukarıdaki kara düzen yerine "std::forward" kullanarak daha rahat edebiliriz. "foo" fonksiyonu birden fazla argüman alacaksa da artık "variadic" şablonları kullanacağız. #include class Myclass {}; void foo(Myclass&) { std::cout << "Myclass&\n"; } void foo(Myclass&&) { std::cout << "Myclass&&\n"; } void foo(const Myclass&) { std::cout << "const Myclass&\n"; } void foo(const Myclass&&) { std::cout << "const Myclass&&\n"; } template void call_foo(T&& m) { foo(std::forward(m)); } int main() { /* # OUTPUT # */ Myclass m; const Myclass cm; foo(m); // Myclass& call_foo(m); // Myclass& puts(""); foo(cm); // const Myclass& call_foo(cm); // const Myclass& puts(""); foo(Myclass{}); // Myclass&& call_foo(Myclass{}); // Myclass&& puts(""); foo(std::move(m)); // Myclass&& call_foo(std::move(m)); // Myclass&& puts(""); foo(std::move(cm)); // const Myclass&& call_foo(std::move(cm));// const Myclass&& puts(""); } * Örnek 2.1, Eğer fonksiyon şablonundaki "T" gibi bir şey söz konusu değilse "Lambda" ifadelerinde de Benzer sonuçları elde edebiliriz. #include class Myclass {}; void foo(Myclass&) { std::cout << "Myclass&\n"; } void foo(Myclass&&) { std::cout << "Myclass&&\n"; } void foo(const Myclass&) { std::cout << "const Myclass&\n"; } void foo(const Myclass&&) { std::cout << "const Myclass&&\n"; } template void call_foo(T&& m) { foo(std::forward(m)); } int main() { /* # OUTPUT # */ Myclass m; const Myclass cm; auto fn = [](auto&& r) { /* * Yukarıdaki "call_foo" çağrısı içerisindeki "forward" çağrısını da aşağıdaki gibi de * yapabiliriz eğer şablon parametresi "T" gibi bir şey söz konusu değilse. Anımsanacağı * üzere buradaki "r" ile yine "Universal Reference" biçimindedir. */ foo(std::forward(r)); }; foo(m); // Myclass& fn(m); // Myclass& puts(""); foo(cm); // const Myclass& fn(cm); // const Myclass& puts(""); foo(Myclass{}); // Myclass&& fn(Myclass{}); // Myclass&& puts(""); foo(std::move(m)); // Myclass&& fn(std::move(m)); // Myclass&& puts(""); foo(std::move(cm)); // const Myclass&& fn(std::move(cm)); // const Myclass&& puts(""); } * Örnek 3, Eğer "foo" fonksiyonu iki argüman alıyorsa "call_foo" fonksiyonunu aşağıdaki gibi de yazabiliriz: //... template void call_foo(T&& t, U&& u) { foo( std::forward(t), std::forward(u) ); } //... * Örnek 4, Eğer "foo" fonksiyonu birden fazla argüman alıyorsa "call_foo" fonksiyonunu aşağıdaki gibi de yazabiliriz: //... template void call_foo(Args&&... args) { foo(std::forward(args)...); } //... * Örnek 5, Şimdi de "std::make_unique" fonksiyonunun nasıl yazıldığını inceleyelim: //... template std::unique_ptr MakeUnique(Args&&... args) { return std::unique_ptr(new T(std::forward(args)...)); } //... Şimdi de "std::forward" fonksiyonunun kaba implementasyonunu inceleyelim: * Örnek 1, Aşağıdaki örnekte "t" değişkeni : template constexpr T&& Forward(typename std::remove_reference::type& t) noexcept { // C++20 ile "typename" kelimesini yazmasak da olur. /* * Anımsayacağınız üzere "T" türü ya "Myclass" ya da "Myclass&" olabilir. Eğer, * "T" türü "Myclass&" ise "std::remove_reference" çağrısı sonucunda "Myclass" olacak. * Dolayısıyla "t" tür ise "Myclass&" olacak ve "L-Value Expression" biçiminde. Şimdi * fonksiyonun geri dönüş ifadesinde "T" yerine "Myclass&" gelirse, yine "Reference Collapsing" * gerçekleşecek ve sonuç "Myclass&" olacaktır. Fonksiyonun geri dönüş değeri "T&&" olması * hasebiyle yine "Referance Collapsing" gerçekleşecektir. Sonuç yine "Myclass&" olacaktır. * Dolayısıyla "T" türü "Myclass&" ise "Forward" fonksiyonuna yapılan çağrının geri dönüş * değeri "Myclass&" olacaktır. * Eğer "T" türü bir referans değilse, "t" yine "L_Value Reference" olacaktır. Fakat fonksiyonun * geri dönüş ifadesinde "T" yerine referans türü gelmediği için, "Reference Collapsing" sonucu * "Myclass&&" olacaktır. Fonksiyonun geri dönüş değeri türü "T&&" olduğu için, yine bir * "Reference Collapsing" gerçekleşecektir. Sonuç yine "Myclass&&" olacaktır. Dolayısıyla "T" türü * referans tür değilse, "Forward" fonksiyonuna yapılan çağrının geri dönüş değeri "Myclass&&" * olacaktır. */ return static_cast(t); } * Örnek 2, template // I T&& Forward(std::remove_reference_t& t) noexcept { return static_cast(t); } template // II T&& Forward(std::remove_reference_t&& t)noexcept { static_assert( !std::is_lvalue_reference_v, // "T" türünün "L_Value Referance" OLMAMASI GEREKİYOR. "Can not forward an R_Value as an L_Value" ); return static_cast(t); } int main() { /* * "T" türü yerine "Myclass&" gelmesi halinde "t" türü "I" nolu fonksiyonda "Myclass&", * "II" nolu fonksiyonda ise "Myclass&&" olacaktır. Dolayısıyla "L-Value" ifadeler için "I", * "R-Value" ifadeler için "II" nolu fonksiyon çağrılacaktır. "L-Value" ifadeler çağrılacak "I" * nolu fonksiyonun nasıl çalıştığını yukarıda anlattık. Pekiyi "R-Value" ifadeler için çağrılacak * "II" nolu fonksiyon nasıl çalışmaktadır? Şöyleki; "t" değişkeninin türü burada "Myclass&&" biçiminde. * Eğer fonksiyon şablonu olan "T" için "Myclass&" ve fonksiyona geçilen argüman da "R-Value" olarak * belirtilirse, yukarıdaki "static_assert" çağrısı devreye girecektir. Bu mekanizma "R-Value" nin * "L-Value" ye dönüştürülmesini engellemek içindir. Böyle bir dönüşüm yoktur. Fakat "L-Value" yi * "R-Value" ye dönüştürmek zaman zaman gerekebilir. */ Forward(Myclass{}); } Şimdi de "Universal Reference" parametreli fonksiyonların ve diğer referans parametreli fonksiyonların "Function Overload Resolution" sırasındaki ilişkisine değinelim: Call of f(Myclass&) f(const Myclass&) f(Myclass&&) f(const Myclass&&) template f(T&&) f(v) 1 3 X X 2 f(c) X 1 X X 2 f(Myclass{}) X 4 1 3 2 f(std::move(v)) X 4 1 3 2 f(std::move(c)) X 3 X 1 2 Buradan da görüleceği üzere eğer tam uyum yok ise "Universal Reference" olan yüksek öncelikli olacaktır. Dolayısıyla bu bilgiyi bir sınıfa "Ctor." yazarken de kullanabiliriz. "Universal Ctor." diyebileceğimiz bir "Ctor." yazmamız halinde, herhangi bir ifadeyi argüman olarak alarak o sınıf türünden nesne oluşturabiliriz. * Örnek 1, class Myclass { public: Myclass(const Myclass&); // Copy Ctor. Myclass(Myclass&&); // Move Ctor. template Myclass(T&&); // Universal Ctor. }; * Örnek 2, #include class Myclass { public: Myclass() = default; Myclass(const Myclass&) { std::cout << "Copy Ctor.\n"; } Myclass(Myclass&&) { std::cout << "Move Ctor.\n"; } template Myclass(T&&) {std::cout << "Universal Ctor.\n"; } }; int main() { Myclass v; const Myclass c; Myclass a{ c }; // Copy Ctor. Myclass b{ v }; // Universal Ctor. Myclass bb{ std::move(v) }; // Move Ctor. Myclass aa{ std::move(c) }; // // Universal Ctor. } * Örnek 3, Aşağıdaki örnek için C++20 kullanmalıyız. #include class Myclass { public: Myclass() = default; Myclass(const Myclass&) { std::cout << "Copy Ctor.\n"; } Myclass(Myclass&&) { std::cout << "Move Ctor.\n"; } template requires (!std::is_same_v, Myclass>) Myclass(T&&) {std::cout << "Universal Ctor.\n"; } }; int main() { /* # OUTPUT # */ Myclass v; const Myclass c; Myclass a{ c }; // Copy Ctor. Myclass b{ v }; // Copy Ctor. Myclass bb{ std::move(v) }; // Move Ctor. Myclass aa{ std::move(c) }; // Copy Ctor. Myclass e{ 31 }; // Universal Ctor. } Diğer yandan "Universal Reference" lar sadece "Perfect Forwarding" için de kullanılmayabilirler. Şöyleki; * Örnek 1, Aşağıdaki örnekte "Perfect Forwarding" yerine argümanın "constness" bilgisine göre uygun fonksiyon çağrısı yapılacaktır. #include #include void navigate(std::string::iterator beg, std::string::iterator end) { std::cout << "non-const semantics on the passed range!\n"; } void navigate(std::string::const_iterator beg, std::string::const_iterator end) { std::cout << "const semantics on the passed range!\n"; } template void process_container(T&& coll) { navigate(coll.begin(), coll.end()); } int main() { /* # OUTPUT # non-const semantics on the passed range! const semantics on the passed range! non-const semantics on the passed range! non-const semantics on the passed range! const semantics on the passed range! */ std::string name{ "mutable one" }; const std::string c_name{ "non-mutable one" }; process_container(name); process_container(c_name); process_container(std::string{ "Life is temporary!" }); process_container(std::move(name)); process_container(std::move(c_name)); } * Örnek 2, Yine aşağıdaki örnekte de argümanın "constness" bilgisine göre uygun fonksiyon çağrısı yapılacaktır. #include #include #include // Since C++20 void func(auto&& r) { if constexpr (std::is_const_v>) { std::cout << "const argument\n"; } else { std::cout << "non-const argument\n"; } } int main() { /* # OUTPUT # non-const argument const argument */ std::string name{ "Ahmet" }; func(name); const std::string c_name{ "Pehlivanli" }; func(c_name); } * Örnek 3, Aşağıdaki örnekte ise "Value Category" bilgisine göre uygun fonksiyon çağrılacaktır. #include #include #include template void func(T&&) { if constexpr (std::is_lvalue_reference_v) { /* * Fonksiyona geçilen argüman "L-Value Expression" * ise "T" nin türü "T&" olacaktır. Dolayısıyla bu * fonksiyon çağrılacaktır. */ std::cout << "L value argument is passed.\n"; } else if constexpr(!std::is_reference_v) { /* * Fonksiyona geçilen argüman "R-Value Expression" * ise "T" nin türü, türün kendisi olacaktır. * Dolayısıyla bu fonksiyon çağrılacaktır. */ std::cout << "non L value argument is passed.\n"; } else { std::cout << "FATAL ERROR\n"; } } class Myclass {}; int main() { /* # OUTPUT # non L value argument is passed. L value argument is passed. non L value argument is passed. */ func(Myclass{}); Myclass m; func(m); func(std::move(m)); } Öte yandan fakat "Universal Reference" kullanırken de "Conflicting Parameters" sorunu ile karşılaşabiliriz. Şöyleki; * Örnek 1.0, #include #include #include template void func(T, T); int main() { // OK. "T" türü "int" olarak açılacaktır. func(1, 2); // OK. "Array-decay" oluşacağı için "T" türü "const char*" olarak açılacaktır. func("ali", "ahmet"); // Sentaks Hatası. "T" için iki farklı tür çıkarımı olacağından, "Ambiguity" oluşacaktır. func(1, 2.); } * Örnek 1.1, #include #include #include template void func(T&, T&); int main() { // OK. "Array-decay" OLUŞMAYACAKTIR. "T" nin tür çıkarımı "const char[4]" türüne olacaktır. func("ali", "amo"); // Sentaks Hatası. "Array-decay" OLUŞMAYACAKTIR. Fakat "T" nin tür çıkarımı // "const char[4]" ve "const char[6]" türlerine olacağı için "Ambiguity" oluşacaktır. func("ali", "ahmet"); } * Örnek 1.2, #include #include template void insert(std::vector& vec, T&& elem) { /* * Burada amaçlanan şey ".push_back" fonksiyonunun "Copy" ve * "Move" tipindeki "overload" larından uygun olanlarına çağrı * yapılmasıdır. Yani ya "T&" parametreli versiyonu ya da "T&&" * parametreli versiyonu çağrılacaktır. */ vec.push_back(std::forward(elem)); } int main() { { std::vector coll; std::string s; /* * Fakat bu çağrı sentaks hatasına yol açacaktır. Çünkü "coll" * argümanına bakıldığında "T" türü için "std::string" çıkarımı * yapılacaktır. İkinci argümana bakıldığında ise "T" için tür * çıkarımı "std::string" türüne referans yönüne olacaktır. * "Ambiguity" meydana gelmiştir. */ insert(coll, s); } { std::vector coll; std::string s; /* * Artık bu çağrı sentaks hatasına yol açmayacaktır. Çünkü "coll" * argümanına bakıldığında "T" türü için "std::string" çıkarımı * yapılacaktır. İkinci argümana bakıldığında ise "T" için tür * çıkarımı yine "std::string" türü yönüne olacaktır. */ insert(coll, std::move(s)); } } * Örnek 1.3, #include #include template void insert(std::vector>& vec, T&& elem) { vec.push_back(std::forward(elem)); } int main() { std::vector coll; std::string s; /* * Burada "s" argümanından dolayı "T" için tür çıkarımı "std::string" * türüne referans tür olacaktır. "coll" argümanından dolayı da yine * "std::string" türüne çıkarım olacaktır fakat bu argüman geçildiğinde * referans olma özelliği silineceği için herhangi bir sorun oluşmayacaktır. */ insert(coll, s); } * Örnek 1.4, #include #include template void insert(std::vector& vec, T&& elem) { vec.push_back(std::forward(elem)); } int main() { std::vector coll; std::string s; insert(coll, s); } * Örnek 1.5, #include #include template void insert(Container& vec, T&& elem) { vec.push_back(std::forward(elem)); } int main() { /* # OUTPUT # non L value argument is passed. L value argument is passed. non L value argument is passed. */ std::vector coll; std::string s; insert(coll, s); } >> Şablon koddan gerçek kodun yazılmasına, o şablonun "Instantiate" edilmesi denir. Oluşturulan ürüne ise "Template Specialization" denmektedir. * Örnek 1, // Aşağıdaki bir sınıf şablonudur. template class Myclass{ public: void func(T); // "T" türünün ne olduğu belirlendiğinde, bu fonksiyonun parametrik yapısı da belirlenmiş olacaktır. // Bu fonksiyon bir "member template" DEĞİLDİR. }; // Aşağıdaki bir sınıf şablonu DEĞİLDİR. class Neco{ public: template void func(T); // Bu bir "member template" dir. }; // Aşağıdaki bir sınıf şablonudur. template class MyNeco{ public: template void func(U); // Bu bir "member template" dir }; int main(){ MyNeco{}.func(3.4); } >> Anımsayacağımız üzere "auto" anahtar sözcüğündeki tür çıkarım kuralları, kullanım yerine göre, değişecektir. Örneğin, * Örnek 1, template void foo(T x); class Myclass{}; int main() { Myclass m; foo(m); // Burada "T" için bir tür çıkarımı yapılacaktır. auto x = m; // Burada da "auto" yerine hangi türün geleceği belirlenecektir. /* * Burada "auto" anahtar sözcüğü yerine hangi türün geleceğini belirleyen kurallar * ile "T" yerine hangi türün geleceğini belirleyen kurallar aynıdır. */ } * Örnek 2, template void foo(T & x); class Myclass{}; int main() { Myclass m; foo(m); // Burada "T" için bir tür çıkarımı yapılacaktır. auto& x = m; // Burada da "auto" yerine hangi türün geleceği belirlenecektir. /* * Burada "auto" anahtar sözcüğü yerine hangi türün geleceğini belirleyen kurallar * ile "T" yerine hangi türün geleceğini belirleyen kurallar aynıdır. Fakat bu kurallar * yukarıdaki kurallardan farklıdır. */ } * Örnek 3.0, template void foo(T && x); class Myclass{}; int main() { Myclass m; foo(m); // Burada "T" için bir tür çıkarımı yapılacaktır. auto&& x = m; // Burada da "auto" yerine hangi türün geleceği belirlenecektir. /* * Burada "auto" anahtar sözcüğü yerine hangi türün geleceğini belirleyen kurallar * ile "T" yerine hangi türün geleceğini belirleyen kurallar aynıdır. Fakat bu kurallar * yukarıdaki iki kural setinden farklıdır. */ } * Örnek 3.1, template void foo(T && x); class Myclass{}; int main() { Myclass m; const Myclass c_m; auto&& r1 = Myclass{}; // Myclass && r1 = Myclass{}; auto&& r2 = m; // Myclass & r2 = m; } Pekiyi bizler "auto&&" ifadesini değişkenler için hangi amaçlarla kullanmalıyız? Çünkü bu zamana kadar genellikler "auto" ve "auto&" biçiminde kullanım gördük. Aşağıdaki örnekleri inceleyelim: * Örnek 1, #include #include class Myclass {}; void foo(const Myclass&) { std::cout << "foo(const Myclass&)\n"; } void foo(Myclass&) { std::cout << "foo(Myclass&)\n"; } void foo(Myclass&&) { std::cout << "foo(Myclass&&)\n"; } void foo(const Myclass&&) { std::cout << "foo(const Myclass&&)\n"; } int main() { { /* # OUTPUT # foo(Myclass&&) foo(Myclass&&) foo(Myclass&) foo(Myclass&) foo(Myclass&&) foo(Myclass&&) */ foo(Myclass{}); auto&& r1 = Myclass{}; foo(std::forward(r1)); Myclass m; foo(m); auto&& r2 = m; foo(std::forward(r2)); foo(std::move(m)); Myclass mm; auto&& r3 = std::move(mm); foo(std::forward(r3)); } { /* # OUTPUT # foo(const Myclass&) foo(const Myclass&) foo(const Myclass&&) foo(const Myclass&&) */ const Myclass m; foo(m); auto&& r2 = m; foo(std::forward(r2)); foo(std::move(m)); const Myclass mm; auto&& r3 = std::move(mm); foo(std::forward(r3)); } } * Örnek 2, #include #include class Myclass {}; void foo(Myclass&) { std::cout << "foo(Myclass&)\n"; } void foo(const Myclass&) { std::cout << "foo(const Myclass&)\n"; } void foo(Myclass&&) { std::cout << "foo(Myclass&&)\n"; } const Myclass& func_const_l_ref(const Myclass& str) { return str; } Myclass& func_non_const_l_ref(Myclass& str) { return str; } Myclass&& func_r_ref(Myclass&& str) { return std::move(str); } Myclass func_value(const Myclass& str) { return str; } int main() { Myclass m; const Myclass c_m; foo(func_r_ref(Myclass{})); // foo(Myclass&&) foo(func_non_const_l_ref(m)); // foo(Myclass&) foo(func_const_l_ref(c_m)); // foo(const Myclass&) foo(func_value(m)); // foo(Myclass&&) puts("\n==================="); auto&& r1 = func_r_ref(Myclass{}); foo(std::forward(r1)); // foo(Myclass&&) auto&& r2 = func_non_const_l_ref(m); foo(std::forward(r2)); // foo(Myclass&) auto&& r3 = func_const_l_ref(c_m); foo(std::forward(r3)); // foo(const Myclass&) auto&& r4 = func_value(m); foo(std::forward(r4)); // foo(Myclass&&) } * Örnek 3.0, "std::vector" sınıfının "bool" türden açılımı, diğer türlerin açılımından farklıdır. "bit" türüne erişim olmadığı için "vector" sınıfının "nested" sınıfı olan "reference" sınıfı ilüzyon oluşturmak için kullanılmıştır. Fakat "range-based for loop" kullanırken dikkatli olmalıyız çünkü ilgili dizinin elemanları değiştirilecektir. #include #include int main() { // "val" değişkeninin türü "int" olacaktır. { /* # OUTPUT # 00000 00000 */ std::vector ivec(5); for (auto val : ivec) std::cout << val; for (auto val : ivec) { val = 1; // val is "int". } std::cout << "\n"; for (auto val : ivec) std::cout << val; } std::cout << "\n"; // "val" değişkeninin türü "bool" değil, "std::vector::reference" olacaktır. // Bu türün ".operator=" fonksiyonu, değişikliğe yol açacaktır. { /* # OUTPUT # 00000 11111 */ std::vector ivec(5); for (auto val : ivec) std::cout << val; for (auto val : ivec) { val = true; // val is "std::vector::reference". It is a nested class of "std::vector". // val.operator=(true); } std::cout << "\n"; for (auto val : ivec) std::cout << val; } std::cout << "\n"; // "val" değişkeninin türü "bool" değil, "std::vector::reference" olacaktır. // Bu türün ".operator*" fonksiyonu, değişikliğe yol açacaktır. { /* # OUTPUT # 00000 10000 */ std::vector ivec(5); auto begin{ ivec.begin() }; // "begin" is "std::vectoriterator". for (auto val : ivec) std::cout << val; *begin = true; // begin.operator*() = true; // begin.operator*().operator=(true); // "begin.operator*()" is of type "std::vector::reference" std::cout << "\n"; for (auto val : ivec) std::cout << val; } std::cout << "\n"; // "range-based for loop" kullanırken de dikkatli olmalıyız. Çünkü artık "std::vector::reference" // sınıfının ".operator*" ve ".operaotor=" fonksiyonları arka planda çağrılmaktadır ki bunlar da veri // elemanını değiştirecektir. { /* # OUTPUT # 00000 11111 */ std::vector ivec(5); for (auto /* auto& */ /* auto&& */ val : ivec) { std::cout << val; } // "range-based for loop" karşılığında derleyicinin yazdığı kod : /* auto&& rng = ivec; auto pos = rng.begin(); auto end = rng.end(); for(;pos != end;++pos){ auto temp = *pos; // auto& temp = *pos; // auto&& temp = *pos; std::cout << val; } */ auto&& rng = ivec; auto pos = rng.begin(); auto end = rng.end(); for (; pos != end; ++pos) { auto temp = *pos; temp = true; } std::cout << "\n"; for(auto val : ivec) std::cout << val; } } * Örnek 3.1, Aşağıdaki örnekteki sentaks hatasının nedeni "R-Value" kategorisindeki ifade "L-Value Reference" ye bağlanmaya çalışılmıştır. #include #include #include template void Fill(C& con, const T& val) { for (auto& elem : con) elem = val; // Derleyicinin yazacağı kod: /* auto&& rng = con; auto pos = rng.begin(); auto end = eng.end(); for(;pos != end; ++pos) { auto& elem = *pos; // "*pos" ifadesinin geri dönüş değeri "R-Value Expression". // auto& elem = pos.operator*(); } */ } int main() { { /* # OUTPUT # 1 2 3 4 5 31 31 31 31 31 */ std::vector ivec{ 1, 2, 3, 4, 5 }; for (auto val : ivec) std::cout << val << " "; std::cout << "\n"; Fill(ivec, 31); for (auto val : ivec) std::cout << val << " "; std::cout << "\n"; } std::cout << "\n"; { /* # OUTPUT # ali naz eda ece 31 31 31 31 31 */ std::vector svec{ "ali", "naz", "eda", "ece" }; for (auto val : svec) std::cout << val << " "; std::cout << "\n"; Fill(svec, "31"); for (auto val : svec) std::cout << val << " "; std::cout << "\n"; } std::cout << "\n"; { std::vector bvec{ true, false, true }; for (auto val : bvec) std::cout << val << " "; std::cout << "\n"; /* * Aşağıdaki "Fill" çağrısı sentaks hatasına yol açacaktır. Anımsayacağınız * üzere, ilgili "range-based for loop" çağrısına karşılık derleyicinin * yazdığı kodda "R-Value Expression", "L-Value Reference" a bağlanmaktadır. * Bu da dilin kurallarına göre sentaks hatasıdır. */ Fill(bvec, false); for (auto val : bvec) std::cout << val << " "; std::cout << "\n"; } } * Örnek 3.2, Yukarıdaki problemi çözmek için "range-based for loop" içerisinde "Universal Reference" kullanmalıyız. #include #include #include template void Fill(C& con, const T& val) { for (auto&& elem : con) elem = val; } int main() { { /* # OUTPUT # 1 2 3 4 5 31 31 31 31 31 */ std::vector ivec{ 1, 2, 3, 4, 5 }; for (auto val : ivec) std::cout << val << " "; std::cout << "\n"; Fill(ivec, 31); for (auto val : ivec) std::cout << val << " "; std::cout << "\n"; } std::cout << "\n"; { /* # OUTPUT # ali naz eda ece 31 31 31 31 31 */ std::vector svec{ "ali", "naz", "eda", "ece" }; for (auto val : svec) std::cout << val << " "; std::cout << "\n"; Fill(svec, "31"); for (auto val : svec) std::cout << val << " "; std::cout << "\n"; } std::cout << "\n"; { /* # OUTPUT # 1 0 1 0 0 0 */ std::vector bvec{ true, false, true }; for (auto val : bvec) std::cout << val << " "; std::cout << "\n"; Fill(bvec, false); for (auto val : bvec) std::cout << val << " "; std::cout << "\n"; } } Şimdi de "Universal Reference" olarak görülen fakat öyle olmayan referanslara değinelim: * Örnek 1, #include #include template void func(const T&&) {} // "Universal Reference" DEĞİL. "const R-Value Reference". class Myclass{}; int main() { Myclass m; func(m); // Sentaks Hatası } * Örnek 2, #include #include #include template void func(Con& x, typename Con::value_type&&) {} // "Universal Reference" DEĞİL. "R-Value Reference". int main() { std::vector ivec; int x{}; func(ivec, x); // Sentaks Hatası } * Örnek 3, #include #include #include template void func(std::vector&&) {} // "Universal Reference" DEĞİL. "R-Value Reference". int main() { std::vector ivec(12); func(ivec); // Sentaks Hatası func(std::vector(21)); // LEGAL } * Örnek 4, #include #include #include template class Stack { public: void push(const T& val) { std::cout << "L-Value\n"; m_con.push_back(val); } void push(T&& val) { // NOT A "Universal Reference". "R-Value Reference". std::cout << "R-Value\n"; m_con.push_back(std::move(val)); } template void foo(U&&) { // A "Universal Reference". } template void func(Args&&... args) { // A "Universal Reference". } //... private: std::vector m_con; }; int main() { Stack MyStack; MyStack.push(std::string{ "PR-Value" }); // R-Value std::string str{ "L-Value" }; MyStack.push(str); // L-Value MyStack.push(std::move(str)); // R-Value MyStack.foo(1.2); MyStack.foo(12); long x = 123321123; MyStack.foo(x); MyStack.func(12, 1.2, "Ahmet", 'a'); } * Örnek 5, #include #include #include template void func(T&&){} class Myclass{}; int main() { Myclass m; /* * Sentaks Hatası. "T" için "Myclass" olsun dedik. Yine * "Reference Collapsing" gerçekleşti. Dolayısıyla fonksiyon * "void func(Myclass&&)" halini aldı. */ func(m); /* * "T" için "Myclass&" olsun dedik.Dolayısıyla Yine * "Reference Collapsing" gerçekleşti. Dolayısıyla fonksiyon * "void func(Myclass&)" halini aldı. */ func(m); /* * Sentaks Hatası. "T" için "Myclass&&" olsun dedik.Dolayısıyla Yine * "Reference Collapsing" gerçekleşti. Dolayısıyla fonksiyon * "void func(Myclass&&)" halini aldı. */ func(m); } >> Anımsayacağınız üzere tür çıkarımı söz konusu olduğunda üç farklı araç karşımıza çıkmaktadır. "auto", "decltype" ve "decltype(auto)". >>> "auto" anahtar sözcüğünün kullanımını daha önceki derslerde işlendi. >>> "decltype" ise argüman olarak bir ifade veya isim alması durumunda farklı işleyiş mekanizması vardır. >>> "decltype(auto)" ise tür çıkarımı "decltype" kullanım kuralları geçerlidir. * Örnek 1, #include #include #include int& foo_1(); int&& foo_2(); int main() { { int x = 4; decltype(auto) y = x; // int y = x; decltype(auto) z = 12; // int z = 12; } { int x = 4; decltype(auto) y = foo_1(); // "y" is "int&". decltype(auto) z = foo_2(); // "y" is "int&&". } { int x = 4; decltype(auto) y = x; // int y = x; decltype(auto) z = (x); // "z" is "int&" } { int x{}, * ptr{ &x }; // Sentaks hatası. "decltype(auto)" ifadesi ilave bir deklaratör almamaktadır. // decltype(auto)& y = x; } } * Örnek 2, #include #include #include struct Myclass { int i = 0; }; decltype(auto) foo_0(void) { return Myclass{}; } // The return expression is of category "PR-Value". // Myclass foo_0(void) { return Myclass{}; } decltype(auto) foo_00(void) { return (Myclass{}.i); } // The return expression is of category "X-Value". // int&& foo_0(void) { return (Myclass{}.i); } // "Dangling Reference" decltype(auto) foo_1(Myclass m) { return m; } // The return expression is of category "PR-Value". // Myclass foo_1(Myclass m) { return m; } decltype(auto) foo_2(Myclass m) { return (m); } // Myclass& foo_2(Myclass m) { return (m); } // "Dangling Reference" decltype(auto) foo_3(int i) { return (i + 1); } // The return expression is of category "PR-Value". // int foo_3(int i) { return (i + 1); }; decltype(auto) foo_4(int i) { return i++; } // The return expression is of category "PR-Value". // int foo_4(int i) { return i++; } decltype(auto) foo_5(int i) { return ++i; } // int& foo_5(int i) { return ++i; } // "Dangling Reference" decltype(auto) foo_6(int i) { return (i >= 0 ? i : 0); }// The return expression is of category "PR-Value". // int foo_6(int i) { return (i >= 0 ? i : 0); } decltype(auto) foo_7(int i, int j) { return i >= j ? i : j; } // int& foo_7(int i, int j) { return i >= j ? i : j; } // "Dangling Reference" int main() { //... } * Örnek 3, template decltype(auto) foo_0(T&& val) { return bar(std::forward(val)); } template decltype(auto) foo_1(Args&&... val) { return bar(std::forward(val)...); } int main() { /* * Burada "trailing return type" kullanılarak "lambda" ifadesinin * geri dönüş değerinin "decltype(auto)" özellikle belirtilmiştir. * Aksi halde "auto" olacaktı. */ auto fn = [](auto&& r)-> decltype(auto) { return bar(std::forward(r)); }; //... } * Örnek 4, #include #include #include template decltype(auto) Call(Func f, Args&&... args) { decltype(auto) return_value{ Func(std::forward(args)...) }; //... if constexpr (std::is_rvalue_reference_v) return std::move(ret); else return return_value; } auto My_Lambd = [](auto Func, auto&& args)->decltype(auto) { decltype(auto) return_value = Func(std::forward(args)...); //... if constexpr (std::is_rvalue_reference_v) return std::move(ret); else return return_value; }; int main() { //... } * Örnek 5, #include #include #include class Myclass { public: ~Myclass() { std::cout << "object destructed...\n"; } std::vector get_vec()const { return ivec; } private: std::vector ivec{ 1, 2, 3, 4 }; }; Myclass foo() { return Myclass{}; } int main() { { /* # OUTPUT # [0] main basladi... object destructed... [1] main devam ediyor... [2] main bitiyor... */ std::cout << "[0] main basladi...\n"; { const auto& r = foo(); // Life Extension // Myclass&& r = foo(); // auto&& r = foo(); } std::cout << "[1] main devam ediyor...\n"; std::cout << "[2] main bitiyor...\n"; } std::cout << "\n"; { /* # OUTPUT # [0] main basladi... object destructed... [1] main devam ediyor... [2] main bitiyor... */ std::cout << "[0] main basladi...\n"; const auto& r = foo().get_vec(); // Not a life extension std::cout << "[1] main devam ediyor...\n"; std::cout << "[2] main bitiyor...\n"; } std::cout << "\n"; /* # OUTPUT # [0] main basladi... [1] main devam ediyor... [2] main bitiyor... object destructed... */ std::cout << "[0] main basladi...\n"; const auto& r = foo(); std::cout << "[1] main devam ediyor...\n"; std::cout << "[2] main bitiyor...\n"; } * Örnek 6, #include #include #include std::vector create_svec(void); int main() { { const auto& ref = create_svec(); // Life Extension. std::vector&& ref_ref = create_svec(); // Life Extension. // auto& ref_ref_ref = create_svec(); // Sentaks Hatası. Sağ taraf değeri, sol taraf referansa bağlanamaz. const auto& Ref = create_svec().at(0); // NOT a Life Extension. } { for (std::string s : create_svec()) { // OK // ... } /* * Derleyici "range-based for loop" için * yazdığı kod aşağıdaki gibi olacaktır: auto&& rg = create_svec().at(0); // NOT a Life Extension. The object destroyed. auto pos = rg.begin(); // "Dangling Reference". auto end = rg.end(); // "Dangling Reference". for (; pos != end; ++pos) char c = *pos; */ for (char c : create_svec().at(0)) { // UB //... } for (char c : create_svec()[0]) { // UB //... } for (char c : create_svec().front()) { // UB //... } } } >> Anımsanacağı üzere sınıfların üye fonksiyonlarında "const" anahtar sözcüğünü kullanabiliyorduk. Böylelikle o üye fonksiyonun sınıfın anlamında bir değişikliğe yol açmasını engelliyorduk hem de "Function Overload Resolution" sırasında seçilebilmesini sağlıyorduk. Malümat olduğu üzere; "const" nesneler "non-const" üye fonksiyonları çağıramazken, "non-const" nesneler her iki türden fonksiyonu da çağırabiliyordu. Artık modern C++ ile fonksiyon bildirimlerinde "&" atomunu kullanabiliriz. Eğer bir adet "&" atomu kullanırsak ilgili fonksiyon "L-Value" nesneler tarafından, iki adet "&" atomu kullanırsak "R-Value" nesneler tarafından ilgili fonksiyon çağrılabilecektir. Fakat "&" olanlar, "&" olmayanlar ile birlikte kullanılamazlar. * Örnek 1, #include #include class Myclass { public: void func()& { std::cout << "func()&\n"; } void func()&& { std::cout << "func()&&\n"; } void func()const& { std::cout << "func()const&\n"; } void func()const&& { std::cout << "func()const&\n"; } }; int main() { Myclass m; m.func(); Myclass{}.func(); std::move(m).func(); const Myclass cm; cm.func(); std::move(cm).func(); } * Örnek 2, Aşağıdaki örnekte de görüldüğü üzere "std::string" sınıfının ".operator=" fonksiyonu "&" atomu ile nitelenmemiştir. İşte ".operator=" fonksiyonunun "&" atomu içermemesi durumunda, geçici nesneye de değer atayabiliyoruz. #include #include #include void foo(bool) { std::cout << "foo(bool)\n"; } void foo(std::string) { std::cout << "foo(std::string)\n"; } std::string get_str() { return "Ulya"; } int main() { if (get_str() == "Ulya") puts("True"); // OK : True //if (get_str() = "Ulya") puts("False"); // Sentaks Hatası foo(get_str() == "Ahmet"); // OK: foo(bool) foo(get_str() = "Yuruk"); // OK: foo(std::string); // foo(get_str().operator=("Yuruk")); // "std::string" sınıfının ".operator=" fonksiyonu "*this" döndürmektedir. // "get_str" fonksiyonunun döndürdüğü geçici nesneye "Yuruk" değerini // atamış olduk. } * Örnek 3, Sınıfın öze üye fonksiyonları "Implicitly Declared" ise veya bizim tarafımızdan deklare edilmiş ama "default" edilmişse, "&" olmadan yazılır. Dolayısıyla bu fonksiyonları ister kendimiz tanımlayalım ister derleyiciye tanımlatalım, her iki senaryoda da "&" atomunu belirtmeliyiz. #include #include #include class Myclass { public: /* * Derleyicinin tanımladığı ilgili fonksiyonlar artık "&" ile nitelenmiş * olacaktır. Dolayısıyla aşağıdaki bu iki fonksiyon sadece "L-Value" olan * ifadeler tarafından çağrılabilir durumdadır. */ Myclass& operator=(const Myclass&)& = default; Myclass& operator=(Myclass&&)& = default; }; Myclass foo() { return Myclass{}; } int main() { foo() = foo(); // Artık sentaks hatası. } * Örnek 4, #include #include std::optional foo(); int main() { if (foo() = "necati") { // LEGAL } } * Örnek 5, #include #include class Person { public: Person(const std::string& name) : m_name{name} {} /* * "range-based for loop" kullanırken hataya neden olacak noktalar * için bu "overload" önlem oluşturacaktır. */ std::string get_name()&& { std::cout << "get_name()&& => "; return std::move(m_name); } const std::string& get_name()const& { std::cout << "get_name()const& => "; return m_name; } const std::string& get_name()& { std::cout << "get_name()& => "; return m_name; } private: std::string m_name; }; template void foo(T&& x) { auto name = std::forward(x).get_name(); std::cout << name << '\n'; } int main() { Person p{ "Ulya" }; const Person cp{ "Yuruk" }; foo(p); // get_name() & => Ulya foo(cp); // get_name()const& => Yuruk foo(Person{"ali"}); // get_name() && => ali } >> "const" nesneler ile "non-const" üye fonksiyonları çağıramayız. Çünkü ilgili üye fonksiyonların gizli parametresi "const" türden DEĞİLDİR, yani "this" göstericisi için kullanılan parametre. Aksi halde "const" özelliği düşmüş OLACAKTIR. * Örnek 1, #include #include #include #include class Myclass { public: void my_const_func() const { std::cout << "CONST!\n"; } void my_func() { std::cout << "NON-CONST!\n"; } }; int main() { /* # OUTPUT # */ Myclass m1; const Myclass cm1; // Non-const var. can call non-const member func. m1.my_func(); // NON-CONST! // Non-const var. can call const member func. m1.my_const_func(); // CONST! // const var. CANNOT call non-const member func. // cm1.my_func(); // ERROR: passing ‘const Myclass’ as ‘this’ argument discards qualifiers [-fpermissive] cm1.my_const_func(); // CONST! } >> "Idiom" demek, bir dile bağlı; "pattern" demek, dilden bağımsız. Eğer bir "Idiom" birden fazla adımdan oluşan kod parçacıkları içerirse, ona da "Technic" denmektedir. >> "using declaration": İlgili isim alanı enjekte edilmektedir. * Örnek 1, #include #include int main() { using std::cout, std::endl; // Using Declarations std::string name; // cin >> name; // Error std::cin >> name; // Ulya cout << name << endl; // Ulya using namespace std; // using namespace directive string surname; cin >> surname; // Yuruk cout << surname << endl; // Yuruk } >>> "ADL" : * Örnek 1, #include #include namespace nec{ class Myclass{}; void swap(Myclass& a, Myclass& b) { std::cout << "nec::swap was called\n"; } void read(std::vector) { std::cout << "nec::read was called\n"; } void write(Myclass) { std::cout << "nec::write was called\n"; } } void write(nec::Myclass) { std::cout << "write was called\n"; } int main() { /* # OUTPUT # */ nec::Myclass mx, my; swap(mx, my); // ADL: nec::swap was called std::vector mvec; read(mvec); // ADL: nec::read was called // ERROR: ambiguous call for "write". "ADL" // mekanizmasından dolayı her iki fonksiyon // ismi de görülür durumdadır. write(mx); } * Örnek 2, #include #include namespace my_ns{ class Myclass{}; void swap(Myclass, Myclass) { std::cout << "my_ns::swap(Myclass, Myclass) was called.\n"; } } template void func(T) { T x, y; // [0] // "T", bir "namespace" içerisindeki türdense ve o türe // ilişkin bir "swap" fonksiyonu da varsa, "std" isim alanı // içerisindeki "swap" fonksiyonunun ÇAĞRILMA İHTİMALİ // KALMAYACAKTIR. swap(x, y); } int main() { /* # OUTPUT # */ my_ns::Myclass mx, my; swap(mx, my); // ADL, my_ns::swap(Myclass) was called. func(mx); // ADL, my_ns::swap(Myclass, Myclass) was called. } >> Tür dönüştürme operatör fonksiyonları: "const" veya "non-const" olabilirler, geri dönüş değerleri hedef türe gösterici veya referams olabilir veya başka bir sınıf türüne dönüşüm de mümkündür. Yine "overload" edilebilirler. Sadece bu fonksiyonun imzasında geri dönüş türüne ilişkin tür bilgisini yazmıyoruz. Bütün bunların yanı sıra bu fonksiyonu "member template" fonksiyon olarak da yazabiliriz. Bu sebepten dolayı "auto" anahtar sözcüğünü de kullanabiliriz. Bu durumda herhangi bir türe dönüşüm yapabiliriz. Diğer yandan, Modern C++ ile birlikte, "Conversion Ctor." da olduğu gibi, "explicit" anahtar sözcüğünü de kullanabiliriz. * Örnek 1, class A{}; class B{}; class C{}; class Myclass { public: operator A() const; // Cast from Myclass to A operator B() const; // Cast from Myclass to B operator C() const; // Cast from Myclass to C }; * Örnek 2, class Myclass { public: // Way - I template operator T() const; // Way - II // operator auto() const; }; * Örnek 3, class Myclass { public: // Artık örtülü tür dönüşümleri sentaks hatasıdır. Sadece "static_cast" gibi tür dönüştürme fonksiyonlarını // kullanabiliriz. explicit operator int() const; }; int main() { Myclass m; int ival = m; // Artık sentaks hatası. } >> "Virtual Dispatch" : Taban sınıfların "Dtor." fonksiyonları ya "virtual" ve "public" ya da "non-virtual" ve "protected" olmalıdır. Böylelikle "virtual dispatch" mekanizması sağlıklı bir şekilde işletilebilsin. Şu durumlarda da bu mekanizma devreye girmemektedir; >>> Taban sınıfın "Ctor." fonksiyonu içerisinde yapılan sanal fonksiyon çağrılarında, bu mekanizma devreye girmez. Eğer uygulansaydı türemiş sınıf nesnesi hayata gelmeden o nesneye ilişkin sanal fonksiyon çağrılacaktı ki bu da felakete yol açacaktır. Dolayısıyla taban sınıfın "Ctor." fonksiyonu içerisinde sanal fonksiyon çağırırken ya bu durumu bilmeli ya da o çağrıyı yapmamalıyız. >>> Taban sınıfın "Dtor." fonksiyonu içerisinde yapılan sanal fonksiyon çağrılarında, bu mekanizma devreye girmez. Eğer uygulansaydı hayatı bitmiş sınıf nesnesi için o nesneye ilişkin sanal fonksiyon çağrılacaktı ki bu felakete yol açacaktır. Dolayısıyla taban sınıfın "Dtor." fonksiyonu içerisinde sanal fonksiyon çağırırken ya bu durumu bilmeli ya da o çağrıyı yapmamalıyız. >>> "qualifid-name" kullanılan durumlarda bu mekanizma devreye girmez. >>> "object-slicing" olduğunda yine bu mekanizma işletilmez. Aşağıda bu konuya ilişkin örnekler verilmiştir; * Örnek 1, Taban sınıf türünden bir gösterici ile türemiş sınıfa ilişkin nesneyi kontrol etmek istersek; #include class Base { public: virtual ~Base() { std::cout << "Base Dtor.\n"; } virtual void foo() {} }; class Der : public Base { public: ~Der() { std::cout << "Der Dtor.\n"; } }; int main() { /* # OUTPUT # Der Dtor. Base Dtor. */ Base* ptr = new Der; //... delete ptr; } * Örnek 2, Taban sınıf türünden bir gösterici üzerinden türemiş sınıfa ilişkin nesnenin kontrolünü engellemek istersek, taban sınıfın "Dtor." fonksiyonunu "virtual" olmaktan çıkartıp sınıfın "protected" bölümüne taşıyoruz. Böylelikle erişim kontrolüne takıldığından, sentaks hatası alacağız. Artık türemiş sınıfı sadece o sınıf türünden bir göstericiyle. #include class Base { public: virtual void foo() {} protected: ~Base() { std::cout << "Base Dtor.\n"; } }; class Der : public Base { public: ~Der() { std::cout << "Der Dtor.\n"; } }; int main() { /* # OUTPUT # Der Dtor. Base Dtor. */ // Unappropriate Usage // Base* ptr = new Der; // ... // delete ptr; // error C2248: 'Base::~Base': cannot access protected member declared in class 'Base' // Proper Usage: Der* ptr = new Der; //... delete ptr; } >> Taban sınıfların "private" bölümündeki sanal fonksiyonları da türemiş sınıflar içerisinde "override" edebiliriz. >> "std::tuple" kullanırken, veri elemanlarına erişmek için "std::get" fonksiyonunu kullanmaktayız. Bu fonksiyonun da indeks bilgisi ve tür bilgisi alan iki farklı versiyonu vardır. Dikkat etmemiz gereken nokta ise tür bilgisi alan versiyonu kullanırken "std::tuple" içerisinde o türe ait birden fazla öğe varsa, sentaks hatası oluşacaktır. * Örnek 1, #include #include #include int main() { /* # OUTPUT # 28 30.03 Ulya Yuruk 28 30.03 Ulya Yuruk 28 30.03 Ulya Yuruk 28 30.03 Ulya Yuruk */ std::tuple tp{ 28, 30.03, "Ulya Yuruk" }; // Way - I std::cout << std::get<0>(tp) << '\n'; std::cout << std::get<1>(tp) << '\n'; std::cout << std::get<2>(tp) << '\n'; // Way - II std::cout << std::get(tp) << '\n'; std::cout << std::get(tp) << '\n'; std::cout << std::get(tp) << '\n'; // Way - III enum class Info { Age, Wage, Name }; std::cout << std::get(tp) << '\n'; std::cout << std::get(tp) << '\n'; std::cout << std::get(tp) << '\n'; // Way - IV using AGE = int; using WAGE = double; using NAME = std::string; std::cout << std::get(tp) << '\n'; std::cout << std::get(tp) << '\n'; std::cout << std::get(tp) << '\n'; } * Örnek 2.0, "tie" fonksiyonu ile bir "std::tuple" oluşturabiliriz. #include #include #include std::tuple foo() { return { 28, 2.8, "Yirmisekiz" }; } int main() { /* # OUTPUT # 28 2.8 Yirmisekiz */ int age; double wage; std::string text; tie(age, wage, text) = foo(); /* * "tie" fonksiyonu geri dönüş değeri olarak bir "std::tuple" * nesnesi döndürür ki bu nesne ise bünyesindeki veri elemanlarına * referans yoluyla bağlıdır. */ std::cout << age << ' ' << wage << ' ' << text << '\n'; } * Örnek 2.1, #include void print_values(int a, int b, int c) { std::cout << a << ' ' << b << ' ' << c << '\n'; } int main() { int a{ 10 }, b{ 20 }, c{ 30 }; print_values(a, b, c); // 10 20 30 // Way - I int temp = a; a = b; b = c; c = temp; print_values(a, b, c); // 20 30 10 // Way - II std::tie(a, b, c) = std::tuple{ b, c, a }; print_values(a, b, c); // 30 10 20 } * Örnek 3, "std::tuple" nesnesini karşılaştırma işlemlerinde de kullanabiliriz. Fakat C++20 ile dile eklenen "3-way Comparison" operatörü sayesinde, bu kullanıma gerek kalmamıştır. #include #include class MyDate { public: MyDate(int d, int m, int y) : md{d}, mm{m}, my{y} {} friend bool operator<(const MyDate& d1, const MyDate& d2) { return std::tuple{d1.my, d1.mm, d1.md} < std::tuple{ d2.my, d2.mm, d2.md }; /* * Buradaki karşılaştırmada ise "std::tuple" içerisindeki öğeler * karşılıklı bir şekilde, sırayla, karşılaştırılır. Esas karşılaştırma * işlemi, "std::tuple" sınıfının "operator<" fonksiyonudur. */ } //... private: int md; int mm; int my; }; int main() { MyDate d1{ 17, 9, 1993 }; MyDate d2{ 24, 7, 1995 }; std::cout << std::boolalpha << (d1 < d2) << '\n'; // True } * Örnek 4, #include int sum(int a, int b, int c) { return a + b + c; } int main() { std::tuple tp{ 20, 30, 40 }; // Way - I auto val_1 = sum( std::get<0>(tp), std::get<1>(tp), std::get<2>(tp) ); std::cout << "Val: " << val_1 << '\n'; // Val: 90 // Way - II auto val_2 = std::apply(sum, tp); // C++17 std::cout << "Val: " << val_2 << '\n'; // Val: 90 } >> "std::integral_constant" : Çok iyi bilinmesi gereken bir türdür. "type_traits" kütüphanesinde bulunur. * Örnek 1, #include template struct IntegralConstant { }; int main() { IntegralConstant x; // std::integral_constant x; // std::true_type x{true}; IntegralConstant y; // std::integral_constant y; // std::false_type x{false}; } * Örnek 2.0, #include int main() { constexpr std::integral_constant::value_type x{}; // "std::integral_constant::value_type" is "int". constexpr auto y = std::integral_constant::value; // "std::integral_constant::value" yields "30". So, // "y" is "int" and its value is "30". constexpr std::integral_constant::type z{}; // "z" is "std::integral_constant" } * Örnek 2.1, #include int main() { constexpr int x = std::integral_constant{}; // constexpr int x = std::integral_constant{}.operator int(); constexpr int y = std::integral_constant{}(); // constexpr int y = std::integral_constant{}.operator(); } * Örnek 3, #include template struct IsPointer : std::false_type {}; template struct IsPointer : std::true_type {}; template constexpr bool IsPointer_v = IsPointer::value; int main() { constexpr auto result = IsPointer_v; // true constexpr auto result2 = IsPointer_v; // false } >> "Object File" gözlemlemek için kullanacağımız komut satırı argümanları; nm -g -C --defined-only * .o >> Değişkenlerimize mutlak suretle ilk değer ataması yapıyoruz; bu amaca yönelim "Lambda Init. Capture" da kullanabiliriz, "Ternary Opt." de kullanabiliriz, herhangi bir ifade de kullanabiliriz. Yeterki ilk değer verebilelim. Böylelikle o nesnemizin değeri deyişmeyecekse, "const" olarak hayata gelmesini de mümkün kılmış oluruz.