/*================================================================================================================================*/ (01_17_06_2023) > Temel Kavramlar: >> "Expression" : Sabitlerin, 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." >>> İ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; rx = 66; // Burada 'rx' ifadesinin türü "int&" DEĞİLDİR. Burada ifadenin türü "int" biçimindedir. int* px = &x; *px = 99; // Fakat bu durum göstericiler için geçerli değildir. Buradaki "px" in türü "int*" biçimindedir. } >>> İ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? Referansların kullanımı sırasında, "Type Deduction" ve "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" ve "Forwarding Reference / Universal Reference". >>>>> "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() { } > 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; } //... /*================================================================================================================================*/ (02_18_06_2023) > Temel Kavramlar: >> "Expression" (devam): >>> İfadenin Değer Kategorisi("Value Category of Expression") (devam): >>>> "Type Deduction" sırasında (devam): * Örnek 1, "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() { { LREF& r = Myclass{}; /* "Reference Collapsing" gereği "r" değişkeni bir "L-Value Reference" biçimindedir. */ // T& & => T& Myclass m; LREF& rr = m; /* "Reference Collapsing" gereği "r" değişkeni bir "L-Value Reference" biçimindedir. */ // T& & => T& LREF&& rrr = Myclass{}; /* "Reference Collapsing" gereği "r" değişkeni bir "L-Value Reference" biçimindedir. */ // T& && => T& LREF&& rrrr = m; /* "Reference Collapsing" gereği "r" değişkeni bir "L-Value Reference" biçimindedir. */ // T& && => T& } { RREF& r = Myclass{}; /* "Reference Collapsing" gereği "r" değişkeni bir "L-Value Reference" biçimindedir. */ // T&& & => T& Myclass m; RREF& rr = m; /* "Reference Collapsing" gereği "r" değişkeni bir "L-Value Reference" biçimindedir. */ // T&& & => T& RREF&& rrr = Myclass{}; /* "Reference Collapsing" gereği "r" değişkeni bir "R-Value Reference" biçimindedir. */ // T&& && => T&& RREF&& rrrr = m; /* "Reference Collapsing" gereği "r" değişkeni bir "R-Value Reference" biçimindedir. */ // 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. > Hatırlatıcı Notlar: >> 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. "Default Ctor.", "Dtor.", "Copy Ctor.", "Move Ctor.", "Copy Assignment" ve "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" ve "Implicitly Declared". >>> "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. /*================================================================================================================================*/ (03_24_06_2023) > "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". >> "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". >>> "Return Value Optimization" : Bu ise kendi içerisinde ikiye ayrılır. Bunlar "Unnamed Return Value Optimization" ve "Named Return Value Optimization". >>>> "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: Nec(Myclass m) : mx{std::move(m)} {} /* Eski C++ da böyle bir kod asla yazılmazdı. C++17 itibariyle bize kazanç sağlayabilir. */ 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: Nec(Myclass m) : mx{std::move(m)} {} /* Eski C++ da böyle bir kod asla yazılmazdı. C++17 itibariyle bize kazanç sağlayabilir. */ 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() { //... } 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(std::is_nothrow_copy_constructible_v); // Static assert is failed. "Copy Ctor." should be an "noexcept". 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. > Hatırlatıcı Notlar: >> 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. }; /*================================================================================================================================*/ (04_25_06_2023) > Taşıma Semantiği (devam): 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: Mystr() : m_str(500, 'A') {} // 1000 adet "a" karakteri. Mystr(const Mystr& other) = default; Mystr(Mystr&& other) = default; // "Move Ctor." eğer "std::string" sınıfının "Move Ctor." u "noexcept" ise "noexcept". 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) }; std::cout << a.as_string() << '\n'; // "a" değişkeni "Moved from State" durumunda. Dolayısıyla değeri boş bir göstericidir. } Şimdi de taşıma semantiği ile jenerik programlama arasındaki ilişkiye değinelim. > Hatırlatıcı Notlar: >> 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. } >> Ş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); } /*================================================================================================================================*/ (05_08_07_2023) > Hatırlatıcı Notlar: >> Anımsayacağınız üzere üç farklı referans türü vardı. Sol taraf referans, sağ taraf referans ve "universal reference". (devam) >>> "Universal Reference" (devam): 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() { func(1, 2); // OK. "T" türü "int" olarak açılacaktır. func("ali", "ahmet"); // OK. "Array-decay" oluşacağı için "T" türü "const char*" olarak açılacaktır. func(1, 2.); // Sentaks Hatası. "T" için iki farklı tür çıkarımı olacağından, "Ambiguity" oluşacaktır. } * Örnek 1.1, #include #include #include template void func(T&, T&); int main() { func("ali", "amo"); // OK. "Array-decay" OLUŞMAYACAKTIR. "T" nin tür çıkarımı "const char[4]" türüne olacaktır. func("ali", "ahmet"); // 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. } * Ö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); } >> 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 }; // decltype(auto)& y = x; // Sentaks hatası. "decltype(auto)" ifadesi ilave bir deklaratör almamaktadır. } } * Ö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 } /*================================================================================================================================*/ (06_09_07_2023) > Fonksiyonların parametre değişkenleri nasıl olmalıdır? Aşağıdaki örneği inceleyelim: * Örnek 1, Normal şartlarda kopyalamadan kaçınmak adına fonksiyonumuzun parametresini "const-reference" biçiminde belirtirdik. Şimdi de argüman olarak aldığı yazıyı ters çeviren bir yazı yazmak isteyelim. Dolayısıyla fonksiyonumuzun parametresini de "const-reference" olarak belirtelim. Parametresi bu şekil olduğu için bizler argüman olarak aldığımız yazıyı fonksiyonun içerisinde tekrardan başka bir yere kopyalamalıyız. Fakat bu örnek nezdinde ilgili fonksiyonumuzun parametresini direkt sınıf türünden yaparsak, fonksiyon gövdesinde tekrardan kopyalamaya gerek kalmayacaktır. Eğer fonksiyona da "PR-Value" argüman da geçersek, "Mandotary Copy Ellision" gerçekleşecektir. #include #include #include class Myclass{ public: Myclass(const std::string& name) : m_name{name} { std::cout << "Default Ctor.\n"; } Myclass(const Myclass& other) : m_name{other.m_name} { std::cout << "Copy Ctor.\n"; } Myclass& operator=(const Myclass& other) { m_name = other.m_name; std::cout << "Copy Assignment.\n"; return *this;} Myclass(Myclass&& other) : m_name{std::move(other.m_name)} { std::cout << "Move Ctor.\n"; } Myclass& operator=(Myclass&& other) { m_name = std::move(other.m_name); std::cout << "Move Assignment.\n"; return *this;} ~Myclass()noexcept { std::cout << "Dtor\n"; } void show_name()const { std::cout << "Name : " << m_name << "\n"; } private: std::string m_name; }; void func(const std::string& str){ auto s_temp = str; reverse(s_temp.begin(), s_temp.end()); std::cout << "[" << str << "] => [" << s_temp << "]\n"; } void func_(std::string str){ reverse(str.begin(), str.end()); std::cout << "[" << str << "]\n"; } void func(const Myclass& other){ auto s_other = other; s_other.show_name(); } void func_(Myclass other){ other.show_name(); } int main() { { /* # OUTPUT # [Ulya] => [aylU] [Ulya] => [aylU] */ std::string name{"Ulya"}; func(name); // [Ulya] => [aylU] std::cout << "[" << name << "] => "; func_(name); // [Ulya] => [aylU] } std::cout << "\n==================================\n"; { /* # OUTPUT # Default Ctor. Name : Ahmet Copy Ctor. Name : Ahmet Dtor Dtor */ Myclass m{"Ahmet"}; m.show_name(); func(m); } std::cout << "\n==================================\n"; { /* # OUTPUT # Default Ctor. Name : Kandemir Dtor */ func_(Myclass{"Kandemir"}); } std::cout << "\n==================================\n"; { /* # OUTPUT # Default Ctor. Name : Ahmet Copy Ctor. Name : Ahmet Dtor Dtor */ Myclass m{"Ahmet"}; m.show_name(); func_(m); } //... } > "Move Only" sınıflar: "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. (10:48 - ) >> "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'; } > "std::copy", "std::move", "std::copy_backward" ve "std::move_backward" isimli algoritma fonksiyonları: * Örnek 1, Aşağıda "copy_backward" algoritma fonksiyonunun temsili gösterimi de kullanılmıştır. #include #include #include #include template BidIt2 Copy_Backward(BidIt1 first, BidIt1 last, BidIt2 dest_end){ while(first != last){ *--dest_end = *--last; } return dest_end; } int main() { { /* # OUTPUT # 0 1 2 3 4 5 6 7 8 9 0 1 0 1 2 3 4 7 8 9 */ std::vector ivec(10); iota(ivec.begin(), ivec.end(), 0); for(auto i : ivec) std::cout << i << " "; std::cout << "\n"; copy_backward(ivec.begin(), ivec.begin() + 5, ivec.begin() + 7); for(auto i : ivec) std::cout << i << " "; std::cout << "\n"; } std::cout << "==================\n"; { /* # OUTPUT # 0 1 2 3 4 5 6 7 8 9 0 1 0 1 2 3 4 7 8 9 */ std::vector ivec(10); iota(ivec.begin(), ivec.end(), 0); for(auto i : ivec) std::cout << i << " "; std::cout << "\n"; Copy_Backward(ivec.begin(), ivec.begin() + 5, ivec.begin() + 7); for(auto i : ivec) std::cout << i << " "; std::cout << "\n"; } } * Örnek 2, Aşağıda "copy" algoritma fonksiyonunun temsili gösterimi de kullanılmıştır. #include #include #include #include template OutIter Copy(InIter first, InIter last, OutIter dest_first){ while(first != last){ *dest_first++ = *first++; } return dest_first; } int main() { { /* # OUTPUT # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 0 1 2 8 9 */ std::vector ivec(10); iota(ivec.begin(), ivec.end(), 0); for(auto i : ivec) std::cout << i << " "; std::cout << "\n"; copy(ivec.begin(), ivec.begin() + 3, ivec.begin() + 5); for(auto i : ivec) std::cout << i << " "; std::cout << "\n"; } std::cout << "==================\n"; { /* # OUTPUT # 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 0 1 2 8 9 */ std::vector ivec(10); iota(ivec.begin(), ivec.end(), 0); for(auto i : ivec) std::cout << i << " "; std::cout << "\n"; Copy(ivec.begin(), ivec.begin() + 3, ivec.begin() + 5); for(auto i : ivec) std::cout << i << " "; std::cout << "\n"; } } * Örnek 3, Aşağıda ise hem "std::move" hem de "std::move_backward" algoritma fonksiyonlarının temsili gösterimi kullanılmıştır. #include #include #include #include template OutIter Move(InIter first, InIter last, OutIter dest_first){ while(first != last){ *dest_first++ = std::move(*first++); } return dest_first; } template BidIt2 Copy_Backward(BidIt1 first, BidIt1 last, BidIt2 dest_end){ while(first != last){ *--dest_end = std::move(*--last); } return dest_end; } int main() { /* # OUTPUT # ================== Size: 4 => ahmet ulya nurettin yuruk ================== Size: 4 => */ std::vector svec{"ahmet", "ulya", "nurettin", "yuruk"}; std::vector destvec(svec.size()); std::cout << "==================\n"; copy(svec.begin(), svec.end(), destvec.begin()); std::cout << "Size: " << svec.size() << " => "; for(auto&& name : svec) std::cout << name << " "; std::cout << "\n"; std::cout << "==================\n"; move(svec.begin(), svec.end(), destvec.begin()); std::cout << "Size: " << svec.size() << " => "; for(auto&& name : svec) std::cout << name << " "; std::cout << "\n"; } Yine bu "std::move" ve "std::move_backward" işlemlerinden sonra geride kalanın "Moved From State" halinde olduğuna da dikkat ediniz. > "Adaptors in STL" : Anımsayacağımız üzere üç farklı "Uyumlandırıcı (Adaptor)" bulunmaktadır. Bunlar Fonksiyon, Sınıf ve iteratör uyumlandırıcıları. >> Fonksiyon Uyumlandırıcıları ("Function Adaptors") : Argüman olarak bir "Callable" alan ve bir "Callable" geri döndürenlerdir. Örneğin, "std::bind". >> Sınıf Uyumlandırıcıları ("Container Adaptors") : "std::stack", "std::queue" ve "std::priority_queue". Bunlar ise bir "Container" ı veri elemanı olarak alırlar ve onun "interface" bilgisini temsil ettikleri "abstrack data type" a göre uyumlandırırlar. Örneğin, "std::stack" arka planda varsayılan durumda "std::dequeu" isimli sınıfı kullanır. "std::stack" sınıfına ilişkin bir fonksiyonu çağırdığımızda ise "std::dequeu" sınıfının ilgili fonksiyonları çağrılır. İki farklı biçimde oluşturabiliriz. Bunlar "Composition" ve "Inheritence". >>> "Composition": * Örnek 1, Artık "Iter" için hangi "Container" açılmışsa, "foo" içerisinde onun üye fonksiyonlarını çağırabiliriz. template class NecIter{ public: void foo(){ /* "Iter" sınıfının herhangi bir fonksiyonu */ } private: Iter m_iter; }; >>> "Inheritence" : * Örnek 1, template class NecIter : public Iter{ /* * "Iter" sınıfına ait "public" ve "protected" * fonksiyonlarını burada başka fonksiyonlar ile * sarmalayabiliriz, "override" edebiliriz. */ }; >> İterator Uyumlandırıcıları ("Iterator Adaptors") : "reverse_iterator", "back_inserter", "front_inserter" şeklinde örnekler verebiliriz. Yine bu tip uyumlandırıcılar, arka planda kullandıkları iteratör sınıfını özel biçimde uyarlamaktadır. * Örnek 1, Aşağıda "std::move_iterator" iterator sınıfı için bir örnek verilmiştir. Sonuçlardan da görüldüğü üzere bu uyumlandırıcı "Move Semantics" mekanizmasını işletmektedir. #include #include #include #include #include #include template typename std::move_iterator MakeMoveIterator(Iter it){ return std::move_iterator{it}; } int main() { /* # OUTPUT # */ std::vector svec{"nurullah", "abdulmuttalip", "giyasetting", "UlyaYuruk"}; // Way - 1 std::move_iterator::iterator> iterDefault{svec.begin()}; auto name = *iterDefault; std::cout << "Name : " << name << ", " << svec[0] << "\n"; // Name : nurullah, // Way - 2 std::move_iterator iterCtat{ svec.begin() + 1}; // CTAT, Since C++17 name = *iterCtat; std::cout << "Name : " << name << ", " << svec[0] << "\n"; // Name : abdulmuttalip, // Way - 3 auto iterMaker{ MakeMoveIterator(svec.begin() + 2) }; name = *iterMaker; std::cout << "Name : " << name << ", " << svec[0] << "\n"; // Name : giyasetting, // Way - 4 auto itermaker = std::make_move_iterator(svec.begin() + 3); name = *itermaker; std::cout << "Name : " << name << ", " << svec[0] << "\n"; // Name : UlyaYuruk, } * Örnek 2, Yine aşağıdaki örnekte de görüleceği üzere, "std::move_iterator" kullanılması durumunda "Move Semantics" işletileceği için geride kalan nesne "Moved From State" durumunda olacaktır. #include #include #include #include #include #include int main() { /* # OUTPUT # Size : 4, nurullah abdulmuttalip giyasetting UlyaYuruk Size : 4, nurullah abdulmuttalip giyasetting UlyaYuruk Size : 4, */ std::vector svec{"nurullah", "abdulmuttalip", "giyasetting", "UlyaYuruk"}; std::cout << "Size : " << svec.size() << ", "; for(auto&& name : svec) std::cout << name << " "; std::cout << "\n"; std::vector ListNames{svec.begin(), svec.end()}; std::cout << "Size : " << svec.size() << ", "; for(auto&& name : svec) std::cout << name << " "; std::cout << "\n"; std::vector NameList{std::move_iterator{svec.begin()}, std::move_iterator{svec.end()}}; std::cout << "Size : " << svec.size() << ", "; for(auto&& name : svec) std::cout << name << " "; std::cout << "\n"; } * Örnek 3, Aşağıdaki "for_each" çağrısı ile birinci ve ikinci parametreden oluşan "range" içerisindeki elemanlar, üçüncü parametredeki "callable" nesneye gönderiliyor. #include #include #include #include #include #include int main() { /* # OUTPUT # =====Original Svec===== Size : 8, nurullah abdulmuttalip giyasetting UlyaYuruk Ahmet Merve John Nishap =====Copyied Ones===== nurullah abdulmuttalip giyasetting UlyaYuruk Ahmet Merve John Nishap =====Original Svec===== Size : 8, nurullah abdulmuttalip giyasetting UlyaYuruk Ahmet Merve John Nishap =====Moved Ones===== nurullah abdulmuttalip giyasetting UlyaYuruk Ahmet Merve John Nishap =====Original Svec===== Size : 8, */ std::vector svec{ "nurullah", "abdulmuttalip", "giyasetting", "UlyaYuruk", "Ahmet", "Merve", "John", "Nishap" }; std::cout << "=====Original Svec=====\n"; std::cout << "Size : " << svec.size() << ", "; for(auto&& name : svec) std::cout << name << " "; std::cout << "\n"; std::cout << "=====Copyied Ones=====\n"; for_each( std::make_move_iterator(svec.begin()), std::make_move_iterator(svec.end()), [](const std::string& name){ /* * Programın akışı buraya geldiği an, * ilgili isim artık "R-Value" haline * gelmiş durumdadır. Çünkü "std::move_iterator" * nesnelerini "dereference" ettiğimiz * zaman oluşan ifade "R-Value Reference". Dolayısıyla * "name" nesnesinin türü "R-Value" * değer kategorilerini tutabilen bir * tür olmalıdır. Ya "const T&", ya "T" * ya da "T&&". Bizler burada "const T&" * dediğimiz için "std::string" sınıfının * "Copy Ctor." fonksiyonu çağrılmıştır. */ std::cout << name << " "; } ); std::cout << "\n"; std::cout << "=====Original Svec=====\n"; std::cout << "Size : " << svec.size() << ", "; for(auto&& name : svec) std::cout << name << " "; std::cout << "\n"; std::cout << "=====Moved Ones=====\n"; for_each( std::make_move_iterator(svec.begin()), std::make_move_iterator(svec.end()), [](std::string name){ /* * Programın akışı buraya geldiği an, * ilgili isim artık "R-Value" haline * gelmiş durumdadır. Çünkü "std::move_iterator" * nesnelerini "dereference" ettiğimiz * zaman oluşan ifade "R-Value Reference". Dolayısıyla * "name" nesnesinin türü "R-Value" * değer kategorilerini tutabilen bir * tür olmalıdır. Ya "const T&", ya "T" * ya da "T&&". Bizler burada "T" * dediğimiz için "std::string" sınıfının * "Move Ctor." fonksiyonu çağrılmıştır. */ std::cout << name << " "; } ); std::cout << "\n"; std::cout << "=====Original Svec=====\n"; std::cout << "Size : " << svec.size() << ", "; for(auto&& name : svec) std::cout << name << " "; std::cout << "\n"; } * Örnek 4, #include #include #include #include #include #include void do_something(std::string name) { std::cout << "Passed Name: " << name << "\n"; } int main() { /* # OUTPUT # Size : 8, nurullah abdulmuttalip giyasetting UlyaYuruk Ahmet Merve John Nishap Passed Name: abdulmuttalip Passed Name: giyasetting Passed Name: Nishap =====Original Svec===== Size : 8, nurullah UlyaYuruk Ahmet Merve John */ std::vector svec{ "nurullah", "abdulmuttalip", "giyasetting", "UlyaYuruk", "Ahmet", "Merve", "John", "Nishap" }; std::cout << "Size : " << svec.size() << ", "; for(auto&& name : svec) std::cout << name << " "; std::cout << "\n"; for_each( std::make_move_iterator(svec.begin()), std::make_move_iterator(svec.end()), [](auto&& name){ /* * Burada "auto&&" yerine "std::string&&" de kullanabiliriz. * Fakat her halükarda "do_something" çağrısına "std::move(name)" * ifadesini geçmeliyiz. Çünkü "name" bir isim olduğu için "L-Value" * ifadedir. */ if(name.find('i') != std::string::npos) do_something(std::move(name)); } ); std::cout << "\n"; std::cout << "=====Original Svec=====\n"; std::cout << "Size : " << svec.size() << ", "; for(auto&& name : svec) std::cout << name << " "; std::cout << "\n"; } * Örnek 5, #include #include #include #include #include #include class Myclass{ public: Myclass(const std::string& name) : m_name{name} { std::cout << "Default Ctor.\n"; } Myclass(const Myclass& other) : m_name{other.m_name} { std::cout << "Copy Ctor.\n"; } Myclass& operator=(const Myclass& other) { m_name = other.m_name; std::cout << "Copy Assignment.\n"; return *this;} Myclass(Myclass&& other) : m_name{std::move(other.m_name)} { std::cout << "Move Ctor.\n"; } Myclass& operator=(Myclass&& other) { m_name = std::move(other.m_name); std::cout << "Move Assignment.\n"; return *this;} ~Myclass()noexcept { std::cout << "Dtor\n"; } void show_name()const { std::cout << "Name : " << m_name << "\n"; } std::string get_name() const { return m_name; } private: std::string m_name; }; void do_something(Myclass name) { std::cout << "Passed Name: "; name.show_name(); } int main() { /* # OUTPUT # Default Ctor. Default Ctor. Default Ctor. Default Ctor. Default Ctor. Default Ctor. Default Ctor. Default Ctor. Size : 8 Name : nurullah Name : abdulmuttalip Name : giyasetting Name : UlyaYuruk Name : Ahmet Name : Merve Name : John Name : Nishap Move Ctor. Passed Name: Name : abdulmuttalip Dtor Move Ctor. Passed Name: Name : giyasetting Dtor Move Ctor. Passed Name: Name : Nishap Dtor =====Original Svec===== Size : 8 Name : nurullah Name : Name : Name : UlyaYuruk Name : Ahmet Name : Merve Name : John Name : Dtor Dtor Dtor Dtor Dtor Dtor Dtor Dtor */ std::vector svec; svec.reserve(8); svec.emplace_back("nurullah"); svec.emplace_back("abdulmuttalip"); svec.emplace_back("giyasetting"); svec.emplace_back("UlyaYuruk"); svec.emplace_back("Ahmet"); svec.emplace_back("Merve"); svec.emplace_back("John"); svec.emplace_back("Nishap"); std::cout << "Size : " << svec.size() << "\n"; for(auto&& name : svec) name.show_name(); std::cout << "\n"; for_each( std::make_move_iterator(svec.begin()), std::make_move_iterator(svec.end()), [](Myclass&& name){ if(name.get_name().find('i') != std::string::npos) do_something(std::move(name)); } ); std::cout << "\n"; std::cout << "=====Original Svec=====\n"; std::cout << "Size : " << svec.size() << "\n"; for(auto&& name : svec) name.show_name(); std::cout << "\n"; } > Literals in C++: Sabitleri temsil etmektedir. * Örnek 1, #include int main() { /* # OUTPUT # 181==181==181==181 */ std::cout << 181 << "==" << 0b10'11'01'01 << "==" << 0XB5 << "==" << 0265 << "\n"; } Buna ek olarak bazı operator fonksiyonlar vardır ki bu fonksiyonların parametrik yapısı belli kurallara bağlı fakat geri dönüş değeri herhangi bir türden olabilir. Bu fonksiyonlar, tıpkı sınıfların operatör fonksiyonları gibi, ilgili sabitin kullandılması durumunda çağrılırlar. Örneğin, bir sınıf için ".operator*()" fonksiyonu yazılmış olsun. Nasılki bu sınıf türünden iki nesneyi "*" operatörünün operandı yapıldığında ".operator*()" fonksiyonu çağrılıyorsa, sabitlere ilişkin bu özel fonksiyonlar da sabitlerin kullanıldığı yerlerde çağrılmaktadır. Sınıflarda bulunan "operator overloading" amaçlı kullanılan fonksiyonlardan farkı, parametrik yapısını kafamıza göre belirleme HAKKIMIZIN OLMAMASIDIR. Sadece geri dönüş değerini belirleme hakkına sahipiz. Bir diğer engel de, kendi oluşturduğumuz bu "custom" fonksiyonları kullanırken, başına "_" karakterini de eklemeliyiz. Eğer standart olanları çağıracaksak, "_" karakterine gerek YOKTUR. Pekiyi bizler "custom" bir şekilde bu fonksiyonları nasıl yazabiliriz? Karşımıza iki farklı yöntem çıkmaktadır. Bunlar "cooked" ve "uncooked". Bu yöntemlerin her ikisini de tam sayı ve gerçek sayılar söz konusu olduğunda kullanabiliriz. Bu fonksiyonların "constexpr" olması bir zorunluluk değildir. Fakat oladabilir. >> "cooked" : Bu yöntemde fonksiyonun parametresi "unsigned long long" türden olmak zorundadır eğer tam sayılar için kullanılacaksa. Gerçek sayıları kullanacaksa, "long double" türden olmak zorundadır. Şu şekilde oluşturabiliriz: T operator""_(U){ //... } Artık "random-chars" kısmında belirtilen karakterleri bir tam sayı ile birlikte kullanırsak, bu fonksiyon çağrılacaktır. * Örnek 1, #include #include #include int operator""_sr(unsigned long long value){ std::cout << "operator\"\"_sr(" << value << ")\n"; return static_cast(std::sqrt(value)); } int main() { /* # OUTPUT # operator""_sr(823423) Result : 907 */ auto x = 823423_sr; // auto x = operator""_sr(823423_sr); std::cout << "Result : " << x << "\n"; } * Örnek 2, #include #include #include #include constexpr double operator""_mt(long double value){ return static_cast(value); } constexpr double operator""_cm(long double value){ return static_cast(value * 100); } constexpr double operator""_mm(long double value){ return static_cast(value * 1000); } constexpr double operator""_km(long double value){ return static_cast(value / 100); } int main() { /* # OUTPUT # Total Distance : 1.47975e+11 */ constexpr auto TotalDistance = 1.234_km + 230.789_mt + 789654.654123_cm + 147896325.523698741_mm; std::cout << "Total Distance : " << TotalDistance << "\n"; } >> "uncooked" : Bu yöntemde ise fonksiyonun parametresi "const char*" türden olmak zorundadır. Yine "cooked" biçimindeki gibi oluşturabiliriz. * Örnek 1, #include #include #include #include int operator""_sr(const char* p){ std::cout << "operator\"\"_sr(" << std::strlen(p) << ")\n"; while(*p){ std::cout << *p << " " << (int)*p << "\n"; ++p; } return 0; } int main() { /* # OUTPUT # operator""_sr(6) 8 56 2 50 3 51 4 52 2 50 3 51 Result : 0 */ auto x = 823423_sr; std::cout << "Result : " << x << "\n"; } * Örnek 2, #include #include #include #include int operator""_bin(const char* p){ int ret{}; while(*p){ if(!(*p == '0' ||*p == '1')) throw std::runtime_error{"bad binary constant!"}; ret = ret * 2 + (*p - '0'); ++p; } return ret; } int main() { /* # OUTPUT # Result : 661 */ auto x = 1010010101_bin; std::cout << "Result : " << x << "\n"; } > Hatırlatıcı Notlar: >> "std::initializer_list" sınıfı: Bu sınıf türünden bir nesne oluştururken derleyici arka planda bir dizi meydana getirmekte, bu sınıfa geçtiğimiz elemanları bu diziye kopyalayarak da ilgili diziyi doldurmaktadır. Dolayısıyla "Copy" fonksiyonları "delete" edilmiş sınıfları, "std::initializer_list" sınıfı için kullanamayız. * Örnek 1, #include #include #include class Myclass{ public: Myclass(const std::string& name = std::string{"Ulya"}) : m_name{name} { std::cout << "Default Ctor.\n"; } Myclass(const Myclass& other) = delete; Myclass& operator=(const Myclass& other) = delete; Myclass(Myclass&& other) : m_name{std::move(other.m_name)} { std::cout << "Move Ctor.\n"; } Myclass& operator=(Myclass&& other) { m_name = std::move(other.m_name); std::cout << "Move Assignment.\n"; return *this;} ~Myclass()noexcept { std::cout << "Dtor\n"; } private: std::string m_name; }; int main() { /* * Derleyici arka planda bir dizi oluşturup, * dizinin elemanlarını aşağıdaki değerleri kopyalayarak belirlemektedir. */ Myclass a, b, c; std::initializer_list myList{ a,b,c }; // Error: error: use of deleted function ‘Myclass::Myclass(const Myclass&)’ } Diğer yandan "std::initializer_list" sınıfı içerisinde iki adet gösterici barındırmaktadır. Bu göstericilerden birisi arka plandaki o dizinin başlangıç noktasını, diğeri ise bitiş noktasını göstermektedir. Fonksiyon parametresi bu sınıf türünden kendisinin olduğu durumlarda ise bu iki gösterici kopyalanmaktadır. * Örnek 1, Aşağıdaki örnekte "myList" objesinin kendisi değil, içerisindeki göstericiler kopyalanmıştır. #include #include void func(std::initializer_list myList){ std::cout << "Address of myList: " << &myList << '\n'; std::cout << "Dizi adresi : " << myList.begin() << '\n'; } int main() { /* # OUTPUT # Address of myList: 0x7fff7a959680 Dizi adresi : 0x7fff7a959690 ============================ Address of myList: 0x7fff7a959650 Dizi adresi : 0x7fff7a959690 */ std::initializer_list myList{ 2, 4, 6, 8, 10 }; std::cout << "Address of myList: " << &myList << '\n'; std::cout << "Dizi adresi : " << myList.begin() << '\n'; std::cout << "============================\n"; func(myList); } Öte yandan parametresi "std::initializer_list" olan fonksiyonlara geçici nesne de pek tabii gönderebiliriz. Bunun istisnai durumu, fonksiyon şablonlarıdır. Çünkü fonksiyon şablonlarındaki "T" için yapılan tür çıkarım kuralları ile "auto" kelimesi için yapılan tür çıkarım kuralları "std::initializer_list" için farklıdır. "Auto Type Deduction" kurallarına göre tür çıkarımı "std::initializer_list" yönüne doğru yapılırken, "Template Argument Deduction" kurallarına göre durum SENTAKS HATASIDIR. Tür çıkarımı söz konusu olduğunda ikisi arasındaki iki fark da budur. * Örnek 1, #include #include template void func(T myList){ for(auto value: myList) std::cout << value << " "; std::cout << "\n"; } int main() { /* # OUTPUT # 2 4 6 8 10 */ /* * Aşağıdaki "myList" değişkeninin türü "std::initializer_list" * türündendir. Eğer "=" yerine direkt olarak "Direct-list-initialization" * yapılsaydı, sentaks hatası olacaktı. Çünkü bu sefer sadece tek bir * elemanın olması gerekmektedir. */ auto myList = { 2, 4, 6, 8, 10 }; // "myList" is of type "std::initializer_list" // auto myList{ 2, 4, 6, 8, 10 }; // direct-list-initialization of ‘auto’ requires exactly one element [-fpermissive] func(myList); // func({10, 8, 6, 4, 2}); // "Template Argument Deduction" is failed. } Bütün bunlara ek olarak şu noktaya da dikkat etmeliyiz; "STL" içerisindeki "Container" sınıfların "insert" fonksiyonlarını teker teker çağırmak yerine bu fonksiyonları tek seferde çağırmak daha verimli olacaktır. Bunun için yine ilgili fonksiyonların "std::initializer_list" parametreli "overload" larını kullanabiliriz. * Örnek 1, #include #include #include std::vector func(int a, int b, int c){ std::vector ivec; ivec = {a,b,c}; return ivec; } int main() { /* # OUTPUT # 31 32 33 */ auto myVec{ func(31, 32, 33) }; for(auto value: myVec) std::cout << value << " "; } Son olarak şunu da belirtmekte fayda vardır ki "std::initializer_list" parametreli "Ctor." ile başka parametreli "Ctor." fonksiyonu aynı seçilebilirliğe sahipse, "std::initializer_list" parametreli olan seçilecektir. * Örnek 1, #include #include #include class Myclass{ public: Myclass(int) { std::cout << "(int)\n"; } Myclass(std::initializer_list) { std::cout << "(std::initializer_list)\n"; } }; int main() { /* # OUTPUT # 31 32 33 */ Myclass m{31}; // (std::initializer_list) Myclass mm(32); // (int) Myclass mmm = 33; // (int) Myclass n{31, 32, 33}; // (std::initializer_list) // Myclass nn(31, 32, 33); // no matching function for call to ‘Myclass::Myclass(int, int, int)’ Myclass nnn = {31, 32, 33}; // (std::initializer_list) } (09:43 - 10:27(1. Araya Kadar)) >> Sınıfımızın veri elemanlarını "init." ederken birden farklı yönteme başvurabiliriz. Örneğin, parametresi "const-reference" olan bir fonksiyon da yazabiliriz veya parametresi bir sınıf türünden olup, aldığı argümanları "std::move" eden de. * Örnek 1, #include #include #include class PersonClassic{ public: PersonClassic(const std::string& name, const std::string& surname) : m_name{name}, m_surname{surname} {} private: std::string m_name; std::string m_surname; }; class PersonInitMove{ public: PersonInitMove(std::string name, std::string surname) : m_name{std::move(name)}, m_surname{std::move(surname)} {} private: std::string m_name; std::string m_surname; }; class PersonInitOverload{ public: PersonInitOverload(const std::string& name, const std::string& surname) : first{name}, last{surname}{} PersonInitOverload(const std::string& name, std::string&& surname) : first{name}, last{std::move(surname)}{} PersonInitOverload(std::string&& name, const std::string& surname) : first{std::move(name)}, last{surname}{} PersonInitOverload(std::string&& name, std::string&& surname) : first{std::move(name)}, last{std::move(surname)}{} PersonInitOverload(const char* name, const char* surname) : first{name}, last{surname}{} PersonInitOverload(const char* name, const std::string& surname) : first{name}, last{surname}{} PersonInitOverload(const char* name, std::string&& surname) : first{name}, last{std::move(surname)}{} PersonInitOverload(const std::string& name, const char* surname) : first{name}, last{surname}{} PersonInitOverload(std::string&& name, const char* surname) : first{std::move(name)}, last{surname}{} private: std::string first; std::string last; }; std::chrono::nanoseconds measure(int num); constexpr int n = 100'000; const char* pname = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; const char* psurname = "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"; //using Person = PersonClassic; using Person = PersonInitMove; //using Person = PersonInitOverload; int main() { /* # OUTPUT # // "Person" is "PersonClassic" test result for 100000 iterations: 109.701ms 3 inits take on average : 1097ns // "Person" is "PersonInitMove" test result for 100000 iterations: 122.841ms 3 inits take on average : 1228ns // "Person" is "PersonInitOverload" test result for 100000 iterations: 64.606ms 3 inits take on average : 646ns */ measure(20); // Başlangıç maliyetlerini örtbas için. std::chrono::nanoseconds nanosecond_duration{ measure(n) }; std::chrono::duration millisecond_duration{ nanosecond_duration }; std::cout << "test result for " << n << " iterations: " << millisecond_duration.count() << "ms\n"; std::cout << "3 inits take on average : " << nanosecond_duration.count() / n << "ns\n"; } std::chrono::nanoseconds measure(int num) { std::chrono::nanoseconds total_duration{}; for(int i = 0; i < num; ++i){ std::string name(1000, 'U'); std::string surname(1000, 'Y'); auto start = std::chrono::steady_clock::now(); Person p1{pname, psurname}; Person p2{name, surname}; Person p3{std::move(name), std::move(surname)}; auto end = std::chrono::steady_clock::now(); total_duration += end - start; } return total_duration; } >> Sınıfların "push_back" fonksiyonları iki "overload" a sahiptir. Bunlar "const T&" türünden ve "T&&" türünden "overload" lardır. Bu iki fonksiyonun takribi gösterimi aşağıdaki biçimde olsun; template> class Vector{ public: //... void push_back(const T& t){ //... new T(t); // Bu aşamada "T" bir sınıf türü ise "Copy Ctor." çağrılacaktır. //... } void push_back(T&& t){ //... new T(std::move(t)); // Bu aşamada "T" bir sınıf türü ise "Move Ctor." çağrılacaktır. //... } }; Buradan da görüleceği üzere "T" sınıf türünden elemanımız, "Moved From State" halini alabilir. Şöyleki; * Örnek 1, #include #include #include int main() { /* # OUTPUT # Name : Ahmet List : Ahmet Name : List : Ahmet Ahmet */ std::vector svec; std::string name{"Ahmet"}; svec.push_back(name); std::cout << "Name : " << name << "\n"; std::cout << "List : "; for(auto name : svec) std::cout << name << " "; std::cout << "\n"; svec.push_back(std::move(name)); std::cout << "Name : " << name << "\n"; std::cout << "List : "; for(auto name : svec) std::cout << name << " "; std::cout << "\n"; } Bu iki fonksiyona ek olarak birde "emplace_back" isimli fonksiyon vardır ki bu fonksiyon çağrısı ne "Copy Ctor." çağrısına ne de "Move Ctor." çağrısını yapmaktadır. Direkt olarak ilgili nesneyi yerinde oluşturmaktadır. Bu fonksiyonu da kabaca şu şekilde gösterebiliriz: template> class Vector{ public: //... template constexpr T& emplace_back(Args&& ...args){ new T(std::forward(args)...); } //... }; Buradan da görüleceği üzere argüman olarak aldığı ifadeleri direkt olarak ilgili sınıfın "Ctor." fonksiyonuna geçmektedir. Şöyleki: * Örnek 1, #include #include #include class Myclass{ public: Myclass(const std::string& name) : m_name{name} { std::cout << "Default Ctor.\n"; } Myclass(const Myclass& other) : m_name{other.m_name} { std::cout << "Copy Ctor.\n"; } Myclass& operator=(const Myclass& other) { m_name = other.m_name; std::cout << "Copy Assignment.\n"; return *this;} Myclass(Myclass&& other) : m_name{std::move(other.m_name)} { std::cout << "Move Ctor.\n"; } Myclass& operator=(Myclass&& other) { m_name = std::move(other.m_name); std::cout << "Move Assignment.\n"; return *this;} ~Myclass()noexcept { std::cout << "Dtor\n"; } void show_name()const { std::cout << "Name : " << m_name << "\n"; } private: std::string m_name; }; int main() { /* # OUTPUT # Default Ctor. Name : Ulya ============== Default Ctor. Name : Ulya Name : Yuruk Dtor Dtor */ std::vector svec; svec.reserve(3); svec.emplace_back("Ulya"); for(auto&& name: svec) name.show_name(); std::cout << "==============\n"; svec.emplace_back("Yuruk"); for(auto&& name: svec) name.show_name(); std::cout << "\n"; } Artık ne "Move Ctor." ne de "Copy Ctor." çağrısı yapıldı. Dolayısıyla bu iki fonksiyonun getirdiği maliyetten kaçınmış olduk. Bu da demektir ki "Move Only" sınıfları da kullanabiliriz. Bu fonksiyonun getirdiği bir diğer fayda ise, yerinde hayata getireceğimiz nesnenin "Ctor." fonksiyonu eğer "R-Value" parametreli ise, "emplace_back" e bir "R-Value Expression" geçtiğimiz zaman yine "Move Semantics" işletilmiş olacaktır. Çünkü "R-Value" olan ifadeler yine "R-Value" olarak hedef nesnenin "Ctor." fonksiyonuna geçilecek. /*================================================================================================================================*/ (07_16_07_2023) > Literals in C++ (devam): Anımsayacağımız üzere "row" ve "cooked" biçimde "literal" oluşturabiliyorduk. "row" biçimde oluşturduklarımız "const char*" parametreli, "cooked" olanlar ise "unsigned long long" veya "long double" parametreli olacaktır. Pekiyi bizler başka hangi parametreleri "custom" fonksiyonlar oluşturabiliriz? >> Normal şartlarda "const char*" parametreli olanlar, argümanı bir yazı biçiminde aldıklarından, yazının adresini geçmiş oluyorduk. Pekiyi böylesi fonksiyonlara "" işareti kullanarak argüman geçmemiz mümkün müdür? Bunun için, tıpkı "++" operatörünün ön ek olarak kullanıldığı senaryolar için ".operator++()" fonksiyonuna boş bir parametre belirtir gibi, bizler de "const char*" parametreli fonksiyona boş bir parametre daha belirtmeliyiz. Fakat ikinci parametrenin türü yine "std::size_t" / "unsigned long" türünden olmalıdır. * Örnek 1, Aşağıdaki örneğin çıktısında da görüleceği üzere "" kullanarak argüman geçtiğimiz zaman sentaks hatası meydana gelmektedir. #include #include int operator""_sr(const char* p){ std::cout << "operator\"\"_sr(" << std::strlen(p) << ")\n"; while(*p){ std::cout << *p << " " << (int)*p << "\n"; ++p; } return 0; } int operator""_vl(unsigned long long value){ std::cout << "operator\"\"_vl(" << value << ")\n"; return 0; } int main() { /* # OUTPUT # operator""_sr(9) 1 49 2 50 3 51 4 52 5 53 6 54 7 55 8 56 9 57 operator""_vl(987654321) */ 123456789_sr; //"12345678"_sr; // error: no matching literal operator for call to 'operator""_sr' with arguments of types 'const char *' and 'unsigned long' 987654321_vl; } * Örnek 2, Artık "operator""_srsr" fonksiyonunu çağırırken "" atomunu kullanarak parametre geçebiliriz. #include #include int operator""_sr(const char* p){ std::cout << "operator\"\"_sr(" << std::strlen(p) << ")\n"; while(*p){ std::cout << *p << " " << (int)*p << "\n"; ++p; } return 0; } int operator""_srsr(const char* p, std::size_t len){ std::cout << "operator\"\"_srsr(" << len << ")\n"; while(*p){ std::cout << *p << " " << (int)*p << "\n"; ++p; } return 0; } int operator""_vl(unsigned long long value){ std::cout << "operator\"\"_vl(" << value << ")\n"; return 0; } int main() { /* # OUTPUT # operator""_sr(9) 1 49 2 50 3 51 4 52 5 53 6 54 7 55 8 56 9 57 operator""_srsr(8) 1 49 2 50 3 51 4 52 5 53 6 54 7 55 8 56 operator""_vl(987654321) */ 123456789_sr; // "12345678"_sr; // error: no matching literal operator for call to 'operator""_sr' with arguments of types 'const char *' and 'unsigned long' "12345678"_srsr; 987654321_vl; } * Örnek 3, #include #include std::string operator""_st(const char* p, std::size_t){ return std::string{p} + p; } int main() { auto name = "Ahmet Kandemir"_st; std::cout << "|" << name << "|\n"; // |Ahmet KandemirAhmet Kandemir| } * Örnek 4, #include #include #include std::vector operator""_v(const char* p, std::size_t){ std::vector n; while(*p) n.push_back(*p++); return n; } int main() { auto vec = "murat yilmaz"_v; std::cout << "Size: " << vec.size() << "\n"; // Size: 12 } >> Bir diğer alternatif ise "char" parametreli fonksiyon yazmaktır. * Örnek 1, #include #include int operator""_i(char p){ return static_cast(p); } int main() { /* # OUTPUT # A : A 65 */ std::cout << "A : " << 'A'<< " " << 'A'_i << "\n"; } Bütün bunlar göz önüne alındığında fonksiyonlarımız "cooked" olmalıdır. "cooked" olmaları işimizi görmüyorsa, "raw" yapmalıyız. Fakat hem "cooked" hem "raw" aynı isim alanında görünür olmamalıdır çünkü ya "Ambiguity" oluşacak ya da birisi seçilecektir. Dolayısıyla bizler bu fonksiyon bildirimlerini bir "namespace" içerisine almalıyız. Şimdi de genel olarak örneklere bakalım: * Örnek 1, Aşağıdaki örnekte ise ya "Ctor." fonksiyonu "explicit" yapılmalı ya da "PreventUsage" parametreli bir "Ctor." yazılmalıdır. Aksi halde "Meter" sınıfına direkt olarak "double" tür atayabiliriz. #include #include #include class Meter{ public: Meter() = default; // explicit Meter(double dval) : m_val{dval} {} // WAY - I // WAY - II struct PreventUsage{}; Meter(PreventUsage, double dval) : m_val{dval} {} void print()const { std::cout << m_val << "\n"; } private: double m_val{}; }; Meter operator""_Mt(long double val){ return Meter{ Meter::PreventUsage{}, static_cast(val) }; } int main() { Meter m; m = 4.67_Mt; m.print(); // 4.67 } > "Raw-String Literal" : Anımsanacağımız üzere "string" içerisinde {", \} karakterlerini kullanabilmek için ve uzun metinleri de bölerek alt satırdan devam etmesini sağlamak için '\' karakterini kullanmamız gerekmektedir. * Örnek 1, #include int main() { const char* name{ "\\Ahmet \"Kandemir\" \ Pehlivanli \\" }; std::cout << name << "\n"; // \Ahmet "Kandemir" Pehlivanli \ } Çıktıdan da görüleceği üzere ilgili "string" i hem yazmak hem de okumak çok yorucu. İşte bu karmaşıklığı gidermek için "Raw-String Literal" kavramı geliştirilmiştir ki bu aslında bir notasyondur. * Örnek 1, #include #include int main() { const char* name{ "\\Ahmet \"Kandemir\" Pehlivanli\\" }; std::cout << name << "\n"; // \Ahmet "Kandemir" Pehlivanli\ const char* surname = R"(\Ahmet "Kandemir" Pehlivanli\)"; std::cout << surname << "\n"; // \Ahmet "Kandemir" Pehlivanli\ } Eğer yazının kendisinde {"(, )"} karakterlerinin de olmasını istiyorsak, tırnak işareti ile açılan parantez ve kapanan parantez ile tırnak işareti arasında bir ayıraç koymalıyız. * Örnek 1, #include #include int main() { auto surname = R"ahmo(Ahmet "(Kandemir)" Pehlivanli)ahmo"; std::cout << surname << "\n"; // Ahmet "(Kandemir)" Pehlivanli } > "IO Manipulators" : >> "quoted" fonksiyonu: Argüman olarak bir yazıyı alır. Fonksiyonun geri dönüş değeri, derleyiciye bağlıdır. * Örnek 1, #include #include #include #include int main() { std::ostringstream oss; oss << std::quoted("ulya", '*'); std::cout << oss.str(); // *ulya* } * Örnek 2, #include #include #include int main() { std::cout << std::quoted("ahmet") << "\n"; // "ahmet" std::cout << std::quoted("\" ahmet \"") << "\n"; // "\" ahmet \"" std::cout << std::quoted(R"( "ali" "can" "nur" )") << "\n";// " \"ali\" \"can\" \"nur\" " } * Örnek 3, #include #include #include #include int main() { std::ostringstream oss; oss << std::quoted("*ulya*", '*'); std::cout << oss.str(); // *\*ulya\** } * Örnek 4, #include #include #include #include int main() { std::ostringstream oss; oss << std::quoted("+ulya+", '+', '-'); std::cout << oss.str(); // +-+ulya-++ } * Örnek 5, #include #include #include #include int main() { std::istringstream iss{ "\"Ulya\"" }; { std::string name; iss >> name; std::cout << name; // "Ulya" } { std::string name; iss >> std::quoted(name); std::cout << name; // Ulya } } * Örnek 6, #include #include #include #include int main() { /* # OUTPUT # */ std::stringstream ss; std::string name; ss << std::quoted("\"ali\""); ss >> std::quoted(name); std::cout << name << "\n"; // "ali" } > "std::string_view" sınıfı : Bu sınıf türünden nesneler, bir yazının gözlemcisi olarak kullanılmaktadır. Anımsanacağımız üzere "yazı" ile kastedilen bellekte ardışık bir şekilde tutulan bayt kümesidir. Örneğin, aşağıdaki tanımlar birer yazıyı belirtmektedir: -> "Ahmet" biçimindeki bir "string-literal". -> "char name[150]" biçimindeki bir "C-Style Array". -> "std::string" sınıf türünden nesneler birer yazı tutmaktadır. -> "std::vector" türünden nesneler bir yazıyı temsil etmektedir. -> "std::array" türünden nesneler yine bir yazıyı temsil etmektedir. İşte bütün bu yazılara gözlemci olarak yaklaşabileceğimiz sınıf ise "std::string_view" sınıfıdır. Tıpkı bir pencereden kapının önündeki arabayı izlemek gibidir. Gözlemlediğimiz şeyin sahibinin kim olduğu bizim için önemli değildir. Yani salt okuma amaçlı kullanılan fakat bunun için kopyalama yapmayan sınıf da diyebiliriz. Çünkü bu sınıf öyle bir sınıftır ki veri elemanı olarak ya iki adet göstericiye ya da bir adet gösterici ve uzunluk bilgisi için değişkene sahiptir. Böylelikle arka planda "pointer-arithmetic" işlemleri ile argüman olarak aldığı yazı üzerinde gezinmektedir. "std::string_view" sınıfı, "std::string" sınıfının "get" amaçlı arayüzünü kullanmaktadır. * Örnek 1, #include #include #include /* Bu fonksiyona gecilen arguman cok uzun ise kopyalamaya oluşabilir. */ void func(const std::string& str) { std::cout << str << "\n"; } /* Bu fonksiyona ise "std::string" türünden bir nesneyi arguman yapamam. */ void foo(const char* p){ std::cout << p << "\n"; } /* Burada kopyalanan, iki göstericiye sahip bir "std::string_view" nesnesi */ void MyFoo(std::string_view sv){ std::cout << sv << "\n"; } int main() { /* # OUTPUT # Bugun hava cok sicak ve ne yazik ki orman yanginlari hala devam etmektedir. Bugun hava cok sicak ve ne yazik ki orman yanginlari hala devam etmektedir. */ func("Bugun hava cok sicak ve ne yazik ki orman yanginlari hala devam etmektedir."); std::string name; //foo(name); // Error MyFoo("Bugun hava cok sicak ve ne yazik ki orman yanginlari hala devam etmektedir."); } * Örnek 2, Aşağıdaki örnekten de görüleceği üzere iki adet gösterici barındırılmaktadır. #include #include #include int main() { /* # OUTPUT # sizeof char* : 8 sizeof std::string_view : 16 */ std::cout << "sizeof char* : " << sizeof(char*) << "\n"; std::cout << "sizeof std::string_view : " << sizeof(std::string_view) << "\n"; } * Örnek 3, #include #include #include int main() { /* # OUTPUT # */ std::string str(100'000, 'u'); size_t idx{ 10'000 }; size_t n{ 50'000 }; /* * Bu fonksiyon ilgili aralıktaki yazıyı kullanarak yeni bir "std::string" * nesnesini geri döndürmektedir. Dolayısıyla bizler kendimiz gösterici * oluşturup, ilgili "range" içerisindekilere bakmamız gerekmektedir. İşte * buradaki maliyetten kaçınmak için "std::string_view" sınıfını kullanabiliriz. */ auto range{ str.substr(idx, n) }; } Fakat bu sınıf türünü kullanırken "dangling-pointer" senaryolarına ÇOK DİKKAT ETMELİYİZ. Yazının sahibi biz olmadığımız için, arka plandaki göstericiler "dangling-pointer" olabilirler. İşte kopyalama maliyetinden kazandığımız maliyet, bu noktada kaybedebiliriz. Diğer yandan dikkat etmemiz gereken nokta ise bu sınıf türünden nesneler "null-terminated" OLMAYAN YAZILAR üzerinde de işlem yapabilmektedir. Böyle yazılar üzerinde işlem yaparken çağıracağımız "get" fonksiyonları, "null-terminated" yazı isteyen fonksiyonlara argüman olarak geçmemiz yine bir felakete yol açacaktır. Diğer yandan "remove" eki içeren üye fonksiyonlar ise yine yazının kendisini değil, "range" olan aralığı değiştirmektedir. * Örnek 1, İçi boş bir nesne. #include #include int main() { /* # OUTPUT # size :0 length :0 true true */ std::string_view sv{}; // İçi boş bir nesne. // std::string_view sv; // İçi boş bir nesne. std::cout << "size :" << sv.size() << "\n"; std::cout << "length :" << sv.length() << "\n"; std::cout << std::boolalpha << sv.empty() << "\n"; std::cout << (sv.data() == nullptr) << "\n"; } * Örnek 2, "C-String" parametreli "Ctor." #include #include #include int main() { /* # OUTPUT # size :12 length :12 false false Ulya Yürük ---------- size :12 length :12 false false Ulya Yürük ---------- size :12 length :12 false false Ulya Yürük */ { std::string_view sv{"Ulya Yürük"}; std::cout << "size :" << sv.size() << "\n"; std::cout << "length :" << sv.length() << "\n"; std::cout << std::boolalpha << sv.empty() << "\n"; std::cout << (sv.data() == nullptr) << "\n"; std::cout << sv << "\n"; } puts("----------"); { const char* p = "Ulya Yürük"; std::string_view sv{p}; std::cout << "size :" << sv.size() << "\n"; std::cout << "length :" << sv.length() << "\n"; std::cout << std::boolalpha << sv.empty() << "\n"; std::cout << (sv.data() == nullptr) << "\n"; std::cout << sv << "\n"; } puts("----------"); { const char* p = "Ulya Yürük"; std::string_view sv; sv = p; std::cout << "size :" << sv.size() << "\n"; std::cout << "length :" << sv.length() << "\n"; std::cout << std::boolalpha << sv.empty() << "\n"; std::cout << (sv.data() == nullptr) << "\n"; std::cout << sv << "\n"; } } * Örnek 3, "data" parametreli "Ctor.". Yani "const char*" ve "size" parametreli "Ctor." #include #include #include #include int main() { /* # OUTPUT # Ulya Yürük Ulya Yürük Ulya ---------- Ulya Yürük Ulya Yürük ---------- Ulya Yuruk Ulya Yuruk Ulya Yuruk Ulya Yuruk ---------- Ulya ---------- |Ulya| |Ulya */ { char str[] = "Ulya Yürük"; std::string_view sv1 = str; std::string_view sv2(str, 4); std::cout << str << "\n"; std::cout << sv1 << "\n"; std::cout << sv2 << "\n"; } puts("----------"); { char str[] = "Ulya Yürük"; std::string_view sv1(str, 4); std::cout << str << "\n"; std::cout << sv1.data() << "\n"; } puts("----------"); { std::array ar{ 'U', 'l','y','a',' ','Y','u','r','u','k' }; std::string_view sv(ar.data(), ar.size()); std::cout << sv << "\n"; std::cout << sv.data() << "\n"; // Bu fonksiyon "null-terminated" yazı istediği için Tanımsız Davranış oluşacaktır. puts(sv.data()); printf("%s\n", sv.data()); } puts("----------"); { std::string_view sv1 = "Ulya\0\0\0Yürük"; puts(sv1.data()); } puts("----------"); { const char* p = "Ulya\0\0\0Yürük"; std::string_view sv(p, 14); std::cout << "|" << sv.data() << "|\n"; std::cout << "|" << sv << "|\n"; } } * Örnek 4, "std::string" parametreli "Ctor." fonksiyonu yoktur. Fakat bünyesindeki "std::string_view" e dönüştürmek operatör fonksiyonu "explicit" olmadığı için "std::string" türden nesneleri argüman olarak alabilmektedir. Yine aynı şekilde atama için de geçerlidir. #include #include #include int main() { /* # OUTPUT # */ { std::string name{ "Ulya" }; std::string_view sv_name{ name }; std::cout << name << " | " << sv_name << "\n"; } puts("----------"); { std::string name{ "Ulya" }; std::string_view sv_name; sv_name = name; std::cout << name << " | " << sv_name << "\n"; } puts("----------"); } * Örnek 5, "const char*" ve "const char*" parametreli "Ctor." fonksiyonu da bulunmaktadır. #include #include #include int main() { /* # OUTPUT # Yuruk ---------- */ { char name[] = "Ulya Yuruk"; std::string_view sv{ name + 5, name + 10 }; std::cout << sv << "\n"; } puts("----------"); } * Örnek 6, C++10 ile birlikte "range" parametreli "Ctor." da eklendi. #include #include #include int main() { /* # OUTPUT # Ulya Yuruk ---------- */ { std::string name{ "Ulya Yuruk" }; // "std::string" yerine "std::vector" da olabilirdi. std::string_view sv{ name.begin(), name.end() }; // Since C++20 std::cout << sv << "\n"; } puts("----------"); } * Örnek 7, Burada bizler yazının sahibi olmadığını unutmamalıyız. #include #include #include int main() { /* # OUTPUT # |Ulya Yuruk| U k |Mlya Puruk| M k */ char str[] = "Ulya Yuruk"; std::string_view sv{str}; std::cout << "|" << sv << "|" << "\n"; std::cout << sv.front() << "\n"; std::cout << sv.back() << "\n"; str[0] = 'M'; str[5] = 'P'; std::cout << "|" << sv << "|" << "\n"; std::cout << sv.front() << "\n"; std::cout << sv.back() << "\n"; } * Örnek 8, "nullptr" parametreli "Ctor". fonksiyonu var fakat "delete" edilmiştir, C++23 ile birlikte. #include #include #include int main() { /* # OUTPUT # Segmentation fault */ std::string_view sv = nullptr; } * Örnek 9, "std::string" sınıfının "std::string_view" parametreli "Ctor." fonksiyonu mevcuttur. Fakat bu fonksiyon "explicit" olarak nitelenmiştir. Dolayısıyla "std::string_view" türüne ÖRTÜLÜ DÖNÜŞÜM SENTAKS HATASIDIR. #include #include #include std::string foo(){ std::string_view sv{ "Ulya" }; return sv; // Sentaks Hatası } void func(std::string str){ //... } int main() { /* # OUTPUT # Ulya Ulya */ std::string_view sv{ "Ulya" }; std::cout << sv << "\n"; std::string str{sv}; std::cout << str << "\n"; // std::string str_str = sv; // ERROR // std::cout << str_str << "\n"; func(sv); // Sentaks Hatası auto xxx = foo(); // Sentaks Hatası } * Örnek 10, "Dangling Pointer" mevzusuna çok dikkat etmeliyiz. #include #include #include std::string foo(){ return "Ulya Yuruk"; } int main() { /* # OUTPUT # Ulya Yuruk Ulya Yuruk Ulya Yuruk Ulya Yuruk */ const std::string& cr_s{ foo() }; std::cout << cr_s << "\n"; // Life Extension std::string&& r_s{ foo() }; std::cout << r_s << "\n"; // Life Extension auto ptr = foo().c_str(); std::cout << ptr << "\n"; // Dangling Pointer std::string_view sv{ foo() }; /* * Dangling Pointer: Burada "sv" nesnesi içerisindeki göstericilerin * gösterdiği adres değerleri artık geçersiz hale gelmiştir. */ std::cout << sv << "\n"; } * Örnek 11, Aşağıdaki örnekte fonksiyonun geri döndürdüğü nesnenin ömrü bittiği için "Dangling Pointer" meydana gelmiştir. #include #include #include class Person{ public: Person(const std::string& other) : m_name{other} {} void print() const { std::cout << m_name << "\n"; } std::string_view get_name() const { return m_name; } private: std::string m_name{}; }; Person create_person(){ return Person{ "Ulya Yuruk" }; } int main() { /* # OUTPUT # |Ulya Yuruk| */ auto name = create_person().get_name(); std::cout << "|" << name << "|\n"; // Dangling Pointer } * Örnek 12, ".append()" fonksiyonunun çağrılmasıyla birlikte "reallocation" gerçekleşeceğinden, "sv" içerisindeki göstericiler artık "Dangling Pointer" halini almıştır. #include #include #include int main() { /* # OUTPUT # |aaaaaaaaaa| |aaaaaaaaaaxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx| |� */ std::string str(10, 'a'); std::string_view sv{ str }; std::cout << "|" << sv << "|\n"; str.append(1000, 'x'); std::cout << "|" << str << "|\n"; std::cout << "|" << sv << "|\n"; } * Örnek 13, #include #include #include int main() { /* # OUTPUT # Ulya Ulya */ auto p = new std::string{"Ulya"}; std::string_view sv{*p}; std::cout << sv << "\n"; delete p; std::cout << sv << "\n"; } * Örnek 14, #include #include #include std::string operator+(std::string_view sv1, std::string_view sv2){ return std::string(sv1) + std::string(sv2); } template T concat(const T& x, const T& y){ return x + y; } int main() { /* # OUTPUT # XRPG */ std::string_view sv = "Ulya"; /* * "concat" çağrısı sonrasında "T" türü için "std::string_view" * çıkarımı yapılacaktır. Fakat "+" operatörü ile bizler "std::string" * elde etmiş olacağız. Yani; * İlk olarak ".operator+()" ile "std::string" türünden geçici bir * nesne elde edilecek. * Daha sonra bu geçici nesne ile yine "std::string_view" türünden * geçici bir nesne "concat" fonksiyonu ile geri döndürülecek. * Her ne kadar "value" değişkeninin türü "std::string_view" olsa da * "Life Extension" olmadığı için "value" içerisindeki göstericiler * "Dangling Pointer" olacaklar çünkü gösterdikleri geçici nesnenin * ömrü bir sonraki satırda bitmiş olacak. Eğer "concat" fonksiyonunun * geri dönüş değeri "auto" olsaydı, "std::string" türünden açılım * yapılacağı için herhangi bir problem oluşmayacaktı. */ auto value = concat(sv, sv); std::cout << value << "\n"; } * Örnek 15, #include #include #include std::string_view foo(std::string s){ /* * Fonksiyonun parametre değişkeni otomatik ömürlü olduğu için * hayatı bitecektir. Dolayısıyla geri dönüş değeri olan * "std::string_view" içerisinde bulunan göstericiler "Dangling Pointer" * halini alacaktır. */ return s; } int main() { //... } * Örnek 16, "std::string_view" sınıfının bir çok üye fonksiyonu "constexpr" fonksiyondur. #include #include #include int main() { /* # OUTPUT # */ constexpr std::string_view sv{ "Ulya Yuruk" }; constexpr auto len{ sv.length() }; constexpr auto cs{ sv.front() }; constexpr auto ce{ sv.end() }; constexpr auto iter_beg = sv.begin(); constexpr auto iter_end = sv.end(); constexpr auto idx = sv.find('k'); constexpr auto idxx = sv.find_first_not_of("Akhpmt"); } * Örnek 17, #include #include #include int main() { /* # OUTPUT # [10] => ulya yuruk [10] => ulya yuruk [5] => yuruk [5] => yuruk [4] => yuru [4] => yuruk */ std::string_view sv{ "ulya yuruk" }; std::cout << "[" << sv.size() << "] => " << sv << "\n"; std::cout << "[" << sv.size() << "] => " << sv.data() << "\n"; sv.remove_prefix(5); std::cout << "[" << sv.size() << "] => " << sv << "\n"; std::cout << "[" << sv.size() << "] => " << sv.data() << "\n"; sv.remove_suffix(1); std::cout << "[" << sv.size() << "] => " << sv << "\n"; std::cout << "[" << sv.size() << "] => " << sv.data() << "\n"; } * Örnek 18, #include #include #include int main() { /* # OUTPUT # ( basimda bir bosluk hissi var.) (basimda bir bosluk hissi var.) */ std::string str{ " basimda bir bosluk hissi var." }; std::string_view sv{ str }; sv.remove_prefix( std::min( sv.find_first_not_of(" "), sv.size() ) ); std::cout << "(" << str << ")\n"; std::cout << "(" << sv << ")\n"; } * Örnek 19, #include #include #include void foo(std::string){ std::cout << "std::string\n"; } void foo(std::string_view){ std::cout << "std::string_view\n"; } int main() { /* # OUTPUT # */ //foo("Ulya Yuruk"); // Ambiguity foo("Ulya Yuruk"s); // std::string foo("Ulya Yuruk"sv);// std::string_view } * Örnek 20, #include #include #include void foo(const char*){ std::cout << "const char*\n"; } void foo(std::string){ std::cout << "std::string\n"; } void foo(std::string_view){ std::cout << "std::string_view\n"; } int main() { /* # OUTPUT # */ foo("Ulya Yuruk"); // const char* foo("Ulya Yuruk"s); // std::string foo("Ulya Yuruk"sv);// std::string_view } * Örnek 21, #include #include #include class Myclass{ public: Myclass(const std::string& other) : m_s{other} {} Myclass(std::string_view other) : m_s{other} {} Myclass(std::string other) : m_s{std::move(other)} {} private: std::string m_s; }; int main() { /* Input Parameter const std::string& std::string_view std::string w/ std::move const char* 2 allocations 1 allocations 1 allocations + move const char* SSO 2 copies 1 copy 2 copies L-Value 1 allocations 1 allocations 1 allocation + move L-Value SSO 1 copy 1 copy 2 copies R-Value 1 allocation 1 allocation 2 moves R-Value SSO 1 copy 1 copy 2 copies *SSO : Small String Optimization */ } * Örnek 22, #include #include #include int main() { /* # OUTPUT # true true true */ std::string_view sv{"Ulya Yuruk"}; std::cout << std::boolalpha << sv.starts_with("Ulya") << "\n"; // C++20 std::cout << std::boolalpha << sv.ends_with("Yuruk") << "\n"; // C++20 std::cout << std::boolalpha << sv.contains("uru") << "\n"; // C++23 } > "std::optional" : C++17 ile dile eklenmiştir. Sınıf şablonudur. Ya "T" türünden bir değere sahip ya da bomboş. Dolayısıyla herhangi bir değere sahip olabilir ama olmayadabilir. Örneğin, boş bir bardak ne kadar doğalsa bardağın dolu olması da bir o kadar doğaldır. Ya da bir kişinin göbek adının olması veya bir "nick" kullanması durumlarında da bu sınıf türü kullanılabilir. * Örnek 1, #include #include #include int main() { /* # OUTPUT # false 31 ----- Ulya ----- 5 ----- ----- Exception Caught!: bad optional access */ { std::optional ox{std::nullopt}; std::cout << std::boolalpha << ox.has_value() << "\n"; // false ox = 31; if(ox){ std::cout << *ox << "\n"; // 31 } } puts("-----"); { std::string name{ "Ulya" }; std::optional surname{ &name }; std::cout << **surname << "\n"; // std::cout << *(surname.operator*()) << "\n"; } puts("-----"); { std::optional x = 5; // CTAT : 'x' is of type "std::optional" std::cout << *x << "\n"; } puts("-----"); { std::optional ox; try{ auto val = *ox; // Tanımsız Davranış auto len = ox->length(); // Tanımsız Davranış } catch(const std::exception& ex){ std::cout << "Exception Caught!: " << ex.what() << "\n"; } } puts("-----"); { std::optional ox; try{ auto val = ox.value(); // Throws An Exception } catch(const std::exception& ex){ std::cout << "Exception Caught!: " << ex.what() << "\n"; // Exception Caught!: bad optional access } } } > Hatırlatıcı Notlar: >> "Trailing Return Type" vs "Auto Return Type" : * Örnek 1, Aşağıdaki örnekte "foo" ve "func" fonksiyonlarının geri dönüş değeri aynı tür olacaktır. // Auto Return Type : template auto foo(T x){ return x+x; } // Trailing Return Type : template auto func(T) -> decltype(x+x){ //... return x+x; } * Örnek 2, Aşağıdaki örnekte ise "foo" nun geri dönüş değerinin türü "x+x" ifadesine, "func" ın ki ise "x.foo()" çağrısına bağlıdır. Artık "func" ın "return" ifadesine göre çıkarım YAPILMAYACAKTIR. // Auto Return Type : template auto foo(T x){ return x+x; } // Trailing Return Type : template auto func(T) -> decltype(x.foo()){ //... return x+x; } /*================================================================================================================================*/ (08_22_07_2023) > "std::optional" (devam): * Örnek 1, #include #include #include #include class Myclass{ public: Myclass() { std::cout << "Default Ctor.\n"; } ~Myclass() { std::cout << "Dtor.\n"; } }; int main(void) { /* # OUTPUT # ------------------ I am empty!.. ************* I am empty!.. ************* I am empty!.. ************* I am empty!.. ------------------ no_name ************* Yuruk ************* I have an value... : ulya I have an value... : ulya yuruk ************* ulya yuruk ************* ulya yuruk ------------------ ************* bad optional access */ puts("------------------"); { // Default Ctor.: { std::optional op; std::cout << std::boolalpha << (op.has_value() ? "I have an value..." : "I am empty!..") << "\n"; } puts("*************"); { std::optional op( std::nullopt ); std::cout << std::boolalpha << (op.has_value() ? "I have an value..." : "I am empty!..") << "\n"; } puts("*************"); { std::optional op{}; std::cout << std::boolalpha << (op.has_value() ? "I have an value..." : "I am empty!..") << "\n"; } puts("*************"); { std::optional op{ std::nullopt }; std::cout << std::boolalpha << (op.has_value() ? "I have an value..." : "I am empty!..") << "\n"; } } puts("------------------"); { // Assiging / Getting Value: { /* * ".value_or" fonksiyonu değer döndürmektedir. İlgili * nesnenin içi boş ise argüman olan ifade, aksi halde * nesnenin tuttuğu değer "get" edilmektedir. */ std::optional op; std::cout << op.value_or("no_name") << "\n"; } puts("*************"); { std::optional op("Yuruk"); std::cout << op.value_or("Mustafa") << "\n"; } puts("*************"); { // ".value" fonksiyonu ise referans döndürmektedir. std::optional op("ulya"); std::cout << std::boolalpha << (op.has_value() ? "I have an value..." : "I am empty!..") << " : " << *op << "\n"; *op += " yuruk"; std::cout << std::boolalpha << (op.has_value() ? "I have an value..." : "I am empty!..") << " : " << op.value() << "\n"; } puts("*************"); { // Cannot hold referance itself, but can hold ReferenceWrapper: std::string name{"ulya"}; // std::optional op{ name }; std::optional> op( std::ref(name) ); op->get() += " yuruk"; std::cout << name << "\n"; } puts("*************"); { // CTAT std::string name{"ulya"}; std::optional op = std::ref(name); op->get() += " yuruk"; std::cout << name << "\n"; } } puts("------------------"); { // Throwing an exception { std::optional op; try{ std::cout << *op << "\n"; } catch(const std::exception& ex){ std::cout << ex.what() << "\n"; } catch(...){ std::cout << "<<>>\n"; } } puts("*************"); { std::optional op; try{ std::cout << op.value() << "\n"; } catch(const std::exception& ex){ std::cout << ex.what() << "\n"; } catch(...){ std::cout << "<<>>\n"; } } } return 0; } * Örnek 2, #include #include #include #include class Myclass{ public: Myclass() { std::cout << "Default Ctor.\n"; } Myclass(int a, int b){ std::cout << "Param Ctor.: "<< a << ", " << b << "\n"; } Myclass(const Myclass&) { std::cout << "Copy Ctor.\n"; } Myclass(Myclass&&) { std::cout << "Move Ctor.\n"; } ~Myclass() { std::cout << "Dtor.\n"; } }; int main(void) { /* # OUTPUT # ------------------ ************* Default Ctor. Move Ctor. Dtor. Dtor. ************* Default Ctor. Dtor. ************* Param Ctor.: 13, 31 Dtor. ************* Param Ctor.: 14, 41 Dtor. ************* Param Ctor.: 15, 51 Dtor. ************* AAAAA AAAAA ************* aaaaa aaaaa */ puts("------------------"); { // "std::in_place" forwards perfectly. { std::optional op; } puts("*************"); { std::optional op{ Myclass{} }; } puts("*************"); { std::optional op{ std::in_place }; } puts("*************"); { std::optional op{std::in_place, 13, 31}; } puts("*************"); { auto op { std::make_optional(14, 41) }; } puts("*************"); { auto op = std::optional(std::in_place, 15, 51); } puts("*************"); { std::optional op_i(std::in_place, 5, 'A'); std::cout << *op_i << "\n"; std::optional> op_ii(std::in_place, { 'A', 'A', 'A', 'A', 'A' }); for(auto c : *op_ii) std::cout << c; std::cout << "\n"; } puts("*************"); { auto op_i{ std::make_optional(5, 'a') }; std::cout << *op_i << "\n"; auto op_ii{ std::make_optional>({ 'a', 'a', 'a', 'a', 'a' }) }; for(auto c : *op_ii) std::cout << c; std::cout << "\n"; } } return 0; } * Örnek 3, #include #include #include #include class Myclass{ public: Myclass() { std::cout << "Default Ctor.\n"; } Myclass(int a, int b){ std::cout << "Param Ctor.: "<< a << ", " << b << "\n"; } Myclass(const Myclass&) { std::cout << "Copy Ctor.\n"; } Myclass(Myclass&&) { std::cout << "Move Ctor.\n"; } ~Myclass() { std::cout << "Dtor.\n"; } }; int main(void) { /* # OUTPUT # ------------------ ************* Default Ctor. Move Ctor. true Dtor. false Dtor. ************* ------------------ ************* Param Ctor.: 0, 1 Dtor. Param Ctor.: 1, 2 Dtor. Param Ctor.: 2, 3 Dtor. ************* false true true ************* false false false ************* ------------------ */ puts("------------------"); { puts("*************"); { // "Dtor." called after ".reset()" call. Myclass mx; std::optional op{ std::move(mx) }; std::cout << std::boolalpha << op.has_value() << "\n"; op.reset(); // op = nullopt; // op = std::optional{}; // op = {}; std::cout << std::boolalpha << op.has_value() << "\n"; } puts("*************"); } puts("------------------"); { puts("*************"); { // ".emplace()" destroy the previously object and creates a new one. std::optional op; for(int i = 0; i < 3; ++i) op.emplace(i, i + 1); } puts("*************"); { std::optional x = 45; std::optional y = 54; std::cout << std::boolalpha << (x == y) << "\n"; std::cout << std::boolalpha << (x < y) << "\n"; using namespace std::string_literals; std::optional z{ "UlyaYuruk"s }; std::optional q; // "nullopt" olan en küçük olacaktır. std::cout << std::boolalpha << (q < z) << "\n"; } puts("*************"); { std::optional a; std::optional b{ true }; std::optional c{ false }; std::cout << std::boolalpha << (a == b) << "\n"; std::cout << std::boolalpha << (b == c) << "\n"; std::cout << std::boolalpha << (c == a) << "\n"; } puts("*************"); } puts("------------------"); return 0; } * Örnek 4, #include #include #include #include class Myclass{ public: Myclass() { std::cout << "Default Ctor.\n"; } Myclass(int a, int b){ std::cout << "Param Ctor.: "<< a << ", " << b << "\n"; } Myclass(const Myclass&) { std::cout << "Copy Ctor.\n"; } Myclass(Myclass&&) { std::cout << "Move Ctor.\n"; } ~Myclass() { std::cout << "Dtor.\n"; } }; int main(void) { /* # OUTPUT # ------------------ ************* x : 32 ************* ------------------ ************* x : 32 ************* ------------------ */ puts("------------------"); { puts("*************"); { int x = 31; std::optional op{&x}; if(op && *op) ++**op; // ++x; std::cout << "x : " << x << "\n"; } puts("*************"); } puts("------------------"); { puts("*************"); { int x = 31; auto op = std::make_optional>(&x); if(op && *op && **op && ***op) ++***op; std::cout << "x : " << x << "\n"; } puts("*************"); } puts("------------------"); return 0; } * Örnek 5, #include #include #include #include std::optional get_person_nick(int id) { if (id < 100) { return "Ulya Yuruk"; } return std::nullopt; // return {}; // std::optional{}; } int main() { auto op{ get_person_nick(31) }; std::cout << *op << "\n"; if ((op = get_person_nick(101)) == std::nullopt){ std::cout << "No Nick Name\n"; } if (auto op = get_person_nick(13); op) { std::cout << *op << "\n"; } if (auto op = get_person_nick(99); op->length() < 35){ std::cout << *op << "\n"; } } * Örnek 6, #include #include #include #include template auto find_if(Con&& c, Pred&& pred) { using std::begin, std::end; auto beg_iter = begin(c), end_iter = end(c); auto result = std::find_if(beg_iter, end_iter, pred); using iterator = decltype(result); if (result == end_iter) return std::optional(); /* Fonksiyonun geri dönüş değeri "auto" olduğu için "{}" kullanamayız. */ return std::optional(result); } template auto find(Con&& c, const T& t_val) { return find_if( std::forward(c), [&t_val](auto&& x) { return x == t_val; } ); } int main() { auto my_vec = std::vector(15); for (size_t i = 0; i < my_vec.size(); i++) { my_vec.at(i) = i; } if (auto op = find(my_vec, 30); op) { std::cout << "Found: " << **op << "\n"; } } * Örnek 7, #include #include #include #include #include std::optional to_int(const std::string& s) { try { return std::stoi(s); } catch (...) { return std::nullopt; // return {}; } } std::optional to_int2(const std::string& s) { std::optional ret; try { ret = std::stoi(s); } catch (...) { } return ret; } int main() { for (auto s : { "42", "077", "ulya", "0x33" }) { std::optional op = to_int(s); if (op) { std::cout << s << " is turned into an int: " << *op << "\n"; } else { std::cout << "(" << s << ") could not be turned into an int!..\n"; } } } * Örnek 8, #include #include int main() { /* # OUTPUT # Size: 10 Size: 10 true true ************ Size: 10 Size: 0 true true */ { std::optional op{"Ulya Yuruk"}; std::cout << "Size: " << op->length() << "\n"; auto opp = op; std::cout << "Size: " << op->length() << "\n"; std::cout << std::boolalpha << op.has_value() << "\n"; std::cout << std::boolalpha << opp.has_value() << "\n"; } puts("************"); { // "op1" içerisinde hala bir "std::string" var fakat o artık "Moved From State". std::optional op{ "Ulya Yuruk" }; std::cout << "Size: " << op->length() << "\n"; auto opp = std::move(op); std::cout << "Size: " << op->length() << "\n"; std::cout << std::boolalpha << op.has_value() << "\n"; std::cout << std::boolalpha << opp.has_value() << "\n"; } } * Örnek 9, #include #include #include struct Nec { std::optional mx; std::optional my; }; struct Erg { bool has_mx; bool has_my; double mx; double my; }; int main() { /* # OUTPUT # sizeof Nec: 32 sizeof Erg: 24 */ /* Burada "std::optional", "Alligned Storage" kullandığı içindir. */ std::cout << "sizeof Nec: " << sizeof(Nec) << "\n"; std::cout << "sizeof Erg: " << sizeof(Erg) << "\n"; } > "std::variant" : C dilindeki "union" yapısının C++ diline uyarlanmış halidir. Tıpkı "std::optional" gibi "std::variant" da bir "Value Type" dır. Şablon parametresi olan türlerden birisini tutmaktadır. Buradaki şablon parametreleri aynı tür olabilir fakat bir "t" anında bunlardan sadece bir tanesini tutmaktadır. Doğrudan "nullable" tür değildir, fakat bir takım yöntemler ile "nullable" hale getirebiliriz. Kendi bünyesinde tuttuğu nesne için dinamik bellek yönetimi uygulamamaktadır. Fakat o nesne kendi bünyesinde dinamik bellek yönetimi uygulayabilir. Son olarak kalıtıma alternatif olarak da kullanabiliriz. * Örnek 1, #include #include #include class Myclass { public: Myclass(int) { /* "Default Ctor." is not declared!.. */ } }; int main() { { /* * 1-) "Default Init." yapılan "std::variant" nesnesi * daima ilk alternatifi tutacaktır. Bu durumda * ilgili nesnemiz bünyesinde "int" tutmaktadır. */ std::variant va; } { /* * 2-) Eğer ilk alternatif "Default Init." edilemezse, * sentaks hatası oluşacaktır. */ std::variant va; } { /* * 3-) Böylesi durumlar için ilk alternatifi "std::monostate" * türü yapabiliriz. */ std::variant va; } } * Örnek 2, #include #include #include class Myclass { public: Myclass(int) { /* "Default Ctor." is not declared!.. */ } }; int main() { { /* * 1-) İlk değer verdiğimiz ifadenin türü ya alternatif * türlerden birisi ya da o türlere dönüştürülebilir bir * tür olmalıdır. */ std::variant va{ 12 }; std::variant vaa{ 1.2 }; std::variant vaaa{ 'a'}; } { /* * 2-) Fakat bu durumda "ambiguity" meydana gelebilir. */ std::variant va{ 3.4 }; } { /* * 3-) Diğer yandan, tıpkı "Function Overload Resolution" sırasında * gerçekleştiği gibi; "float" türünden "double" türüne dönüşüm en * yüksek öneme sahip olduğundan, ilk değer olarak "double" tutulacaktır. */ std::variant va{ 1.2f }; } } * Örnek 3, #include #include #include void what_is_holded(const std::variant& va); int main() { { /* * 1-) Tabii bünyesinde hangi türün tuttuğunu öğrenmek için * ".index()" fonksiyonunu çağırmalıyız. */ std::variant va{}; std::cout << "Type: " << va.index() << "\n"; va = 12; std::cout << "Type: " << va.index() << "\n"; va = 1.2; std::cout << "Type: " << va.index() << "\n"; va = 12; std::cout << "Type: " << va.index() << "\n"; va = 'a'; std::cout << "Type: " << va.index() << "\n"; } { /* * 2-) Fakat C++20 öncesinde eğer alternatifler "bool" ve "std::string" * ise "bool" olanın indeksi ilgili fonksiyon tarafından geri döndürülmektedir. * Şüphesiz "vx" nesnesine "ulya" yerine "ulya"s geçilseydi, direkt olarak * "std::string" olanın indis bilgisini elde edecektik. */ std::variant vx("ulya"); std::cout << "Index : " << vx.index() << "\n"; } { /* * 3-) Bir diğer alternatif fonksiyon ise "hold_alternatif()" * fonksiyonudur. */ std::variant va{}; what_is_holded(va); va = 12; what_is_holded(va); va = 1.2; what_is_holded(va); } } void what_is_holded(const std::variant& va) { if (std::holds_alternative(va)) { std::cout << std::boolalpha << "A char is being holded!..\n"; } else if (std::holds_alternative(va)) { std::cout << std::boolalpha << "An int is being holded!..\n"; } else { std::cout << std::boolalpha << "A double is being holded!..\n"; } } * Örnek 4, #include #include #include int main() { { /* * 1-) İlk değer verirken parametreleri "perfect forward" etmek istiyorsak * "std::in_place_index" nesnesini kullanmalıyız. */ std::variant va{ std::in_place_index<2>, 10, 'a'}; } { /* * 2-) Bunun bir diğer alternatifi ise "std::in_place_type" nesnesini kullanmaktır. */ std::variant va{ std::in_place_type, 10, 'a'}; } { /* * 3-) Bu iki nesnesi kullanarak da "ambiguity" hatasını giderebiliriz. */ std::variant va{ 34u }; // Ambiguity std::variant vaa{ std::in_place_index<2>, 34u }; // OK std::variant vaa{ std::in_place_type, 34u }; // OK std::variant vb{ 12 }; // Ambiguity std::variant vb{ std::in_place_index<1>, 12 }; // OK } } * Örnek 5, #include #include #include class Myclass { public: Myclass() { std::cout << "Default Ctor.\n"; } Myclass(const Myclass&) { std::cout << "Copy Ctor.\n"; } Myclass(Myclass&&) { std::cout << "Move Ctor.\n"; } }; int main() { { std::variant vaa; std::variant vaaa{ std::in_place_index<1> }; // Default Ctor. std::variant va; // Default Ctor. std::variant vb{ Myclass{} }; // Default Ctor. & Move Ctor. std::variant vbb{ std::in_place_index<1> }; // Default Ctor. std::variant vbbb{ std::in_place_type }; // Default Ctor. } } * Örnek 6, #include #include #include struct Nec { int x, y; }; struct Erg { double x, y; }; struct Buffer { unsigned char len[256]; }; using var_type = std::variant; using var_type_custom = std::variant; int main() { { /* # OUTPUT # Sizeof Nec : 8 Sizeof Erg : 16 Sizeof Buffer : 256 Sizeof var_type : 16 Sizeof var_type_custom : 264 */ std::cout << "Sizeof Nec : " << sizeof(Nec) << "\n"; std::cout << "Sizeof Erg : " << sizeof(Erg) << "\n"; std::cout << "Sizeof Buffer : " << sizeof(Buffer) << "\n"; std::cout << "Sizeof var_type : " << sizeof(var_type) << "\n"; std::cout << "Sizeof var_type_custom : " << sizeof(var_type_custom) << "\n"; } } * Örnek 7, #include #include #include void what_to_have(const std::variant& va); int main() { { /* * 1-) "std::get()" fonksiyonu ile "std::variant" * içerisinde tutulan değere erişebiliriz. */ std::variant vx{ 4.5 }; std::cout << std::get<1>(vx) << "\n"; } { /* * 2-) Fakat bu fonksiyona geçilen indeks bilgisi * eğer "std::variant" nesnesinin tuttuğu değere * ilişkin değilse, bir "exception" gönderilecektir. */ std::variant vx{ 4.5 }; try { std::cout << std::get<2>(vx) << "\n"; } catch (const std::exception& ex) { std::cout << ex.what() << "\n"; } } { /* * 3-) Fakat bu fonksiyona geçilen indeks bilgisi * geçersiz bir indeks ise sentaks hatası oluşacaktır. */ std::variant vx{ 4.5 }; // std::cout << std::get<4>(vx) << "\n"; // ERROR } { /* * 4-) Bu fonksiyon, "std::variant" içerisindeki nesneye * referans döndürmektedir. */ std::variant vx{ 4.5 }; std::cout << std::get<1>(vx) << "\n"; std::get<1>(vx) = 5.4; std::cout << std::get<1>(vx) << "\n"; } { /* * 5-) "std::get()" fonksiyonunun alternatifi ise "std::get_if()" * fonksiyonudur. Bu fonksiyon "exception" fırlatmıyor ve "pointer" * semantiğiyle birlikte kullanmalıyız. Eğer geçersiz bir indeks * girersek de "nullptr" değerini döndürmektedir. */ std::variant vx{ "Ulya" }; what_to_have(vx); vx = 1.2; what_to_have(vx); vx = 12; what_to_have(vx); } } void what_to_have(const std::variant& va) { if (va.index() == 0) { std::cout << "Value: " << std::get<0>(va) << "\n"; } if (std::holds_alternative(va)) { std::cout << "Value: " << std::get<1>(va) << "\n"; } if (auto ptr{std::get_if(&va)}; ptr) { std::cout << "Value: " << *ptr << "\n"; } } * Örnek 8, #include #include #include int main() { { /* * 1-) Okumayı kolaylaştırmak adına bir takım "using" * bildirimleri de oluşturulmaktadır. */ enum index : size_t { age, wage, name }; using Age = int; using Wage = double; using Name = std::string; std::variant vx(45); std::cout << "Age : " << std::get(vx) << "\n"; vx = 1'000'000.00'987; std::cout << "Wage: " << std::get(vx) << "\n"; vx = "Ulya Yuruk"; std::cout << "Name: " << std::get(vx) << "\n"; } } * Örnek 9, #include #include #include class Myclass { public: Myclass(int a, double d) { std::cout << "a : " << a << ", d : " << d << "\n"; } void print(void) const { std::cout << "Myclass::print()\n"; } ~Myclass() { std::cout << "Dtor.\n"; } }; int main() { { /* * 1-) "std::variant" nesnesine değer atamanın bir diğer * alternatifi ise ".emplace()" fonksiyonunu çağırmaktır. * Bu fonksiyon da bir şablon olduğundan, şablon parametresini * belirtmeliyiz. Bu fonksiyon "Perfect Forwarding" yapabildiği * gibi önceki değeri de silmektedir. */ std::variant vx; vx.emplace(Myclass{5, 5.f}); std::get(vx).print(); vx.emplace(1.2); std::cout << std::get(vx) << "\n"; vx.emplace(12); std::cout << std::get(vx) << "\n"; } } * Örnek 10, #include #include #include struct A { A(int) { //... } }; struct B { B(int) { } }; int main() { { /* * 1-) Eğer "std::variant" nesnesinin alternatifleri * "Default Init." yapılamıyorsa sentaks hatası alacağız. * Eğer alternatifler arasında "std::monostate" sınıfını * kullanırsak hatayı gidermiş olacağız. Her ne kadar bu * sınıfın ilk alternatif olması bir zorunluluk değilse de, * tipik kullanımda ilk alternatiftir. */ // std::variant va; // ERROR // std::variant vb; // ERROR std::variant vc; // OK } { /* * 2-) "std::monostate" sınıfının bir diğer kullanım yeri * ise "std::variant" sınıfını "nullable" hale getirmektir. */ std::variant vx; if (vx.index() == 0) { std::cout << "Mono State\n"; } if (!vx.index()) { std::cout << "Mono State\n"; } if (std::holds_alternative(vx)) { std::cout << "Mono State\n"; } if (std::get_if(&vx)) { std::cout << "Mono State\n"; } vx = "Ulya Yuruk"; std::cout << std::get<3>(vx) << "\n"; vx = 1.2; std::cout << std::get<2>(vx) << "\n"; vx = 12; std::cout << std::get<1>(vx) << "\n"; vx = std::monostate{}; // vx = {}; // vx.emplace(); // vx.emplace<0>(); if (vx.index() == 0) { std::cout << "Mono State\n"; } } } * Örnek 11, #include #include #include #include struct PrintVisitor { void operator()(int x)const { std::cout << "int : " << x << "\n"; } void operator()(double x)const { std::cout << "double: " << x << "\n"; } void operator()(const std::string& x)const { std::cout << "string: " << x << "\n"; } }; struct PrintVisitorTemp { // Alternative Way - I template void operator()(T x)const { std::cout << "[" << x << "]\n"; } template<> void operator()(const std::string& x) const { std::cout << "[" << x << "]\n"; } // Alternative Way - II, Since C++20 /* void operator()(const auto& x)const{ std::cout << "[" << x << "]\n"; } */ // Alternative Way - III /* template void operator()(const T& x) { if constexpr (std::is_same_v) { std::cout << "int, [" << x << "]\n"; } else if constexpr (std::is_same_v) { std::cout << "double, [" << x << "]\n"; } else if constexpr (std::is_same_v) { std::cout << "string, [" << x << "]\n"; } } */ }; struct IncVisitor { template void operator()(T& x) { ++x; } template<> void operator()(std::string& x) { x += x; } }; class Myclass {}; struct MyCustomCallable { void operator()(char)const { std::cout << "char\n"; } void operator()(int)const { std::cout << "int\n"; } void operator()(double)const { std::cout << "double\n"; } void operator()(Myclass)const { std::cout << "Myclass\n"; } void operator()(auto)const { std::cout << "Others\n"; } }; int main() { { /* * 1-) Şimdi de "std::visit" fonksiyonunu inceleyelim. Bu fonksiyon * bir "callable" alıyor ve her bir alternatif için bu "callable" ı * çağırıyor. Dolayısıyla ilgili "callable" nesnesi içerisinde, * alternatiflerin türleri parametre olarak alan fonksiyonlar olması * gerekmektedir. Aksi halde sentaks hatası alacağız. */ std::variant vx("Ulya Yuruk"); std::visit(PrintVisitor{}, vx); vx = 1.2; PrintVisitor pv; std::visit(pv, vx); vx = 12; std::visit(PrintVisitor{}, vx); } puts("#########################"); { /* * 2-) Eğer bu fonksiyonların yaptıkları şey de aynı ise ilgili * fonksiyonu şablon olarak da yazabiliriz. */ std::variant vx("Ulya Yuruk"); std::visit(PrintVisitorTemp{}, vx); vx = 1.2; PrintVisitorTemp pv; std::visit(pv, vx); vx = 12; std::visit(PrintVisitorTemp{}, vx); } puts("#########################"); { /* * 3-) Tabii bu fonksiyona her defasında farklı bir işi yapan * "callable" da geçilebilir. */ std::variant vx("Ulya Yuruk"); std::visit(PrintVisitorTemp{}, vx); std::visit(IncVisitor{}, vx); std::visit(PrintVisitorTemp{}, vx); vx = 1.2; PrintVisitor pv; IncVisitor ic; std::visit(pv, vx); std::visit(ic, vx); std::visit(pv, vx); vx = 12; std::visit(PrintVisitorTemp{}, vx); std::visit(IncVisitor{}, vx); std::visit(PrintVisitorTemp{}, vx); } puts("#########################"); { /* * 4-) Bize bir "callable" lazım olduğundan, "lambda" ifadelerini * de kullanabiliriz. Fakat burada bizim "f" ismini kullanmamıza * gerek yoktur. İlgili "lambda" ifadesini fonksiyona direkt olarak * da gönderebiliriz. */ auto f = [](const auto& x) { std::cout << "(" << x << ")\n"; }; std::variant vx("Ulya Yuruk"); std::visit(f, vx); vx = 1.2; std::visit(f, vx); vx = 12; std::visit(f, vx); } puts("#########################"); { /* * 5-) Pek tabii iş bu "callable" sınıfını istediğimiz şekilde organize * edebiliriz. */ std::variant> vx; std::visit(MyCustomCallable{}, vx); vx = 12; std::visit(MyCustomCallable{}, vx); vx = 1.2; std::visit(MyCustomCallable{}, vx); vx = Myclass{}; std::visit(MyCustomCallable{}, vx); vx = std::bitset<16>(56u); std::visit(MyCustomCallable{}, vx); } } > Hatırlatıcı Notlar: >> "Value Type" : Her nesnenin değeri kendine demektir. Yani ortada paylaşılan bir alan/değer söz konusu değildir. Yani kopyalama yapıldığında "Deep Copy" yapılmakta, "Shallow Copy" yapılmamaktadır. /*================================================================================================================================*/ (09_23_07_2023) > "std::variant" (devam): * Örnek 1, #include #include #include #include struct MyVisitor{ /* // Alternative - I template void operator()(const T& t, const U& u){ std::cout << typeid(T).name() << ", " << typeid(U).name() << " => "; std::cout << "(" << t << ", " << u << ")\n"; } */ /* // Alternative - II (C++20) void operator()(const auto& t, const auto& u){ std::cout << typeid(t).name() << ", " << typeid(u).name() << " => "; std::cout << "(" << t << ", " << u << ")\n"; } */ // Alternative - III void operator()(int t, int u){ std::cout << typeid(t).name() << ", " << typeid(u).name() << " => "; std::cout << "(" << t << ", " << u << ")\n"; } void operator()(const auto& t, const auto& u){ std::cout << "Other types!..\n"; } }; int main(void) { { std::variant vx{3.4}; std::variant vy{34}; std::visit(MyVisitor{}, vx, vy); vx = "Ulya Yuruk"; vy = 4.3f; std::visit(MyVisitor{}, vx, vy); } { auto fn = [](const auto& t, const auto& u){ std::cout << typeid(t).name() << ", " << typeid(u).name() << " => "; std::cout << "(" << t << ", " << u << ")\n"; }; std::variant vx{3.4}; std::variant vy{34}; std::visit(fn, vx, vy); vx = "Ulya Yuruk"; vy = 4.3f; std::visit(fn, vx, vy); } return 0; } * Örnek 2, #include #include #include #include class Nec : public std::variant{ //... }; int main(void) { Nec nec{ "Ulya Yuruk" }; std::cout << "Index : " << nec.index() << ", "; std::cout << std::get<1>(nec) << "\n"; nec.emplace<0>(31); std::cout << "Index : " << nec.index() << ", "; std::cout << std::get<0>(nec) << "\n"; return 0; } * Örnek 3, Aşağıdaki örnekte ise ilgili "variant" nesnesinin alternatif eklenirken bir hata gönderilmiştir. Dolayısıyla ilgili alternatif oluşturulamamıştır. İşte bu durumu tespit eden ise ".valueless_by_exception()" isimli fonksiyondur. Bu durumda artık ".index()" fonksiyonu ise artık "std::variant_npos" konumunu döndürecektir. #include #include #include struct S { operator int() const{ /* * 2-) Sonrasında bu fonksiyon çağrılacaktır fakat programın akışı * "return" deyimine girmeyecektir. Dolayısıyla ilgili alternatif * OLUŞTURULMAMIŞ OLACAKTIR. */ throw std::runtime_error{ "hata"}; return 1; } }; int main() { using namespace std; variant var{ 12.2 }; try { /* * 1-) "int" yerine yeni bir alternatif eklenmek istenmiştir. */ var.emplace<1>(S{}); } catch (const exception& ex) { /* * 3-) Bu durumda "std::variant" nesnesi geçersiz durumda olacaktır. */ cout << "hata yakalandi: " << ex.what() << "\n"; cout << boolalpha << var.valueless_by_exception() << "\n"; cout << "Index: " << var.index() << "\n"; cout << (var.index() == variant_npos) << "\n"; } } * Örnek 4, #include #include #include using v_type = std::variant; int main() { { constexpr auto n{ std::variant_size::value }; // constexpr auto n{ std::variant_size_v }; std::cout << "Total of <" << n << "> different types in the variant object\n"; } { std::variant_alternative<0, v_type>::type i; // int i; // std::variant_alternative_t<0, v_type> i; // int i; std::variant_alternative<1, v_type>::type d; // double d; // std::variant_alternative_t<1, v_type> d; // double d; std::variant_alternative<2, v_type>::type l; // long l; std::variant_alternative<3, v_type>::type c; // char c; std::variant_alternative<4, v_type>::type s; // std::string s; } } * Örnek 5, #include #include struct Data { //... }; enum ErrorType { System, Archieve, Log }; std::variant foo() { /* * Bir hata durumunda ilgili "std::variant" * nesnesi "ErrorType" türünü tutarken, hata * olmadığında ise "Data" türünü tutacaktır. */ //... } int main() { //... } * Örnek 6, İşin başında toplamda kaç adet türemiş sınıf olacağı belliyse ve daha sonra ekleme yapılmayacağı kesinse, "std::variant" sınıfını kalıtıma alternatif olarak da kullanabiliriz. Böylesi durumlara ise "Closed Inheritence" denir. #include #include class Xls {}; class Pdf {}; class Txt {}; class Word {}; using Document = std::variant; int main() { //... } > "1. Cpp Idioms/Patterns > Overloader Idiom" : Aşağıdaki örnekleri inceleyelim: * Örnek 1, Bu örnekte "CTAT" ve "Deduction Guide" arasındaki ilişkiye değinilmiştir. #include template struct Pair{ Pair(const T& t, const U& u) : m_first{t}, m_second{u} {} /* // Alternative - I Pair(T t, U u) : m_first{t}, m_second{u} {} */ private: T m_first; U m_second; }; // Alternative - II /* * Bu "Deduction Guide" a göre "T" ve "U" * türleri, "array-decay" mekanizmasından * dolayı birer gösterici olacaktır. Dolayısıyla * "t" ve "u" değişkenlerinin türleri ise "int*(&)" * ve "double*(&)" biçiminde olacaktır... */ template Pair(T, U)->Pair; /* * ..."Deduction Guide" için şablonlara da gerek yoktur. * Örneğin, "Pair" sınıfının "Ctor." fonksiyonuna "const char*" * türden parametre gönderilirse, tür çıkarımı "std::string" * türüne göre yapılacaktır. */ Pair(const char*, const char*)->Pair; int main(void) { int a[10]{}; double d[20]{}; /* * Aşağıdaki değişkene ilk değer verirken şablon * parametreleri şu türlere denk gelecektir: * T : int[10] * U : double[20] * Dolayısıyla "ctor." fonksiyon parametreleri * ise şu türlerden olacaktır: * t : int(&)[10] * u : double(&)[20] * Fakat sınıfın veri elemanları ise aşağıdaki * biçimde olacaktır: * int m_first[10]; * double m_second[20]; * İşte burada şu meydana gelmektedir: Sınıfın * veri elemanları birer dizi. "Ctor." fonksiyonuna * argümanları ise diziye-referans. Dilin kuralları * gereği iki dizi birbirine atanamaz olduğundan, * SENTAKS HATASI MEYDANA GELECEKTİR. Burada sentaks * hatasını iki şekilde giderebiliriz: * 1-) Eğer sınıfın "Ctor." fonksiyonunun parametreleri * "const T&" yerine direkt olarak "T" olsaydı, "array-decay" * gerçekleşecek ve tür çıkarımları "pointer" olarak * yapılacaktır. Dolayısıyla hem "T" hem de "t" bir * gösterici olduğundan birbirine atanabilir durumdadır. * Fakat bu durum kopyalama maliyetini de beraberinde * getirecektir!!! * 2-) "Deduction Guide" mekanizmasından faydalanmaktır. */ Pair p(a, d); return 0; } * Örnek 2, Aşağıdaki örnekte ise "Overloader" deyimine dair bir örnek verilmiştir. #include #include #include template struct Overload : Args...{ using Args::operator()...; }; // Until C++20 template Overload(Args...) -> Overload; int main() { std::variant vx{ "ulya yuruk" }; std::visit( Overload{ [](int) { std::cout << "int\n"; }, [](double) { std::cout << "double\n"; }, [](long) { std::cout << "long\n"; }, [](std::string) { std::cout << "string\n"; }, [](auto) { std::cout << "other types\n"; } }, vx ); } * Örnek 3, #include #include #include template struct overload : Ts... { using Ts::operator()...; }; // Until C++20 template overload(Ts...) -> overload; int main() { using namespace std; variant vx(99); visit(overload{ [](int ival) {cout << "int: " << ival << "\n"; }, [](const string& s) {cout << "string: " << s << "\n"; } }, vx ); auto twice = overload{ [](std::string& s) { s += s; }, [](auto& i) {i *= 2; } }; visit(twice, vx); cout << get<0>(vx) << "\n"; } * Örnek 4, #include #include #include template struct overload : Ts... { using Ts::operator()...; }; // Until C++20 template overload(Ts...) -> overload; int main() { using namespace std; variant vx(99); variant vy(9.9f); visit( overload{ [](int, int) { cout << "int, int\n"; }, [](string, const char*) { cout << "string, const char*\n"; }, [](float, double) { cout << "float, double"; }, [](auto, auto) { cout << "other types\n"; } }, vx, vy ); } * Örnek 5, #include #include #include #include #include class Dog { public: Dog(const std::string& name) : m_name{ name } {} void Bark(void) const { std::cout << m_name << " is barking!..\n"; } private: std::string m_name; }; class Cat { public: Cat(const std::string& name) : m_name{ name } {} void Meow(void) const { std::cout << m_name << " meows!..\n"; } private: std::string m_name; }; class Lamb { public: Lamb(const std::string& name) : m_name{ name } {} void Bleat(void) const { std::cout << m_name << " is bleating!..\n"; } private: std::string m_name; }; ////////////////////////////////////////////////////// using Animal = std::variant; template bool is_type(const Animal& a) { return std::holds_alternative(a); } struct AnimalVoice { void operator()(const Dog& dog) const { dog.Bark(); }; void operator()(const Cat& dog) const { dog.Meow(); }; void operator()(const Lamb& dog) const { dog.Bleat(); }; }; int main() { /* # OUTPUT # ----------------------- pamuk meows!.. kont is barking!.. kuzucuk is bleating!.. pamuk is bleating!.. tekir meows!.. ----------------------- pamuk meows!.. kont is barking!.. kuzucuk is bleating!.. pamuk is bleating!.. tekir meows!.. ----------------------- pamuk meows!.. kont is barking!.. kuzucuk is bleating!.. pamuk is bleating!.. tekir meows!.. ----------------------- In farm, we have: 1 dogs, 2 cats, 2 lambs. */ std::list animal_farm{ Cat{"pamuk"}, Dog{"kont"}, Lamb{"kuzucuk"}, Lamb{"pamuk"}, Cat{"tekir"} }; puts("-----------------------"); for (const auto& a : animal_farm) { switch (a.index()) { case 0: std::get(a).Bark(); break; case 1: std::get(a).Meow(); break; case 2: std::get(a).Bleat(); break; } } puts("-----------------------"); for (const auto& a : animal_farm) { if (const auto ptr = std::get_if(&a)) { ptr->Bark(); } if (const auto ptr = std::get_if(&a)) { ptr->Meow(); } if (const auto ptr = std::get_if(&a)) { ptr->Bleat(); } } puts("-----------------------"); for (const auto& a : animal_farm) { std::visit(AnimalVoice{}, a); } puts("-----------------------"); std::cout << "In farm, we have: " << std::count_if(std::begin(animal_farm), std::end(animal_farm), is_type) << " dogs, " << std::count_if(std::begin(animal_farm), std::end(animal_farm), is_type) << " cats, " << std::count_if(std::begin(animal_farm), std::end(animal_farm), is_type) << " lambs.\n"; } > "std::any" : Sınıf şablonu DEĞİLDİR. "Default Init." edildiğinde bir değer TUTMAMAKTADIR, tıpkı "std::optional" gibi. * Örnek 1, #include #include class Myclass { public: Myclass() { std::cout << "Myclass\n"; } }; int main() { /* # OUTPUT # false false */ std::any x1; std::cout << std::boolalpha << x1.has_value() << "\n"; std::any x2{}; std::cout << std::boolalpha << x2.has_value() << "\n"; } * Örnek 2, #include #include #include #include int main() { using namespace std::string_literals; std::any a1(12); std::any a2(1.2); std::any a3("mustafa"); std::any a4("mustafa"s); std::any a5(std::vector{ 2, 5, 9, 12, 56 }); } * Örnek 3, "std::any" nesnesinin tuttuğu değişken yeteri kadar büyükse, o değişken için dinamik bellek yönetimi uygulanabilir. Bu durum derleyiciye bağlıdır. Anımsanacağı üzere "std::optional" ve "std::variant" için böyle bir dinamik bellek yönetimi söz konusu değildi. #include #include #include #include void* operator new(std::size_t sz) { std::cout << "operator new called, size: " << sz << "\n"; if (!sz) ++sz; if (void* ptr = std::malloc(sz)) return ptr; throw std::bad_alloc{}; } struct Nec { char buf[256]; }; struct Custom { char buf[5]{}; }; struct CustomLarge { char buf[50]{}; }; int main() { /* # OUTPUT # -------------------- operator new called, size: 4 operator new called, size: 256 -------------------- -------------------- operator new called, size: 50 -------------------- */ puts("--------------------"); { auto p = new int; // operator new called, size: 4 auto pp = new Nec; // operator new called, size: 256 } puts("--------------------"); { std::any a = Custom{}; } puts("--------------------"); { std::any a = CustomLarge{}; } puts("--------------------"); } * Örnek 4, #include #include int main() { std::any a; a = 45; a = 4.5; a = "ulya"; a = std::string{"yuruk"}; } * Örnek 5, #include #include #include int main() { /* # OUTPUT # void int double const char* std::string int* */ std::any a; std::cout << (a.type() == typeid(void) ? "void" : "ELSE") << "\n"; a = 45; std::cout << (a.type() == typeid(int) ? "int" : "ELSE") << "\n"; a = 4.5; std::cout << (a.type() == typeid(double) ? "double" : "ELSE") << "\n"; a = "ulya"; std::cout << (a.type() == typeid(const char*) ? "const char*" : "ELSE") << "\n"; a = std::string{"yuruk"}; std::cout << (a.type() == typeid(std::string) ? "std::string" : "ELSE") << "\n"; int arr[10]; a = arr; std::cout << (a.type() == typeid(int*) ? "int*" : "ELSE") << "\n"; } * Örnek 6, #include #include #include int main() { /* # OUTPUT # */ int arr[10] = { 10, 9, 8, 7, 6 }; std::any a = arr; std::cout << "&: " << std::any_cast(a) << ", *&: " << *std::any_cast(a) << "\n"; puts("---------------"); a = 3.4; std::cout << std::any_cast(a) << "\n"; puts("---------------"); a = 34; try { std::cout << std::any_cast(a) << "\n"; } catch (const std::bad_any_cast& ex /* const std::bad_cast& ex */) { std::cout << "Hata: " << ex.what() << "\n"; } puts("---------------"); a = 4.3; std::cout << std::any_cast(a) << "\n"; // std::any_cast(a) = 6.9; // ERROR : left operand of ('=') must be l-value. std::any_cast(a) = 6.9; std::cout << std::any_cast(a) << "\n"; puts("---------------"); } * Örnek 7, #include #include class Myclass { public: Myclass() = default; Myclass(int) { std::cout << "Myclass(int)\n"; } Myclass(int, int) { std::cout << "Myclass(int, int)\n"; } }; int main() { /* # OUTPUT # --------------- Myclass(int, int) --------------- Myclass(int, int) --------------- */ puts("---------------"); std::any a{ std::in_place_type, 3, 6 }; puts("---------------"); auto aa = std::make_any(6, 3); puts("---------------"); } * Örnek 8, #include #include #include class Myclass { public: Myclass() = default; Myclass(int) { std::cout << "Myclass(int)\n"; } Myclass(int, int) { std::cout << "Myclass(int, int)\n"; } }; int main() { /* # OUTPUT # --------------- true false --------------- Myclass(int) true --------------- */ puts("---------------"); auto a = std::make_any(5, 'u'); std::cout << std::boolalpha << a.has_value() << "\n"; a.reset(); std::cout << std::boolalpha << a.has_value() << "\n"; puts("---------------"); a.emplace(9); std::cout << std::boolalpha << a.has_value() << "\n"; puts("---------------"); } * Örnek 9, #include #include #include #include int main() { /* # OUTPUT # --------------- ulya fena --------------- Size: 0 Size: 4 --------------- */ using namespace std::string_literals; puts("---------------"); std::any a{ "ulya"s }; std::cout << std::any_cast(a) << '\n'; auto& ra = std::any_cast(a); ra[0] = 'f'; ra[1] = 'e'; ra[2] = 'n'; std::cout << std::any_cast(a) << '\n'; puts("---------------"); auto str = std::any_cast(std::move(a)); static_assert(std::is_same_v); std::cout << "Size: " << std::any_cast(&a)->size() << '\n'; std::cout << "Size: " << str.size() << '\n'; puts("---------------"); } * Örnek 10, #include #include #include #include #include int main() { /* # OUTPUT # -------------------- int int : 12 -------------------- -------------------- double double : 1.23 -------------------- -------------------- char const * const char* : ulya -------------------- -------------------- long long : 34 -------------------- -------------------- class std::basic_string,class std::allocator > std::string : yuruk -------------------- */ using namespace std::string_literals; std::vector avec{ 12, 1.23, "ulya", 34L, "yuruk"s }; for (const auto& i : avec) { puts("--------------------"); // Alternative - I std::cout << i.type().name() << '\n'; // Alternative - II if (auto ptr = std::any_cast(&i)) { std::cout << "int : " << *ptr << '\n'; } else if (auto ptr = std::any_cast(&i)) { std::cout << "double : " << *ptr << '\n'; } else if (auto ptr = std::any_cast(&i)) { std::cout << "const char* : " << *ptr << '\n'; } else if (auto ptr = std::any_cast(&i)) { std::cout << "long : " << *ptr << '\n'; } else if (auto ptr = std::any_cast(&i)) { std::cout << "std::string : " << *ptr << '\n'; } puts("--------------------"); } } * Örnek 11, #include #include #include #include #include #include using tv_pair = std::pair; int main() { /* # OUTPUT # name ulya yuruk year 1998 month 11 day 22 wage 87.67 town ordu gender female country Turkiye */ using namespace std::string_literals; std::vector vec; vec.emplace_back("name", "ulya yuruk"s); vec.emplace_back("year", 1998); vec.emplace_back("month", 11); vec.emplace_back("day", 22); vec.emplace_back("wage", 87.67); vec.emplace_back("town", "ordu"s); vec.emplace_back("gender", "female"s); vec.emplace_back("country", "Turkiye"s); std::cout << std::left; for (const auto& [property, value] : vec) { if (value.type() == typeid(int)) { std::cout << std::setw(16) << property << std::any_cast(value) << '\n'; } else if (value.type() == typeid(double)) { std::cout << std::setw(16) << property << std::any_cast(value) << '\n'; } else if (value.type() == typeid(std::string)) { std::cout << std::setw(16) << property << std::any_cast(value) << '\n'; } } } > Hatırlatıcı Notlar: >> "Multiple Inheritence" sırasında "Ambiguity" oluşmaması için: * Örnek 1, #include struct A{ void foo(int){ std::cout << "foo(int)\n"; } }; struct B{ void foo(double){ std::cout << "foo(double)\n"; } }; struct Der : A, B{ /* // Alternative - II using A::foo; using B::foo; */ }; int main(void) { /* * Burada "foo" ismi taban sınıflar içerisinde * aynı anda aranacağı için ikisinde de bulunacaktır. * Bu da "Ambiguity" e yol açacaktır. Bu problemi * gidermek için: * 1-) İlgili fonksiyonu çağırırken, ilgili sınıf ismini * niteleyerek çağırmak. * 2-) İlgili fonksiyon ismini taban sınıf içerisinde * "using" bildirimi ile görünür kılmak. */ Der myDer; // myDer.foo(1.2); // ERROR: ambiguous // Alternative - I myDer.A::foo(12); myDer.B::foo(1.2); /* // Alternative - II myDer.foo(12); myDer.foo(1.2); */ return 0; } * Örnek 2, #include template struct Der : Args ...{ // Alternative - I using Args::foo...; }; struct A{ void fA(){ std::cout << "A::fA()\n"; } void foo(float){ std::cout << "float\n"; } }; struct B{ void fB(){ std::cout << "B::fB()\n"; } void foo(int){ std::cout << "int\n"; } }; struct C{ void fC(){ std::cout << "C::fC()\n"; } void foo(double){ std::cout << "double\n"; } }; int main(void) { { Der x; x.fA(); Der y; y.fA(); y.fB(); Der z; z.fA(); z.fB(); z.fC(); } puts("********"); { Der myDer; /* * Aşağıdaki "foo" çağrısı da yine sentaks * hatasına yol açacaktır. Çözümler: * 1-) "Der" sınıfı içerisinde "using" * bildirimleri ile "foo" ismini görünür kılmak. * 2-) İlgili "foo" fonksiyonunu, sınıfının * ismini de niteleyerek çağırmak. */ myDer.foo(1.2); /* // Alternative - II myDer.C::foo(1.2); */ } return 0; } >> "Aggregate Init." : * Örnek 1, struct A{ A(int) {} }; struct B{ B(float) {} }; struct C{ C(double) {} }; struct Der : A, B, C{}; int main(void) { Der myDer = {11, 1.1f, 1.1}; // Aggregate Init. return 0; } >> C++20 ile "Stateless-Lambda-Expression" olan ifadelerin türlerini tür bildirimlerinde kullanamıyorduk çünkü bu türlerin "Default Ctor." ve "Copy Assignment" fonksiyonları "delete" edilmişlerdir. * Örnek 1, #include #include #include #include int main(void) { auto fn = [](int x){ return x+x; }; /* * İlgili "lambda expression" bir "stateless" * ise yani dışarıdan bir değişken yakalmıyorsa, * aşağıdaki biçimde kullanabiliriz. Tabii C++20 * ve sonrasında. Fakat öncesinde "Default Ctor." * ve "Copy Assignment" fonksiyonları "delete" * edildiği için sentaks hatası oluşacaktır. */ decltype(fn) fx; // Legal since C++20 /* * Aşağıdaki ise C++17 öncesinde de geçerlidir * çünkü burada "Copy Ctor." fonksiyonu çağrılır. * Aşağıdaki kullanım için ilgili "lambda" ifadesinin * "stateless" olup olmamasınnı bir önemi yoktur. */ decltype(fn) fy(fn); return 0; } * Örnek 2, Aşağıdaki örnekte "stateless-lamda" ifadeleri ile arka planda oluşturulan sınıfın "Default Ctor." ve "Copy Assignment" fonksiyonları C++20 öncesinde "delete" edildiği için, "Copy Ctor." çağrısı yapmak durumundaydık. #include #include int main(void) { auto fn = [](int i, int j){ return std::abs(i) < std::abs(j); }; // Until C++20: //std::set mySet(fn); // Since C++20: std::set mySet; return 0; } * Örnek 3, Aşağıdaki kod ise C++20 itibariyle geçerlidir çünkü bir "lambda" ifadesinin "Unevaluated Context" içerisinde bulunabilmesi C++20 ile mümkün kılınmıştır. Bu kural ile yukarıdaki kural farklıdır. #include #include int main(void) { std::set< int, decltype( [](int i, int j){ return std::abs(i) < std::abs(j); } ) > mySet; return 0; } >> "Lambda" ifadelerini kalıtım mekanizmasında kullanabiliriz: * Örnek 1, #include auto fn1 = [](int x){ return x+x; }; auto fn2 = [](float x){ return x+x+x; }; auto fn3 = [](double x){ return x+x+x+x; }; struct A : decltype(fn1), decltype(fn2), decltype(fn3){ using decltype(fn1)::operator(); using decltype(fn2)::operator(); using decltype(fn3)::operator(); }; int main(void) { /* # OUTPUT # i: 2 f: 3.3 d: 4.4 */ A a; int i = 1; std::cout << "i: " << a(i) << "\n"; float f = 1.1f; std::cout << "f: " << a(f) << "\n"; double d = 1.1; std::cout << "d: " << a(d) << "\n"; return 0; } * Örnek 2, #include #include template struct Der : Args...{ }; int main(void) { auto f1 = [](){}; auto f2 = [](){}; auto f3 = [](){}; Der myDer; return 0; } * Örnek 3, #include template struct Overload : Args...{ }; int main(void) { /* * Aşağıda "CTAT" mekanizmasından faydalanılmıştır. C++20 ile * artık sentaks hatası değildir fakat C++17 için "deduction guide" * mekanizmasından da faydalanmamız gerekmektedir. */ Overload x{ [](int a){ return a*1; }, [](int a){ return a*2; }, [](int a){ return a*3; }, [](int a){ return a*4; }, }; return 0; } /*================================================================================================================================*/ (10_29_07_2023) > "CTAD" : * Örnek 1, #include template class Myclass{ public: Myclass(const T& t){ std::cout << typeid(T).name() << '\n'; } }; template Myclass make_Myclass(const T& t) { return Myclass(t); } int main() { // Before CTAD Myclass m1(31); auto m2 = make_Myclass(3.1); // After CTAD Myclass m3(3.2); return 0; } * Örnek 2, #include template class Myclass{ public: Myclass(const T& t, const U& u){ std::cout << typeid(T).name() << '\n'; std::cout << typeid(U).name() << '\n'; } }; int main() { Myclass m1{34, 6.7}; std::cout << '\n'; Myclass m2{'U', "Yuruk"}; return 0; } * Örnek 3, #include template class Myclass{ public: Myclass(T(&)[N]){ /* * "Ctor." fonksiyonun parametresi "N" elemanlı bir diziye * referams biçimindedir. */ std::cout << typeid(T).name() << "[" << N << "]\n"; } }; int main() { // Before CTAD int a[10]{}; Myclass m1(a); // After CTAD double b[]{ 1., 2., 3., 4. }; Myclass m2(b); return 0; } * Örnek 4, #include template class Myclass{ public: Myclass(T(*f_ptr)(U(&)[N])) { /* * Buradaki "f_ptr" türü aslında fonksiyon göstericisi. * Gösterilen fonksiyon ise geri dönüş değeri "T" türünden, * parametresi ise "U" türünden "N" elemanlı diziye referans. */ std::cout << typeid(f_ptr).name() << " : " << typeid(T).name() << "(*)(" << typeid(U).name() << "[" << N << "])\n"; } }; int foo(double(&)[20]) { /* * Bu fonksiyonun parametresi ise "20" elemanlı * diziye referans biçimindedir. */ return 1; } int main() { Myclass m(foo); return 0; } * Örnek 5, #include template struct Nec{ T val; Nec() : val() {} Nec(T val) : val(val) {} //... }; int main() { Nec n1{10}; // T is "int" Nec n2; // T is "double" return 0; } * Örnek 6, #include template class Myclass{ public: Myclass(T = T{}, U = U{}, W = W{}){ /* * Burada "T, U, W" türleri sınıf ise ilgili sınıfların * "Default Ctor." fonksiyonları çağrılacak, temel türler * ise "Value Init." ile hayata gelecekti. */ std::cout << typeid(T).name() << '\n'; std::cout << typeid(U).name() << '\n'; std::cout << typeid(W).name() << '\n'; } }; int main() { /* # OUTPUT # ***** i d l ***** d d l ***** d c l ***** d c j ***** */ puts("*****"); Myclass m1; puts("*****"); Myclass m2(9.9); puts("*****"); Myclass m3(9.9, 'U'); puts("*****"); Myclass m4(9.9, 'Y', 6U); puts("*****"); return 0; } * Örnek 7, Aşağıdaki kodda "std::sort" fonksiyonunun üçüncü parametresine "std::ref" sarmalayıcısı kullanılmıştır. Eğer kullanılmasaydı, "f" nesnesi kopyalanacaktı. Bu durumda da kopyalanmış versiyonun "m_count" değişkeni artacaktı. #include #include #include template class CountCalss{ public: CountCalss(F f) : m_f{f} {} template auto operator()(Args&&... args) { ++m_count; return m_f(std::forward(args)...); } int count()const { return m_count; } private: F m_f; int m_count{}; }; int main() { /* # OUTPUT # Count: 4 Count: 4 */ // Before CTAD { std::vector msvec{"Ulya Yuruk", "Ahmet Kandemir Pehlivanli", "Merve Pehlivanli"}; auto myLambda = [](const std::string& x, const std::string& y){ return x < y; }; auto f = CountCalss{myLambda}; std::sort(msvec.begin(), msvec.end(), std::ref(f)); std::cout << "Count: " << f.count() << '\n'; } // After CTAD { std::vector msvec{"Ulya Yuruk", "Ahmet Kandemir Pehlivanli", "Merve Pehlivanli"}; auto f = CountCalss{ [](const std::string& x, const std::string& y){ return x < y; } }; std::sort(msvec.begin(), msvec.end(), std::ref(f)); std::cout << "Count: " << f.count() << '\n'; } return 0; } * Örnek 8, #include "MyRandom.h" #include "MyUtility.h" int main() { vector v1(100); vector v2{ v1 }; // "v2" is of type "std::vector" vector v3{ v1, v1 }; // "v3" is of type "std::vector>" return 0; } * Örnek 9, #include "MyRandom.h" #include "MyUtility.h" #include #include #include #include int main() { std::list> myList{ { "necati", 4.25 }, { "saadet", 6.92 }, { "umut", 4.90 }, { "selim", 2.6 } }; std::vector vec( myList.rbegin(), myList.rend() ); // "Range Ctor." for(const auto& [name, wage] : vec){ std::cout << name << ' ' << wage << '\n'; } std::vector vec2{ myList.rbegin(), myList.rend() }; // "vec2" is of type "std::vector" return 0; } * Örnek 10, #include #include int foo(int) { std::cout << "foo(int)\n"; return 1; } double bar(double, double) { std::cout << "bar(double, double)\n"; return 1.1; } struct Functor{ void operator()(int); }; int main() { std::function f1{ foo }; // "f1" is of type "std::function" std::function f2{ &foo }; // "f2" is of type "std::function" std::function f3{ bar }; // "f3" is of type "std::function" std::function f4{ Functor{} }; // "f4" is of type "std::function" std::function f5{ [](int x, int y){ return x+y; } }; // "f5" is of type "std::function" return 0; } * Örnek 11, #include int main() { std::vector vec1{ { 12, 5.6 }, { 23, 5.1 }, { 5, 1.1 } }; // ERROR std::vector> vec2{ { 12, 5.6 }, { 23, 5.1 }, { 5, 1.1 } }; // Before CTAD std::vector vec3{ std::pair{ 12, 5.6 }, std::pair{ 23, 5.1 }, std::pair{ 5, 1.1 } }; // After CTAD return 0; } * Örnek 12, #include #include template struct Nec{ Nec(T){ std::cout << "Primary Template\n"; } }; // Explicit/Full Specialization template<> struct Nec { Nec(const double& x) { std::cout << "Nec spec.\n"; } }; int main() { Nec x{ 324 }; // Nec spec. return 0; } * Örnek 13, #include #include #include #include #include #include #include #include std::mutex mt; int main() { std::optional op{ 23 }; // "std::optional" std::atomic ato{ 56 }; // "std::atomic" std::tuple tx{ 2, 5.6, "ali" }; // "std::tuple" (*) std::lock_guard guard{ mt }; // "std::lock_guard" std::complex cx{ 1.4, 7.87 }; // "std::complex" using namespace std::literals; std::pair x{ 1, 4.5 }; // "std::pair" std::pair y{ "ayse"s, "fatma" }; // "std::pair" std::pair z{ 7u, 4.5f }; // "std::pair" /* (*) * İlgili nesnenin kurucu işlevine gönderilen parametre bir dizi. Kurucu işlevin * prototipi ise referans. Fakat arka plandaki "Deduction Guide" dan dolayı * tür çıkarımı gösterici yönünde. */ return 0; } * Örnek 14, #include int main() { std::complex c1{ 1.4f, 2.2f }; // "std::complex" std::complex c2{ 2.8, 6.3 }; // "std::complex" std::complex c3 = 1.2; // "std::complex" std::complex c4 = { 4.7 }; // "std::complex" std::complex c5{ 5, 3.3 }; // ERROR (*) /* (*) * "std::complex" sınıfının tipik implementasonunda sadece * bir adet şablon parametresi "T" olduğundan, farklı türler * söz konusu olduğunda "ambiguity" meydana gelmektedir. */ return 0; } > "Deduction Guide" : Aşağıdaki örnekleri sırasıyla inceleyelim: * Örnek 1.0, Aşağıdaki örnekte ilgili "ctor." fonksiyonu referans ile parametre almakta fakat bu sefer sentaks hatası meydana gelmektedir. template class Myclass{ public: Myclass(T& r) : mx{r}{} private: T mx; }; int main() { int a[40]; Myclass m(a); // In instantiation of ‘Myclass::Myclass(T&) [with T = int [40]]’: (*) /* (*) * Burada "T" türü "int[40]" olarak çıkarım yapılmış. Bunun yegane sebebi ise ilgili "ctor." * fonksiyonunun parametresinin "T&" olmasından dolayıdır. Burada "T" türü yerine "int[40]" * geleceğinden, "mx" bir dizi olacaktır. Bir diziye de yukarıdaki şekilde ilk değer * verilemeyeceğinden dolayı, sentaks hatası oluşacaktır. */ return 0; } * Örnek 1.1, İlgili sentaks hatasını gidermek için ilgili "ctor." fonksiyonunun parametresini referans olmaktan çıkartabiliriz. Fakat bu durumda da büyük nesnelerin getirdiği kopyalama maliyetini bertaraf etmek için ilgili nesneleri "ReferanceWrapper" sınıfı ile sarmalamak gerekebilir. template class Myclass{ public: Myclass(T r) : mx{r}{} private: T mx; }; int main() { int a[40]; Myclass m(a); // In instantiation of ‘Myclass::Myclass(T&) [with T = int*]’: (*) /* (*) * Burada "T" türü "int*" olarak çıkarım yapılmış. Bunun yegane sebebi ise ilgili "ctor." * fonksiyonunun parametresinin "T" olmasından dolayıdır. Fakat burada ilgili "ctor." fonksiyonu * bir referans ile parametre almayacağı için, kopyalama maliyetinden kaçınmak adına, bu fonksiyona * gönderilen nesneleri "ReferenceWrapper" sınıfı ile sarmalamak icab edebilir. */ return 0; } * Örnek 1.2, Aşağıdaki örnekteki "Deduction Guide" ile hem "ctor." fonksiyonumuz referans parametreli hem de tür çıkarımı diziler için "array-decay" oluşturmakta. template class TypeTeller; template class Myclass{ public: Myclass(const T& r) : mx{r}{ TypeTeller tx; } private: T mx; }; // Deduction Guide: template Myclass(T)->Myclass; int main() { int a[40]; Myclass m(a); // In instantiation of ‘Myclass::Myclass(const T&) [with T = int*]’: (*) /* (*) * Artık "T" türü "int*" olarak çıkartım yapılmıştır. Yalnız bardaki "ctor." fonksiyonunun * parametrik yapısının "const" olması gerekmektedir. Çünkü "int*" dönüşmesi sonucunda * "R-Value" bir ifade oluşmakta. Böylesi bir ifadeyi de "non-const L_Value" referanslara * bağlayamayız. */ return 0; } * Örnek 2, template class TypeTeller; template class Myclass{ public: Myclass(T){ TypeTeller tx; } }; template Myclass(T)->Myclass; int main() { Myclass m(31); /* * Burada da "T" türü için tür çıkarımı "int&" olarak yapıldı. Fakat * sol taraf referanslara sağ taraf değeri bağlanamadığı için yine * sentaks hatası oluştu. */ int x{}; Myclass mm{x}; // In instantiation of ‘Myclass::Myclass(T) [with T = int&]’: return 0; } * Örnek 3, İlla da şablon olması gerekmemektedir. template class TypeTeller; template class Myclass{ public: Myclass(T){ TypeTeller tx; } }; Myclass(char)->Myclass; Myclass(short)->Myclass; Myclass(int)->Myclass; Myclass(unsigned)->Myclass; int main() { Myclass m1(5.6); // In instantiation of ‘Myclass::Myclass(T) [with T = double]’: Myclass m2('u'); // In instantiation of ‘Myclass::Myclass(T) [with T = long int]’: Myclass m3(23); // In instantiation of ‘Myclass::Myclass(T) [with T = long int]’: Myclass m4(23u); // In instantiation of ‘Myclass::Myclass(T) [with T = long int]’: return 0; } * Örnek 4, #include template class TypeTeller; template class Myclass{ public: Myclass(T){ TypeTeller tx; } }; // "Deduction Guide" : Herhangi bir tam sayı türü söz konusu olduğunda // tür çıkarımı "unsigned long long" türüne yapılacaktır. template Myclass(T)->Myclass; int main() { Myclass m1(234L); // In instantiation of ‘Myclass::Myclass(T) [with T = long long unsigned int]’: Myclass m2(234); // In instantiation of ‘Myclass::Myclass(T) [with T = long long unsigned int]’: Myclass m3(234u); // In instantiation of ‘Myclass::Myclass(T) [with T = long long unsigned int]’: Myclass m4(234LL); // In instantiation of ‘Myclass::Myclass(T) [with T = long long unsigned int]’: return 0; } * Örnek 5, template class TypeTeller; template class Myclass{ public: Myclass(T){ TypeTeller tx; } }; template Myclass(T)->Myclass; int main() { Myclass m1(10); /* * "T" için tür çıkarımı "int*" yönüne yapılacaktır. Fakat * ilgili "ctor." fonksiyonuna adres göndermediğimiz için * sentaks hatası oluşacaktır. */ return 0; } * Örnek 6, #include template class TypeTeller; template class Myclass{ public: Myclass(T){ TypeTeller tx; } }; Myclass(const char*)->Myclass; int main() { Myclass m1("ulya"); // In instantiation of ‘Myclass::Myclass(T) [with T = std::__cxx11::basic_string]’: return 0; } * Örnek 7, "std::pair" sınıfının tipik implementasyonu template class TypeTeller; template class Pair{ public: Pair(const T&, const U&){ TypeTeller tx; TypeTeller ty; } }; template Pair(T,U)->Pair; int main() { /* * Görüleceğiz üzere ilgili "ctor." fonksiyonunun parametresi * bir referans. Fakat ilgili "Deduction Guide" sonucunda tür * çıkarımı diziler için dizi olarak değil, gösterici olarak * yapılmaktadır. */ int a[10]{}; double b[15]{}; Pair p1(a,b); // In instantiation of ‘Pair::Pair(const T&, const U&) [with T = int*; U = double*]’: return 0; } * Örnek 8, #include #include #include template class TypeTeller; template struct Sum{ T value; template Sum(Types&& ...values) : value{ (values + ...) }{} // "(values + ...)" is called "Fold Expression" }; template Sum(Types&& ...ts)->Sum>; int main() { Sum s{ 1u, 2.0, 3, 4.0f }; // "common type" of the arguments of "1u, 2.0, 3, 4.0f" is "double". So, "s" is of type "Sum" Sum str{ std::string{"abcd"}, "ef" }; // "common type" of the arguments of "std::string{"abcd"}, "ef"" is "std::string". So, "s" is of type "Sum" std::cout << s.value << " / " << str.value << '\n'; // 10 / abcdef return 0; } * Örnek 9, #include #include #include template struct Nec{ T str; }; // Before C++20 template Nec(T)->Nec; int main() { /* * "Aggregate" türler için C++20 ve öncesinde "Deduction Guide" * belirtmemiz gerekmektedir. Fakat C++20 ile birlikte artık * gerek kalmamıştır. */ Nec mynec = {23}; return 0; } * Örnek 10, template class TypeTeller; template class Nec{ public: Nec(T param) : mx(param) { TypeTeller tx; } private: T mx; }; // Deduction Guide template Nec(T)->Nec; int main() { Nec x{ 5 }; // In instantiation of ‘Nec::Nec(T) [with T = int]’: Nec y( 3.3 ); // In instantiation of ‘Nec::Nec(T) [with T = double]’: auto z = Nec{ 4L }; // In instantiation of ‘Nec::Nec(T) [with T = long int]’: Nec* p = &x; // ERROR Nec n1{ 'A' }, n2{ 23 }; // ERROR return 0; } * Örnek 11, #include template struct Nec{ T val; }; // Deduction Guide explicit Nec(const char*)->Nec; int main() { Nec n1 = { "neco" }; // (*) Nec n2{ "neco" }; // "n2" is type of "Nec" Nec n3 = Nec{ "neco" }; // "n3" is type of "Nec" Nec n4 = { Nec{"neco"} }; // "n4" is type of "Nec" /* (*) * İlgili "Deduction Guide", "explicit" olarak tanımlandığı için ve * argüman olan ifade eşitliğin sağ tarafına yazıldığı için "ignore" * edilecektir. Bu durumda hata meydana gelecektir. */ return 0; } * Örnek 12, #include template struct Nec{ // 1 Nec(T) { std::cout << "Nec(T)\n"; std::cout << typeid(T).name() << '\n';} // 2 template Nec(U) { std::cout << "Nec(U)\n"; std::cout << typeid(*this).name() << '\n'; std::cout << typeid(U).name() << '\n'; } }; // Deduction Guide template explicit Nec(T)->Nec; int main() { /* * Burada "Deduction Guide" devreye girecektir çünkü "copy init." * kullanılmamıştır. Dolayısıyla "T" türü yerine "int*" türü gelecektir. * Fakat birinci "ctor." çağrılmayacaktır çünkü gönderdiğimiz argüman olan * "42" ise "int" türden. Dolayısıyla ikinci "ctor." çağrılacaktır. Bu * durumda "U" türü "int" olacaktır, gönderdiğimiz "42" argümanından dolayı. */ Nec p1{ 42 }; /* * Burada "copy init." kullanıldığı için "Deduction Guide" görmezden * gelinecektir. Dolayısıyla birinci "ctor." fonksiyonu çağrılacak ve * "T" türü "int" olarak açılacaktır. */ Nec p2 = 42; /* * Burada "Deduction Guide" devreye girecektir çünkü "copy init." * kullanılmamıştır. Dolayısıyla "T" yerine "int**" türü gelecektir. Bu * durumda yine birinci "ctor." çağrılmayacaktır çünkü "T" türü "int**" * iken gönderdiğimiz argümanın olan "i" nin türü "int*" biçiminde. Dolayısıyla * ikinci "ctor." çağrılacak ve "U" türü "int*" olacaktır. */ int i = 42; Nec p3{ &i }; /* * Burada "copy init." kullanıldığı için "Deduction Guide" görmezden * gelinecektir. Dolayısıyla birinci "ctor." fonksiyonu çağrılacak ve * "T" türü "int*" olarak açılacaktır. */ int j = 42; Nec p4 = &j; return 0; } > "Structural Binding" : Bir dizi için, elemanları "public" olan bir sınıfın ve "tuple" benzeri sınıf türünden nesnelerin elemanlarının ayrıştırılmasının daha pratik ve daha yüksek verimlilikle, örneğin gereksiz kopyalamalar yapılmadan, sağlanmasıdır. * Örnek 1, Burada "auto" anahtar sözcüğünden sonra "&" deklaratörünü de getirerek referans türler olmasını da sağlayabiliriz. #include #include struct Nec{ int i{ 31 }; double d{ 3.1 }; std::string s{ "ulya" }; }; std::pair foo() { return std::pair{ 13, 1.3 }; } int main() { //auto [first, second] = foo(); //auto [first, second]( foo() ); auto [first, second]{ foo() }; std::cout << "<" << first << "," << second << ">\n"; // <13,1.3> int my_array[3]{ 10, 11, 12 }; auto [one, two, three] = my_array; std::cout << "<" << one << "," << two << "," << three << ">\n"; // <10,11,12> Nec my_nec; auto [age, wage, name] = my_nec; std::cout << "<" << age << "," << wage << "," << name << ">\n"; // <31,3.1,ulya> } * Örnek 2, #include #include struct Nec{ double d{ 3.1 }; int i[4]{ 31 }; }; std::pair foo() { return std::pair{ 13, 1.3 }; } int main() { Nec my_nec; auto [x,y] = my_nec; // Buradaki tür çıkarımı "d" ve "x" için değil, "my_nec" içindir. Kabaca şöyledir: /* Nec __abc_tmp_var = my_nec; __abc_tmp_var.x = my_nec.d; __abc_tmp_var.y = my_nec.i; */ } * Örnek 3, #include int main() { int a[ ]{ 10, 20, 30 }; for(auto i : a) std::cout << i << " "; // 10 20 30 std::cout << '\n'; auto [m1, m2, m3] = a; ++m1; ++m2; ++m3; for(auto i : a) std::cout << i << " "; // 10 20 30 std::cout << '\n'; } * Örnek 4, #include int main() { int a[2]{0, 1}; for(auto i : a) std::cout << i << " "; // 0 1 auto& [x,y] = a; /* int (&__e)[2] = a; #define x __e[0] #define y __e[1] */ ++x; ++y; for(auto i : a) std::cout << i << " "; // 1 2 } * Örnek 5, #include int main() { int a[2]{0, 1}; auto&& [x,y] = a; // int (&__e0)[2] = a; for(auto i : a) std::cout << i << " "; // 0 1 ++x; ++y; for(auto i : a) std::cout << i << " "; // 1 2 puts("\n--------"); auto&& [i,j] = (int[2]){0, 1}; // int (&__e1)[2] = (int[2]){0, 1}; std::cout << i << " " << j << " "; // 0 1 ++i; ++j; std::cout << i << " " << j << " "; // 1 2 } * Örnek 6, #include // Cpp-Style auto get_arrayCpp()->int(&)[3] { static int a[]{ 1, 2, 3 }; return a; } // Cpp-Style with using using ar3_t = int[3]; ar3_t& get_arrayCpp2() { static int a[]{ 1, 2, 3 }; return a; } // C-Style int(&get_arrayC())[3] { static int a[]{ 10, 20, 30 }; return a; } typedef int arr4_t[3]; arr4_t& get_arrayC2() { static int a[]{ 10, 20, 30 }; return a; } int main() { /* # OUTPUT # 10,20,30 11,21,31 1,2,3 2,3,4 */ { auto [a,b,c] = get_arrayC(); std::cout << a << "," << b << "," << c << '\n'; } { auto& [d,e,f] = get_arrayC2(); std::cout << ++d << "," << ++e << "," << ++f << '\n'; } { auto [a,b,c] = get_arrayCpp(); std::cout << a << "," << b << "," << c << '\n'; } { auto& [d,e,f] = get_arrayCpp2(); std::cout << ++d << "," << ++e << "," << ++f << '\n'; } return 0; } * Örnek 7, class Myclass { int x{ 10 }; int y{ 20 }; int z{ 30 }; friend void foo(); }; void foo() { auto [x, y, z] = Myclass{}; } int main() { /* * Buradaki sentaks hatasının yegane sebebi, * ilgili veri elemanlarının "private" olmasıdır. */ auto [x, y, z] = Myclass{}; /* * Burada "foo" fonksiyonu sınıfın "private" * bölümüne erişebildiği için herhangi bir * problem oluşmayacaktır. */ foo(); } * Örnek 8, Taşıma semantiğinden de faydalanabiliriz. #include #include class Person { public: std::string m_name{ "murat" }; std::string m_surname{ "yilmaz" }; }; int main() { Person p; std::cout << "I. " << p.m_name << " " << p.m_surname << '\n'; auto [name, surname] = std::move(p); std::cout << "II. " << p.m_name << " " << p.m_surname << '\n'; std::cout << "III." << name << " " << surname << '\n'; } * Örnek 9, #include #include struct Point { int x, y, z; }; int main() { /* (1) * Anımsanacağı üzere aşağıdaki ifade sentaks * hatası oluşturacaktır. Çünkü sınıfın veri * elemanlarının adedi ile ilgili bağlamda * kullandığımız değişkenlerin adedi aynı * değildir. */ auto [a, b] = Point{}; // ERROR /* (2) * Bu durumda kullanmak istemeyeceğimiz veri * elemanlarına karşılık, ilgili bağlamda * "dummy" değişken kullanabiliriz. Pek tabii * buradaki "_" yerine başka bir şey de yazabilirdik. * Örneğin, "galib". */ auto [d, e, _] = Point{}; // OK /* * (3) * Fakat bu durumda da şöyle bir sorun meydana * gelmekte; "_" karakterini aynı isim alanı içerisinde * tekrar kullanmamız yine sentaks hatası oluşturacaktır. * Fakat yinede "_" yerine "__" karakterlerini kullanabiliriz. */ auto [f, _, g] = Point{}; // ERROR auto [f, __, g] = Point{}; // OK } * Örnek 10, #include #include #include std::tuple foo() { return { "Ulya", "Yuruk", 1995 }; } int main() { { // (1) std::tuple tx(12, 1.2, "oniki"); std::tuple ty; ty = tx; /* * Burada "tx" in öğeleri "ty" ye karşılıklı olarak kopyalanmıştır. */ } { // (2) std::tuple tx(12, 1.2, "oniki"); int i; double d; std::string s; std::tuple ty(i, d, s); ty = tx; /* * Burada "ty" nin öğeleri, atama öncesinde çöp değer taşırken atama * sonrasında, "tx" in öğelerinin değerini almıştır. */ } { // (3) std::tuple tx(12, 1.2, "oniki"); int i; double d; std::string s; std::tuple(i, d, s) = tx; /* * Burada ilgili "tuple" nesnesinin öğeleri, "tx" nesnesinin * öğelerinin değerini almıştır. */ } { // (4) /* # OUTPUT # 0, 0, 12, 1.2, oniki */ std::tuple tx(12, 1.2, "oniki"); int i{}; double d{}; std::string s; std::cout << i << ", " << d << ", " << s << '\n'; std::tie(i, d, s) = tx; std::cout << i << ", " << d << ", " << s << '\n'; /* * Burada ilgili "tuple" nesnesinin öğeleri, "tx" nesnesinin * öğelerinin değerini almıştır. Sadece yukarıdaki gibi isimsiz * "tuple" nesnesi yerine "std::tie" fonksiyonu kullanılmıştır. * Çünkü kendisi de öğeleri referans olan bir "tuple" nesnesi * döndürmektedir. */ } { // (5) /* # OUTPUT # , , 0 Ulya, Yuruk, 1995 */ std::string name, surname; int i{}; std::cout << name << ", " << surname << ", " << i << '\n'; std::tie(name, surname, i) = foo(); std::cout << name << ", " << surname << ", " << i << '\n'; /* * "Structural Binding" öncesinde benzer mekanizmayı bu şekilde * kurmaktaydık. Artık "name", "surname" ve "i" değişkenlerinin * değerleri, "foo" fonksiyonunun geri döndürdüğü değişkenlerdeki * öğelerin değerlerine sahiptir. */ } { // (6) /* # OUTPUT # , , 0 Ulya, Yuruk, 1995 */ auto&& [name, surname, i] = foo(); std::cout << name << ", " << surname << ", " << i << '\n'; } } * Örnek 11, #include int main() { std::set myset{ 12,5,67,90 }; if (auto [iter, inserted] = myset.insert(67); inserted) {} /* * ".insert()" fonksiyonunun geri dönüş değeri: * "std::pair". * Eğer ekleme işlemi başarısız olmuşsa, bu "pair" * nesnesinin "second" isimli öğesi "false" değerini * alacak ve "first" isimli "iterator" nesnesi de * "set" içerisinde halihazırda bulunan öğenin * konumunu tutacak. * Eğer ekleme işlemi başarılı olmuşsa, bu "pair" * nesnesinin "second" isimli öğesi "true" değerini * alacak ve "first" isimli öğesi de "set" içerisine * yeni eklenen öğenin konumunu tutacaktır. * İşte yukarıdaki "Structural Binding" ile "iter" * isimi değişken "std::pair" nesnesinin "first" * isimli öğesini, "inserted" ise o "pair" nesnesinin * "second" isimli öğesini gösterir durumda olacaktır. * Yukarıdaki "if" deyimi ile de amaçlanan şey şu olmuştur: * "67" nesnesi "set" içerisine eklenirse, programın akışı * "if" bloğuna girsin. */ } * Örnek 12, #include #include #include #include #include "MyRandom.h" #include "MyUtility.h" int main() { std::vector svec; MyUtility::rfill(svec, 20, MyRandom::rname); MyRandom::print(svec); if(auto [iter_min, iter_max] = minmax_element(svec.begin(), svec.end()); *iter_min > "ulya" && *iter_max < "yuruk"){ std::cout << "Found!..\n"; } } > Hatırlatıcı Notlar: >> "for_each" fonksiyonu geri dönüş değeri olarak "Callable" döndürmektedir. * Örnek 1, #include "MyRandom.h" #include "MyUtility.h" class Functor{ public: Functor(int val) : m_val{val} {} void operator()(int val) { //... if(val > m_val) ++m_count; //... } int get_count()const { return m_val; } private: int m_val, m_count{}; }; int main() { std::vector ivec; MyUtility::rfill(ivec, 20'000, MyRandom::Irand{0, 100'000}); std::cout << "Size: " << ivec.size() << '\n'; auto f = std::for_each(ivec.begin(), ivec.end(), Functor{90'000}); std::cout << f.get_count() << '\n'; return 0; } >> Derleyicinin "Template Argument" için nasıl bir çıkarım yaptığını anlamak için: * Örnek 1, template class TypeTeller; template class Myclass{ public: Myclass(const T&) { TypeTeller x; } private: T mx; }; int main() { Myclass m{ "ulya" }; // In instantiation of ‘Myclass::Myclass(const T&) [with T = char [5]]’: return 0; } * Örnek 2, template class TypeTeller; template void func(T&&) { TypeTeller x; } int main() { func(12); // In instantiation of ‘void func(T&&) [with T = int]’: int i; func(i); // In instantiation of ‘void func(T&&) [with T = int&]’: func(31); // In instantiation of ‘void func(T&&) [with T = int&&]’: return 0; } >> "Aggregate initialization" için "https://en.cppreference.com/w/cpp/language/aggregate_initialization" /*================================================================================================================================*/ (11_30_07_2023) > "Structural Binding" (devam): * Örnek 1, #include #include #include #include #include #include "MyUtility.h" #include using namespace MyUtility; using Person = std::tuple; int main() { std::vector people; people.reserve(10'000u); for (size_t i = 0; i < 10'000; i++) { people.emplace_back( Random::Irand{ 0, 100'000 }(), Utility::rname() + ' ' + Utility::rfname(), Utility::rtown() ); } std::ofstream ofs{ "out.txt" }; if (!ofs) { std::cerr << "out.txt could not be created!..\n"; exit(EXIT_FAILURE); } ofs << std::left; /* * "std::tuple" nesnelerini karşılaştırırken birinci öğesi * küçük olan küçüktür. Eğer birinci öğeler eşitse, ikinci * öğesi küçük olan küçüktür. Eğer o da eşitse, üçüncü öğesi * küçük olan küçüktür vs. */ std::sort(people.begin(), people.end()); std::cout << "Size: " << people.size() << '\n'; // W/O "Structural Binding" : for (const auto& person : people) { ofs << std::setw(12) << std::get<0>(person) << '\t' << std::setw(32) << std::get<1>(person) << '\t' << std::setw(4) << std::get<2>(person) << '\n'; } // W/ "Structural Binding" : for (const auto& [id, name, town] : people) { ofs << std::setw(12) << id << '\t' << std::setw(32) << name << '\t' << std::setw(4) << town << '\n'; } } * Örnek 2.0, #include #include #include int main() { /* # OUTPUT # 456, ulya 456, ulya 31, Otuzbir 31, Otuzbir */ std::tuple my_tuple(456, std::string("ulya")); std::cout << std::get<0>(my_tuple) << ", " << std::get<1>(my_tuple) << '\n'; auto& [id, name] = my_tuple; std::cout << id << ", " << name << '\n'; id = 31; name = "Otuzbir"; std::cout << std::get<0>(my_tuple) << ", " << std::get<1>(my_tuple) << '\n'; std::cout << id << ", " << name << '\n'; } * Örnek 2.1, #include #include #include int main() { /* (I) * Derleyici burada "id" ve "name" isimleri için * "int" ve "std::string" TÜRÜNDEN NESNE OLUŞTURMUYOR. * İlk olarak "my_tuple" nesnesine referans gizli bir * değişken hayata getiriyor. Çünkü "Structural Binding" * sırasında bizler referans deklaratörünü kullandık. (a) * Daha sonra bu gizli değişkeni kullanarak, ilgili "tuple" * nesnesinin öğe sayısını derleme zamanında kontrol ediyor. (b) * Eğer elde ettiği rakam ile ilgili "tuple" nesnesinin öğe * sayısı aynı DEĞİLSE, SENTAKS HATASI OLUŞACAK. Aksi halde * bir sonraki aşamaya geçilecek. * Bir sonraki aşamada ilgili "tuple" nesnesinin öğelerini * elde etmek için tekrar tekrar "std::get" fonksiyonunu * çağırmak yerine, bu fonksiyonu bir kez çağırıyor ve geri * dönüş değerini de yine BİR BAŞKA REFERANS DEĞİŞKENİNDE * SAKLIYOR. Burada "std::tuple_element_t" meta fonksiyonu * kullanılıyor. (c) * Gün sonunda derleyicinin ürettiği kodlar aşağıdaki gibi * olacaktır. (d) * Şimdi buradaki "std::get" fonksiyonu "L-Value Reference" * döndürdüğü için, "a_hidden_id" ve "a_hidden_name" isimli * gizli değişkenler "L-Value Reference" oldular. Pekala * "std::get" fonksiyonu "L-Value Reference" döndürmezse, * yani "value" döndürürse ne olur? Bu durumda "a_hidden_id" * ve "a_hidden_name" isimli değişkenler "R-Value Reference" * olur ve "Life Extension" oluşur. * Pekiyi bizim "Structural Binding" sırasında kullandığımız * "id" ve "name" isimli bu işin neresinde? Aslında bu isimler * "Special Identifiers" olarak geçiyor ve derleyicinin oluşturduğu * "a_hidden_id" ve "a_hidden_name" isimli değişkenlere bağlanıyor. * Fakat yine de bunlar gerçekte değişken değillerdir. Öte yandan * "Structural Binding" sırasında "&" deklaratörü kullanmasaydık ne * olacaktı? Yine "a_hidden_id" ve "a_hidden_name" isimli değişkenler * birer REFERANS OLACAKTI FAKAT "a_hidden_variable" ARTIK BİR REFERANS * OLMAYACAKTI. Direkt sınıf türünden olacaktı. */ std::tuple my_tuple(456, std::string("ulya")); auto& [id, name] = my_tuple; // (I) // auto& a_hidden_variable = my_tuple; // (a) // ...std::tuple_size_v>... // (b) // std::tuple_element_t<0, std::remove_reference_t>& a_hidden_id = std::get<0>(my_tuple); // (c) // std::tuple_element_t<1, std::remove_reference_t>& a_hidden_name = std::get<1>(my_tuple); // (c) // (d) // std::tuple my_tuple(456, std::string("ulya")); // auto& a_hidden_variable = my_tuple; // int& a_hidden_id = std::get<0>(my_tuple); // std::string& a_hidden_name = std::get<1>(my_tuple); id = 31; name = "Otuzbir"; std::cout << id << ", " << name << '\n'; } * Örnek 3, struct Myclass{ constexpr Myclass(int x, int y) : mx(x), my(y) {} int mx, my; }; int main() { constexpr Myclass m(3, 6); // OK constexpr auto [x,y] = m; // ERROR: error: structured binding declaration cannot be ‘constexpr’ } * Örnek 4.0, Aşağıdaki örnekte ise kendi sınıfımız "tuple-like" arayüzü desteklemesi sağlanmıştır. #include #include #include #include class Person{ public: Person(int id, double wage, std::string name) : m_id(id), m_wage(wage), m_name(std::move(name)) {} int get_id()const { return m_id; } double get_wage()const { return m_wage; } std::string get_name()const { return m_name; } private: int m_id; double m_wage; std::string m_name; }; namespace std{ // "tuple_size": // APPROACH - I template<> struct tuple_size : std::integral_constant{ }; /* // APPROACH - II template<> struct tuple_size{ constexpr static std::size_t value = 3u; }; */ // "tuple_element": template<> struct tuple_element<0, Person>{ using type = int; }; template<> struct tuple_element<1, Person>{ using type = double; }; template<> struct tuple_element<2, Person>{ using type = string; }; } // "get": template auto get(const Person& p) { if constexpr (N == 0) return p.get_id(); else if constexpr (N == 1) return p.get_wage(); else return p.get_name(); } int main() { Person p1{ 348'975, 245.87, "ruveyda" }; auto [ID, WAGE, NAME] = p1; std::cout << "<" << ID << "," << WAGE << "," << NAME << ">\n"; } * Örnek 4.1, #include #include #include class Customer{ private: std::string first; std::string last; long val; public: Customer(std::string f, std::string l, long v) : first(std::move(f)), last(std::move(l)), val(v) {} const std::string& firstname()const { return first; } // "const overload" std::string& firstname() { return first; } const std::string& lastname()const { return last; } // "const overload" std::string& lastname() { return last; } long value()const { return val; } // "const overload" long& value() { return val; } }; // "tuple_size" : template<> struct std::tuple_size{ static constexpr int value = 3; }; // "tuple_element" : template<> struct std::tuple_element<2, Customer>{ using type = long; }; // "explicit/full specialization" template struct std::tuple_element{ using type = std::string; }; // "partial specialization" // "std::get" : template decltype(auto) get(Customer& c) { static_assert(I < 3); if constexpr (I==0) return c.firstname(); else if constexpr (I==1) return c.lastname(); else return c.value(); } template decltype(auto) get(const Customer& c) { static_assert(I < 3); if constexpr (I==0) return c.firstname(); else if constexpr (I==1) return c.lastname(); else return c.value(); } template decltype(auto) get(Customer&& c) { static_assert(I < 3); if constexpr (I==0) return std::move(c.firstname()); else if constexpr (I==1) return std::move(c.lastname()); else return c.value(); } int main() { Customer c1{ std::string("ruveyda"), std::string("celik"), 20082023 }; auto [NAME, SURNAME, YEAR] = c1; std::cout << "<" << NAME << "," << SURNAME << "," << YEAR << ">\n"; } > "3-Way Comparison Operator" : "compare" başlık dosyasını kullanmalıyız. C++20 öncesinde toplamda altı adet karşılaştırma operatörü mevcuttur. Bunlar ise iki gruba ayrılmıştır; "Equality Operators" ve "Relational Operatos". >> "Equality Operators" : "==" ve "!=" operatörleridir. >> "Relational Operators" : "<", "<=", ">" ve ">=" operatörleridir. Bu operatörlerin hepsi de aynı statüdedir ve hepsinin ürettiği değer "bool" türündendir. "custom" türler için yine bu operatörleri kullanmak istediğimiz zamanda genel olarak "<" ve "==" operatörleri "overload" edilir, diğer dört operatör ise bu "overload" edilmiş fonksiyonları çağırması sağlanır. Örneğin, a != b !(a == b) a > b b < a a >= b !(a < b) a <= b !(b < a) şeklinde. Fakat bazı durumlarda bu şekildeki yaklaşım da problemli olabiliyor: * Örnek 1, #include #include int main() { /* # OUTPUT # d1 < d2 = false d1 > d2 = false d1 <= d2 = false d1 >= d2 = false d1 == d2 = false d1 != d2 = true */ double d1{ NAN }; double d2{ 4.56 }; std::boolalpha(std::cout); std::cout << "d1 < d2 = " << (d1 < d2) << '\n'; std::cout << "d1 > d2 = " << (d1 > d2) << '\n'; std::cout << "d1 <= d2 = " << (d1 <= d2) << '\n'; std::cout << "d1 >= d2 = " << (d1 >= d2) << '\n'; std::cout << "d1 == d2 = " << (d1 == d2) << '\n'; std::cout << "d1 != d2 = " << (d1 != d2) << '\n'; } Pekiyi nedir bu "3-Way Comparison Operator"? Bu operatör "<=>" atomu ile temsil edilmektedir. Önceliği diğer karşılaştırma operatörlerinden daha yüksektir. Bu operatörün dile eklenmesi ile operatörler iki gruba ayrılmıştır: "Primary", "Secondary". >> "Primary" : Argümanları "reverse" edilebilen operatörlerdir. Bunlar "==" ve "<=>" operatörleridir. Yani "a==b" demek ile "b==a" demek aynıdır. Anımsanacağı üzere sınıfın üye fonksiyonu olarak "overload" edilen ".operator==()" fonksiyonu söz konusu olduğunda, bu fonksiyonun çağrılabilmesi için "==" operatörünün sol operandının ilgili sınıf türünden OLMASI GEREKİYOR İDİ. >> "Secondary" : Geri kalan dört operatör bu gruptandır. Bu gruptakiler ise "rewritable" operatörlerdir. Yani derleyici bu operatörleri, yukarıdaki "Primary" gruptaki operatörlere çevirebilmektedir. Örneğin, "!=" operatörü bu gruptandır. Derleyici bu operatörü gördüğü yerde "!(==)" ifadesini yazabilmektedir, tabii bu süreç ise isim arama sırasında gerçekleşmektedir. * Örnek 1, #include #include class Myclass{ public: bool operator==(int)const{ //... return true; } }; int main() { /* # OUTPUT # */ Myclass m; bool b1 = (m == 5); // I bool b2 = (m != 5); // II bool b3 = (5 == m); // III bool b4 = (5 != m); // IV /* * C++20 öncesinde sadece "I" nolu ifade doğru, diğerleri sentaks * hatasına yol açmaktaydı. Bunun yegane sebebi ise şudur: * II : "!=" operatörü için ilgili fonksiyon "overload" edilmemiştir. * III: "==" operatörünün sol operandının bir sınıf türünden olması gerek. * IV : Hem "!=" operatörü için ilgili fonksiyon "overload" değil hem de * bu operatörün sol operandı bir sınıf türünden değil. */ /* * Fakat C++20 ile yukarıdaki dört ifade derleyici tarafından aşağıdaki * biçimde ele alınmakta: * I : bool b1 = (m == 5); * II : bool b2 = !(m == 5); * III: bool b3 = (m == 5); * IV : bool b4 = !(m == 5); */ } Diğer yandan bu operatörün dile eklenmesiyle hem "==" hem de "<=>" operatörleri "default" edilebiliyor. Pekala sadece "<=>" operatörünü "default" edersek, derleyici bizim için "==" operatörünü de "default" etmektedir. Fakat sadece "==" operatörünü "default" etmemiz durumunda, "<=>" operatörü "default" EDİLMEYECEKTİR. Pek tabii "<=>" operatör fonksiyonunu da hem "global" hem de "member" fonksiyon olarak "overload" edebilmekteyiz. * Örnek 1, #include class Myclass{ public: Myclass(int x) : mx(x) {} auto operator<=>(const Myclass&)const = default; private: int mx; }; int main() { /* # OUTPUT # m1 < m2 : true m1 > m2 : false m1 <= m2 : true m1 >= m2 : false m1 == m2 : false m1 != m2 : true */ std::boolalpha(std::cout); Myclass m1{ 24 }, m2{ 56 }; std::cout << "m1 < m2 : " << (m1 m2 : " << (m1>m2) << '\n'; std::cout << "m1 <= m2 : " << (m1<=m2) << '\n'; std::cout << "m1 >= m2 : " << (m1>=m2) << '\n'; std::cout << "m1 == m2 : " << (m1==m2) << '\n'; std::cout << "m1 != m2 : " << (m1!=m2) << '\n'; } Tabii gerek "==" gerek "<=>" fonksiyonlarının parametrelerinin "const" olması ve sınıf içerisinde tanımlandığında da yine "const" bir "member function" olması gerekmektedir. Eğer bu iki fonksiyonu "default" etmemiz durumunda ise derleyici fonksiyonların kodlarını şu şekilde yazacaktır: -> "==" operatör fonksiyonunu "default" ettiğmiz zaman derleyici, veri elemanlarının bildirim sırasına göre, bütün veri elemanları için "==" operatör fonksiyonunu çağıracak. Çünkü bu operatör dilin temel sentaks kuralına eklendiği için, temel türler ve standart kütüphanedeki diğer sınıflar için bu operatör fonksiyonu belirtilmiştir. Bütün fonksiyon çağrılarının sonuçları "true" olduğunda, "default" edilen fonksiyon da "true" değer döndürecektir. -> "<=>" operatör fonksiyonunu "default" ettiğmiz zaman derleyici, veri elemanlarının bildirim sırasına göre, bütün veri elemanları için "<=>" operatör fonksiyonunu çağıracak. Çünkü bu operatör dilin temel sentaks kuralına eklendiği için, temel türler ve standart kütüphanedeki diğer sınıflar için bu operatör fonksiyonu belirtilmiştir. Burada temel türler söz konusu olduğunda karşılaştırma sonucu "strong_ordering", gerçek sayı türleri için "partial_ordering", standart kütüphanedeki diğer sınıflar için ise ilgili türler olacaktır. Örneğin, "std::string" sınıfı için "strong_ordering". Öte yandan bu operatörün geri dönüş değerinin türü neden "auto" olarak belirlendi? Aslında bakarsanız "<=>" operatörü üç farklı sınıf türünden değer döndürmektedir. Bunlar "strong_ordering", "weak_ordering" ve "partial_ordering". >> "strong_ordering" : Bu sınıf türü ise şu değerlere sahiptir: "std::strong_ordering::equal", "std::strong_ordering::equivalent", "std::strong_ordering::less" ve "std::strong_ordering::greater". Bu sınıf türü şunu temsil etmektedir: "a" ve "b" iki operand olmak şartıyla; ya "a > b" ya "a < b" ya da "a == b". >> "weak_ordering" : Bu sınıf türü ise şu değerlere sahiptir: "std::weak_ordering::equivalent", "std::weak_ordering::less" ve "std::weak_ordering::greater". Bu sınıf türü şunu temsil etmektedir: "a" ve "b" iki operand olmak şartıyla; ya "a > b" ya "a < b" ya da "a" ve "b" eş değer olabilir ki eş değer olmaları eşit olmaları zorunluluğunu da beraberinde getirmemektedir. Örneğin, "ahmet" ile "AHMET" yazılarının karşılaştırılması buna bir örnektir. Özünde birbirine eşit değillerdir fakat "incase-sensitive" karşılaştırma yaptığımız zaman bunları eşit kabul etmekteyiz. >> "partial_ordering" : Bu sınıf türü ise şu değerlere sahiptir: "std::partial_ordering::equivalent", "std::partial_ordering::less", "std::partial_ordering::greater" ve "std::partial_ordering::unordered". Büyük ölçüde gerçek sayı türlerinin karşılaştırılmasına yöneliktir. "weak_ordering" e ilaveten karşılaştırılmasında herhangi bir sıralama olmayan özel değerler de olabilir. Örneğin, "NAN" değerinin karşılaştırılması gibi. Pekala bu operatörün geri döndürdüğü iş bu sınıfları "0" değeri ile de karşılaştırabiliyoruz ki bu işlem de bizlere "bool" türünü geri döndürmektedir. * Örnek 1, #include #include template void print_compare(const T& t, const U& u) { using result_type = std::compare_three_way_result_t; std::string s_type = typeid(result_type).name(); std::cout << "Comare Result Type: " << s_type << '\n'; auto result = t <=> u; std::cout << "Result of Comparison: "; if(result == 0){ if(std::is_same_v) std::cout << "equal"; else std::cout << "equivalent"; } else if (result > 0) std::cout << "greater"; else if (result < 0) std::cout << "less"; else std::cout << "unordered"; } int main() { /* # OUTPUT # Comare Result Type: St15strong_ordering Result of Comparison: greaterComare Result Type: St15strong_ordering Result of Comparison: equalComare Result Type: St15strong_ordering Result of Comparison: less ----------------------------------- Comare Result Type: St16partial_ordering Result of Comparison: lessComare Result Type: St16partial_ordering Result of Comparison: equivalentComare Result Type: St16partial_ordering Result of Comparison: lessComare Result Type: St16partial_ordering Result of Comparison: unordered ----------------------------------- */ puts("\n-----------------------------------"); print_compare(12, 6); print_compare(12, 12); print_compare(12, 18); puts("\n-----------------------------------"); print_compare(1.2, 6.); print_compare(1.2, 1.2); print_compare(1.2, 1.8); print_compare(1.2, NAN); puts("\n-----------------------------------"); } Tabii "<=>" operatörünün geri döndürdüğü yukarıdaki sınıf türleri de birbirine dönüşebilmektedir. Şöyleki: -> "strong_ordering" türünü, "weak_ordering" ve "partial_ordering" türlerine örtülü olarak dönüştürebiliriz. -> "weak_ordering" türünü ise "partial_ordering" türlerine örtülü olarak dönüştürebiliriz. Diğer yandan kendi sınıflarımız için bu operatör fonksiyonunu nasıl yazardık? Anımsanacağı üzere bu operatör dilin temel sentaks kuralına eklendi. Yani temel türler ve standart kütüphanedeki çoğu fonksiyon bu operatöre ilişkin bir fonksiyonu barındırmaktadır. Dolayısıyla kendi sınıflarımız için olan versiyonunu da yine bu operatörü kullanarak yazacağız: * Örnek 1, #include #include class Person{ public: Person(const char* p, int a) : name{p}, age{a} {} auto operator<=>(const Person& other) { if(auto cmp = name <=> other.name; cmp != 0) return cmp; return age <=> other.age; } private: std::string name; int age; }; int main() { /* # OUTPUT # true ----- true ----- false ----- false */ { Person p1{"muhittin", 56}; Person p2{"ayse", 40}; std::cout << std::boolalpha << (p1 > p2) << '\n'; // std::cout << std::boolalpha << (p1 <=> p2 > 0) << '\n'; } puts("-----"); { Person p1{"ayse", 56}; Person p2{"ayse", 40}; std::cout << std::boolalpha << (p1 > p2) << '\n'; } puts("-----"); { Person p1{"ayse", 20}; Person p2{"ayse", 40}; std::cout << std::boolalpha << (p1 > p2) << '\n'; } puts("-----"); { Person p1{"ali", 40}; Person p2{"ayse", 40}; std::cout << std::boolalpha << (p1 > p2) << '\n'; } } * Örnek 2.0, Fakat aşağıdaki kodda sentaks hatası bulunmaktadır. Çünkü ilgili operatör fonksiyonunun geri dönüş değer tür çıkarımı için farklı çıkarımlarda bulunulmuştur. #include #include class Person{ public: Person(const char* p, double s, int a) : name{p}, salary{s}, age{a} {} auto operator<=>(const Person& other) { if(auto cmp = name <=> other.name; cmp != 0) return cmp; if(auto cmp = age <=> other.age; cmp != 0) return cmp; return salary <=> other.salary; // inconsistent deduction for auto return type: ‘std::strong_ordering’ and then ‘std::partial_ordering’ } private: std::string name; double salary; int age; }; int main() { /* # OUTPUT # */ { Person p1{"muhittin", 5.6, 56}; Person p2{"ayse", 4., 40}; std::cout << std::boolalpha << (p1 > p2) << '\n'; // std::cout << std::boolalpha << (p1 <=> p2 > 0) << '\n'; } puts("-----"); { Person p1{"ayse", 5.6, 56}; Person p2{"ayse", 4., 40}; std::cout << std::boolalpha << (p1 > p2) << '\n'; } puts("-----"); { Person p1{"ayse", 2., 20}; Person p2{"ayse", 4., 40}; std::cout << std::boolalpha << (p1 > p2) << '\n'; } puts("-----"); { Person p1{"ali", 4., 40}; Person p2{"ayse", 4., 40}; std::cout << std::boolalpha << (p1 > p2) << '\n'; } } Dolayısıyla bu örnekteki sentaks hatasını gidermek için şu yöntemleri kullanabiliriz: -> Şablonları kullansaydık, "Trailing Return Type" mekanizmasından faydalanabilirdik. Fakat bu örneğimiz için buna gerek yoktur. -> Bu örnek için "auto" yerine iki farklı tür gelecektir: "strong_ordering" ve "partial_ordering". Bunlardan en zayıf olanı seçmemiz tavsiye edilmektedir. Dolayısıyla "auto" yerine "std::partial_ordering" yazmamız sorunu çözecektir. -> Bir meta fonksiyon olan "std::common_comparison_category" fonksiyonunu kullanmak. Böylelikle derleme zamanında, "n" tane argümanın ortak türünü hesaplamış oluyoruz. Örneğin, auto operator<=>(const Person& other) const -> std::common_comparison_category_t < decltype(name<=>other.name), decltype(salary<=>other.salary), decltype(age<=>other.age) > { if(auto cmp = name <=> other.name; cmp != 0) return cmp; if(auto cmp = age <=> other.age; cmp != 0) return cmp; return salary <=> other.salary; } -> İlgili operatör fonksiyonunun yazım biçimini uygun biçimde değiştirerek. Örneğin, std::strong_ordering operator<=>(const Person& other)const { if(auto cmp = name <=> other.name; cmp != 0) return cmp; if(auto cmp = age <=> other.age; cmp != 0) return cmp; auto cmp = salary <=> other.salary; // WAY - I // return std::strong_order(salary, other.salary); // WAY - II std::assert(cmp != std::partial_ordering::unordered); return cmp == 0 ? std::strong_ordering::equal : cmp > 0 ? std::strong_ordering::greater : std::strong_ordering::less; } * Örnek 2.1, #include #include #include class Person{ private: std::string name; double salary; int age; public: Person(const char* p, double s, int a) : name{p}, salary{s}, age{a} {} auto operator<=>(const Person& other) const -> std::common_comparison_category_t < decltype(name<=>other.name), decltype(salary<=>other.salary), decltype(age<=>other.age) > { if(auto cmp = name <=> other.name; cmp != 0) return cmp; if(auto cmp = age <=> other.age; cmp != 0) return cmp; return salary <=> other.salary; } }; int main() { /* # OUTPUT # true ----- true ----- false ----- false */ { Person p1{"muhittin", 5.6, 56}; Person p2{"ayse", 4., 40}; std::cout << std::boolalpha << (p1 > p2) << '\n'; // std::cout << std::boolalpha << (p1 <=> p2 > 0) << '\n'; } puts("-----"); { Person p1{"ayse", 5.6, 56}; Person p2{"ayse", 4., 40}; std::cout << std::boolalpha << (p1 > p2) << '\n'; } puts("-----"); { Person p1{"ayse", 2., 20}; Person p2{"ayse", 4., 40}; std::cout << std::boolalpha << (p1 > p2) << '\n'; } puts("-----"); { Person p1{"ali", 4., 40}; Person p2{"ayse", 4., 40}; std::cout << std::boolalpha << (p1 > p2) << '\n'; } } * Örnek 2.2, #include #include #include class Person{ private: std::string name; double salary; int age; public: Person(const char* p, double s, int a) : name{p}, salary{s}, age{a} {} std::strong_ordering operator<=>(const Person& other)const { if(auto cmp = name <=> other.name; cmp != 0) return cmp; if(auto cmp = age <=> other.age; cmp != 0) return cmp; auto cmp = salary <=> other.salary; assert(cmp != std::partial_ordering::unordered); return cmp == 0 ? std::strong_ordering::equal : cmp > 0 ? std::strong_ordering::greater : std::strong_ordering::less; } }; int main() { /* # OUTPUT # true ----- true ----- false ----- false */ { Person p1{"muhittin", 5.6, 56}; Person p2{"ayse", 4., 40}; std::cout << std::boolalpha << (p1 > p2) << '\n'; // std::cout << std::boolalpha << (p1 <=> p2 > 0) << '\n'; } puts("-----"); { Person p1{"ayse", 5.6, 56}; Person p2{"ayse", 4., 40}; std::cout << std::boolalpha << (p1 > p2) << '\n'; } puts("-----"); { Person p1{"ayse", 2., 20}; Person p2{"ayse", 4., 40}; std::cout << std::boolalpha << (p1 > p2) << '\n'; } puts("-----"); { Person p1{"ali", 4., 40}; Person p2{"ayse", 4., 40}; std::cout << std::boolalpha << (p1 > p2) << '\n'; } } > Hatırlatıcı Notlar: >> "std::tuple_element_t", "std::tuple_size_v" ve "std::get" fonksiyonlarını "std::tuple", "std::pair" ve "std::array" sınıfları ile birlikte kullanabiliriz. Dahası, kendi sınıflarımız da "tuple-like interface" sunuyorsa, kendi sınıflarımız için de kullanabiliriz. Bu durumda "std::get" fonksiyonunu "overload" etmemiz veya "Specialization" yazmamız gerekmektedir. * Örnek 1, #include #include #include #include using tp_type = std::tuple; using pr_type = std::pair; using ar_type = std::array; int main() { /* # OUTPUT # 100, 100.001, Hundred Ulya, Yuruk 12...00 */ auto tuple_size{ std::tuple_size_v }; // "tuple_size" is of type "size_t" with value of "3U" std::tuple_element_t<0, tp_type> var0; // "var0" is of type "int". std::tuple_element_t<1, tp_type> var1; // "var1" is of type "double". std::tuple_element_t<2, tp_type> var2; // "var2" is of type "std::string". tp_type my_tuple{ 100, 100.001, "Hundred" }; std::cout << std::get<0>(my_tuple) << ", " << std::get<1>(my_tuple) << ", " << std::get<2>(my_tuple) << '\n'; auto pair_size{ std::tuple_size_v }; // "pair_size" is of type "size_t" with value of "2U" std::tuple_element_t<0, pr_type> var00; // "var00" is of type "std::string". std::tuple_element_t<1, pr_type> var11; // "var11" is of type "std::string". pr_type my_pair{ "Ulya", "Yuruk" }; std::cout << std::get<0>(my_pair) << ", " << std::get<1>(my_pair) << '\n'; auto ar_size{ std::tuple_size_v }; // "ar_size" is of type "size_t" with value of "20U" std::tuple_element_t<0, ar_type> var000; // "var000" is of type "double". ar_type my_array{ 1., 2., 3., }; std::cout << std::get<0>(my_array) << std::get<1>(my_array) << "..." << std::get<18>(my_array) << std::get<19>(my_array) << '\n'; } >> "Lambda Expression" ile "Structural Binding" kullanabilmek için "Lambda Init. Capture" yöntemini kullanmamız gerekiyordu, C++20'ye kadar. C++20 ile direkt olarak "capture" edebiliyoruz. * Örnek 1, #include #include int main() { auto [x,y] = std::make_tuple(10, 1.0); /* Until C++20: * (7): error C2429: language feature 'structured bindings' requires compiler flag '/std:c++17' * (8): error C2439: 'main::::x': member could not be initialized * (8): note: see declaration of 'main::::x' * (8): error C2439: 'main::::y': member could not be initialized * (8): note: see declaration of 'main::::y' */ auto f = [x,y]{ return x+y; }; // ERROR // auto f = [&x = x, &y = y]{ return x+y; }; // SOLUTION: "&x = x, &y = y" is a "Lambda Init. Capture". // Since C++20 auto f = [x,y]{ return x+y; }; // OK } /*================================================================================================================================*/ (12_05_08_2023) > "3-Way Comparison Operator" (devam): Bu operatör fonksiyonunu tanımladığımız zaman, sınıfımızı kullanacak "client" kodlar yine diğer karşılaştırma operatörlerini kullanmaya devam edecekler. Fakat o operatörler bizim "<=>" operatör fonksiyonumuzu çağıracaklar. Örneğin, "Date" sınıfını ele alalım. C++20 öncesinde bu sınıf türü karşılaştırma yapabilmek için yukarıdaki zikredilen altı operatör için de fonksiyon tanımlamamız gerekiyordu. Daha sonra "Date" sınıfını kullanacak kişiler yine karşılaştırma yapmak için istediği operatörü kullanabilirmektedir. Fakat C++20 ile birlikte bizler ilgili altı operatör için de ayrı ayrı operatör fonksiyonu yazmamıza gerek kalmadı. Sadece "<=>" operatör fonksiyonunu yazarak bu yazma yükünden kurtulabiliriz. Yine bizim sınıfımızı kullanacak kişiler yine istedikleri karşılaştırma operatörü ile işlerini halledebilirler. * Örnek 1, // Until C++20: namespace Date { //... class Date { //... friend bool operator<(const Date& d1, const Date& d2); friend bool operator==(const Date& d1, const Date& d2); friend int operator-(const Date& d1, const Date& d2); //... }; //... inline bool operator>=(const Date& d1, const Date& d2) { return !(d1 < d2); } inline bool operator<=(const Date& d1, const Date& d2) { return !(d2 < d1); } inline bool operator!=(const Date& d1, const Date& d2) { return !(d1 == d2); } bool operator<(const Date& d1, const Date& d2) { return d1.y_ != d2.y_ ? d1.y_ < d2.y_ : d1.m_ != d2.m_ ? d1.m_ < d2.m_ : d1.d_ < d2.d_; } bool operator==(const Date& d1, const Date& d2) { return d1.y_ == d2.y_ && d1.m_ == d2.m_ && d1.d_ == d2.d_; } int operator-(const Date& d1, const Date& d2) { return d1.totaldays() - d2.totaldays(); } //... } // Since C++20: namespace Date { //... class Date { //... auto operator<=>(const Date& date)const = default; }; } * Örnek 2, Aşağıdaki örnekte ise yine "Date" sınıfımız için "operator<=>()" fonksiyonunu "default" etmemiz halinde derleyicinin yazacağı takribi semantik belirtilmiştir. Burada dikkat etmemiz gereken nokta, veri elemanlarının bildirim sırası gözetilerek karşılaştırmaya tabii tutulmalarıdır. Dolayısıyla bu noktaya dikkat etmemiz gerekmektedir. class Date{ public: std::strong_ordering operator<=>(const Date& other) const noexcept { if(auto cmp = md <=> other.md; cmp != 0) return cmp; if(auto cmp = mm <=> other.mm; cmp != 0) return cmp; return my <=> other.my; } private: int md, mm, my; }; //... Diğer yandan bu "==" ve "<=>" fonksiyonlarını "default" ederken dikkat etmemiz gereken bir diğer nokta ise bu fonksiyonlar derleyici tarafından yazıldığında "constexpr" ve "noexcept" olacak şekilde yazılırlar. Her ne kadar bu fonksiyonların bildirimlerinde bizler bu kelimeleri yazmasak da. Tabii burada öğelerin karşılaştırılmasının da "noexcept" garantisinin VERMESİ ve fonksiyonun "constexpr" OLMA KOŞULLARININ SAĞLAMIŞ OLMASI GEREKMEKTEDİR. Öte yandan sadece "<=>" operatörünü "default" ederken kullandığımız belirteçler de "==" operatörüne aktarılmaktadır. * Örnek 1.0, #include class Nec{ public: constexpr Nec(int x = 0) : mx{x} {} bool operator==(const Nec& other)const { return true; } // ERROR private: int mx; }; int main() { constexpr Nec n1{ 345 }; constexpr Nec n2{ 456 }; constexpr auto b = n1 == n2; // ERROR: error: call to non-‘constexpr’ function ‘bool Nec::operator==(const Nec&) const’ constexpr auto c = noexcept(n1 == b2); // "b" is not "noexcept". } * Örnek 1.1, #include class Nec{ public: constexpr Nec(int x = 0) : mx{x} {} // bool operator==(const Nec& other)const = default; // OK private: int mx; }; int main() { constexpr Nec n1{ 345 }; constexpr Nec n2{ 456 }; constexpr auto b = n1 == n2; // OK constexpr auto c = noexcept(n1 == b2); // "b" is "noexcept". } * Örnek 2, #include class Nec{ public: constexpr Nec(int x = 0) : mx{x} {} [[nodiscard]] auto operator<=>(const Nec& other)const = default; private: int mx; }; int main() { constexpr Nec n1{ 345 }; constexpr Nec n2{ 456 }; constexpr auto b = n1 == n2; constexpr auto c = noexcept(n1 == n2); n1 == n2; // warning: ignoring return value of ‘constexpr bool Nec::operator==(const Nec&) const’, declared with attribute ‘nodiscard’ [-Wunused-result] } * Örnek 3, #include // Bizlerin yazdığı: template class Type{ public: [[nodiscard]] virtual std::strong_ordering operator<=>(const Type&) const requires(!std::same_as) = default; // ^ ^ // Bu kısım "requires closures" olarak // geçer. "T" türünün "bool" olmaması // GEREKİYOR. }; // Derleyicinin yazdığı: template class Type{ //... public: [[nodiscard]] virtual std::strong_ordering operator<=>(const Type&) const requires(!std::same_as) = default; [[nodiscard]] virtual bool operator==(const Type&) const requires(!std::same_as) = default; }; int main() { /* * Görüleceği üzere "<=>" operatör fonksiyonunu nasıl bildirdiysek, derleyici de aynı * şekilde "==" operatör fonksiyonunu bildirmektedir. */ } Öte yandan "<=>" operatörünü "container" sınıfların karşılaştırılmasında da kullanabiliriz. Anımsayacağınız üzere bu tip sınıfları karşılaştırmanın bir yolu da "std::lexicographical_compare" fonksiyonuna çağrıda bulunmaktır ki bu fonksiyon da "<" operatörüne göre karşılaştırma yapmaktadır. Eğer "<=>" operatörünü kullanmak istersek, "lexicographical_compare_three_way" fonksiyonuna çağrı yapmamız gerekmektedir. * Örnek 1, #include #include #include #include int main() { /* # OUTPUT # false false */ std::list my_list{ 3, 6, 9, 12, 9, 6, 3 }; std::vector my_vec{ 3, 6, 9, 12 }; std::cout << std::boolalpha << std::lexicographical_compare(my_list.begin(), my_list.end(), my_vec.begin(), my_vec.end()) << '\n'; /* * Burada karşılaştırma kriteri olarak "std::less" kullanılmıştır. * Pekala bunun yerine "<=>" operatörünü de karşılaştırma için * kullanabilirdik. Fakat bu durumda "lexicographical_compare_three_way" * fonksiyonunu çağırmamız gerekmektedir. Bu fonksiyonun geri dönüş * değerinin türü "<=>" operatörünün geri döndürdüğü sınıf türleridir. */ std::cout << std::boolalpha << (std::lexicographical_compare_three_way(my_list.begin(), my_list.end(), my_vec.begin(), my_vec.end()) < 0) << '\n'; } * Örnek 2, #include #include #include #include std::ostream& operator<<(std::ostream& os, std::strong_ordering sval) { return os << ( sval == 0 ? "equal" : sval < 0 ? "less" : "greater" ); } int main() { /* # OUTPUT # greater ------- equal ------- less ------- greater */ { std::list my_list{ 3, 6, 9, 12, 9, 6, 3 }; std::vector my_vec{ 3, 6, 9, 12 }; auto result = std::lexicographical_compare_three_way(my_list.begin(), my_list.end(), my_vec.begin(), my_vec.end()); std::cout << result; } puts("\n-------"); { std::list my_list{ 3, 6, 9, 12 }; std::vector my_vec{ 3, 6, 9, 12 }; auto result = std::lexicographical_compare_three_way(my_list.begin(), my_list.end(), my_vec.begin(), my_vec.end()); std::cout << result; } puts("\n-------"); { std::list my_list{ 3, 6, 9 }; std::vector my_vec{ 3, 6, 9, 12 }; auto result = std::lexicographical_compare_three_way(my_list.begin(), my_list.end(), my_vec.begin(), my_vec.end()); std::cout << result; } puts("\n-------"); { std::list my_list{ 3, 6, 9 }; std::vector my_vec{ 3, 3, 9 }; auto result = std::lexicographical_compare_three_way(my_list.begin(), my_list.end(), my_vec.begin(), my_vec.end()); std::cout << result; } } > "Advanced Lambda Expression" : Anımsanacağı üzere "lambda expression" karşısında derleyici bir sınıf türü oluşturmakta ve ilgili ifadeyi de bu sınıf türünden "PR-Value" olarak ele almaktadır. Yani bir deyişle derleyicinin oluşturduğu sınıf türünden geçici bir nesne olarak ele alınıyor, iş bu "lambda expressions". Burada derleyicinin oluşturduğu bu sınıfa "closure type", bu sınıf türünden oluşturulan nesneye de "closure object" denmektedir. Bir "lambda expression" ise aşağıdaki bileşenlerden meydana gelmektedir: [ ] ( ) specifiers exception attr ->ret { /* code; */ } ^ ^ ^ ^ ^ ^ | | | | | | | | | | | vi. LAMBDA BODY | | | | v. Trailing Return Type | | | iv. (optional): mutable, constexpr, consteval, noexcep, attributes | | iii. Parameter List(it is optional when no specifiers added) | ii. (optional): Template parameter list i. The lambda introducer with an optional capture list -> i. Eğer ilgili "lambda" ifadesi dışarıdan herhangi bir şey "capture" etmiyorsa, o "lamda" için "stateless" terimi kullanılır. Eğer en az bir şey "capture" ediyorsa da "statefull" ifadesi kullanılır. -> ii. C++20 ile eklenen bir öğedir. Özetle; derleyicinin yazdığı sınıfın ".operator()()" fonksiyonunu şablon fonksiyon olarak yazmasını sağlamak içindir. -> iii. Derleyicinin yazdığı ".operator()()" fonksiyonunun parametrelerini belirtmektedir. Eğer "iv." ve "v." kısımlar için bir şey yazmazsak ve o fonksiyon da parametre almayacaksa, buradaki parantezleri kullanmaya gerek yoktur. Öte yandan C++23 ile birlikte, yine "iv." ve "v." kısımlar için bir şey yazmasak fakat o fonksiyon parametre alsa, yine de buradaki parantezleri kullanmayabiliriz. -> iv. Belirtilen anahtar sözcükleri yazabileceğimiz kısım. -> v. Derleyicinin yazdığı ".operator()()" fonksiyonunun geri dönüş değerinin türünü belirttiğimiz kısımdır. Artık tür çıkarımı yapılmayacaktır, geri dönüş değeri için. -> vi. Derleyicinin yazdığı ".operator()()" fonksiyonunun ana bloğudur. Şimdi de birkaç hatırlatıcı örneklere bakalım: * Örnek 0, #include #include int g_x = 99; auto fx = [=]{ return g_x + 1; }; auto fy = [g_x = g_x] { return g_x + 1; }; double i{}; int main() { { g_x = 500; std::cout << fx() << '\n'; std::cout << fy() << '\n'; } puts("\n---"); { auto x = []{static int x{}; return ++x; }; auto y = []{static int x{}; return ++x; }; std::cout << x() << x() << x() << '\n'; std::cout << y() << y() << y() << '\n'; } puts("\n---"); { auto x = []{ static int x{}; return ++x; }; decltype(x) y; decltype(x) z; std::cout << y() << y() << y() << '\n'; std::cout << z() << z() << z() << '\n'; } puts("\n---"); { const int x = 10; auto f = [x]()mutable{ ++x; }; /* ^ ^ * I II * ERROR: * "I" demek "Copy Capture" demektir. Dolayısıyla * derleyicinin yazdığı sınıfın veri elemanı "const" * olmuştur. "II" deki "mutable" anahtar sözcüğü ise * yine aynı sınıfın ".operator()()" fonksiyonunun * "non-const" olması demektir. Sınıfın veri elemanı * "const" olduğundan, onu değiştirmeye çalışmak * sentaks hatası olacaktır. */ auto g = [x = x](){ ++x; }; /* ^ * I * ERROR: * "I" demek "Lambda Init. Capture" demektir. Derleyici * yazacağı sınıfa yeni bir veri elemanı koyacak ve onu * da yukarıdaki "x" ile hayata getirecek. Burada yeni * veri elemanı "const" DEĞİLDİR. Fakat sınıfın üye * fonksiyonu olan ".operator()()" fonksiyonu "const" * olduğu için, "const" üye fonksiyon içerisinde sınıfın * veri elemanını değiştirmeye çalıştığımız için sentaks * hatası oluşacaktır. */ auto h = [x = x]()mutable{ ++x; }; // OK } puts("\n---"); { int x = 4; auto y = [x = x+1, &r = x](){ r+=2; return x*x; }(); std::cout << "y : " << y << '\n'; std::cout << "x : " << x << '\n'; } puts("\n---"); { auto f = [](int x = ++g_x){ return x*x; }; auto x = f(); auto y = f(); std::cout << x << ' ' << y << ' ' << g_x << '\n'; } puts("\n---"); { auto f = [i = 0]()->decltype(i){ return 1; }(); std::cout << std::is_same_v; } puts("\n---"); } * Örnek 1, #include template void my_lambda_handler(T f) { f(N); } int main() { /* # OUTPUT # 961 --- 961 --- 169 --- false true --- --- --- 11 11 12 11 12 13 --- 10 35 --- 1225 12.25 20.2 */ { [](int x){ std::cout << x*x << '\n'; }(31); // Immediately invoked function expression. } puts("\n---"); { /* const */ auto f = [](int x){ std::cout << x*x << '\n'; }; // Tavsiye edilen, "f" değişkeninin "const" olarak nitelenmesidir. f(31); } puts("\n---"); { auto f = [](int x){ std::cout << x*x << '\n'; }; // Tavsiye edilen, "f" değişkeninin "const" olarak nitelenmesidir. my_lambda_handler(f); } puts("\n---"); { auto f1 = [](){}; auto f2 = [](){}; std::cout << std::boolalpha << (std::is_same_v) << '\n'; auto f3 = f2; std::cout << std::boolalpha << (std::is_same_v) << '\n'; } puts("\n---"); { auto f = [](int x){ return x*x; }; // "f" is of type "closure" auto x = [](int x){ return x*x; }(45); // "x" is of type "int" } puts("\n---"); { auto f = [](int x)->double{ if(x < 10) return 5; else return 5.6; }; /* * Eğer burada "Trailing Return Type" kullanmasaydık, * sentaks hatası oluşacaktı. Çünkü derleyici ilgili * operator fonksiyonunun geri dönüş değeri için tür * çıkarımı yapmak istediğinde "ambiguity" oluşacaktır. * Fakat artık fonksiyonun geri dönüş değeri "double" * türünden olacaktır. Burada "promotion" vs. söz * konusu değildir. */ } puts("\n---"); { auto f1 = [](){ static int x = 10; ++x; std::cout << x << '\n'; }; f1(); auto f2 = [](){ static int x = 10; ++x; std::cout << x << '\n'; }; f2(); f2(); auto f3 = [](){ static int x = 10; ++x; std::cout << x << '\n'; }; f3(); f3(); f3(); } puts("\n---"); { // Since C++14 auto f = [](int x = 10){ std::cout << x << '\n'; }; f(); f(35); } puts("\n---"); { // Since C++14 auto f = [](auto x){ return x*x; }; std::cout << f(35) << '\n'; std::cout << f(3.5) << '\n'; /* * Buradaki parantez içerisinde "auto" yazarak aslında derleyiciye * diyoruz ki ilgili ".operator()()" fonksiyonunu "Member Template" * olarak yaz. Yani fonksiyon artık bir fonksiyon şablonu olacaktır. * Tabii fonksiyonun geri dönüş değeri de yine şablon parametresi "T" * olacaktır. Pekala bu "auto" anahtar kelimesi ile birlikte "&" * deklaratörünü ve "const" anahtar kelimesini de kullanabilirdik. */ auto g = [](auto a, auto b){ return a*b; }; std::cout << g(10, 2.02) << '\n'; } } * Örnek 2, #include int g = 5; int main() { /* # OUTPUT # */ { auto f = [](int x){ return x * g; }; std::cout << f(5) << '\n'; /* * Burada "static" ömürlü nesneleri doğrudan yukarıdaki * gibi kullanabiliriz. Çünkü bu tip nesneleri "capture" * edemeyiz. Sentaks hatası alıp almamak önemli değildir. */ } puts("-----\n"); { const int val{35}; auto f = [](int x){ return val + x; }; /* * Benzer şekilde "const" olan ve bir sabit ifadesi ile ilk değerini * alan yerel değişkenleri de "capture" etmeden kullanabiliriz. * "capture" etmeye kalkmamız durumunda sentaks hatası da oluşmayacaktır. */ } puts("-----\n"); { int val{35}; //auto f = [](int x){ return val + x; }; /* * Buradaki "val" değişkeni aslında "capture" EDİLMEMİŞTİR. */ } puts("-----\n"); { int a{}; double d{}; char c{}; float f{}; auto g = [a,d,c](int temp){ return a+d+c+temp;}; std::cout << g(100) << '\n'; /* * Burada sadece "a", "d" ve "c" değişkenleri "capture" edilmiştir. */ } puts("-----\n"); { int a{}; double d{}; char c{}; float f{}; auto g = [=](int temp){ return a+d+c+temp;}; std::cout << g(100) << '\n'; /* * Burada görülür bütün değişkenleri kopyalama yoluyla "capture" * edilmiştir. Fakat burada dikkatli olmamız gerekmektedir. */ } puts("-----\n"); { int a[4]{ 1,2,3,4 }; auto f = [a]{ /* * Burada "a" ismi bir gösterici değil, diziyi belirtmektedir. * "array-decay" MEYDANA GELMEMEKTEDİR. */ for(auto i : a) std::cout << i << ' '; std::cout << '\n'; }; f(); // Since C++14 auto g = [x = a]{ /* * Fakat burada "x" ismi artık bir göstericidir. "x=a" ifadesine * de "Lambda Init. Capture" denmektedir. Burada derleyicinin * yazdığı sınıfın veri elemanı olan "x", yukarıda dizi olarak * belirttiğimiz "a" ile ilk değerini almaktadır. */ std::cout << *(x + 0) << ' ' << *(x + 1) << ' ' << *(x + 2) << ' ' << *(x + 3) << '\n'; }; g(); } puts("-----\n"); { int x = 50; auto f = [x]{ // x = 67; // Derleyicinin yazdığı ".operator()()" fonksiyonu bir "const" // üye fonksiyon olduğundan, bu atama sentaks hatasıdır. }; f(); auto g = [x]()mutable{ x = 67; // Artık derleyicinin yazmış olduğu ".operator()()" fonksiyonu // "const" bir üye fonksiyon değildir. }; g(); } puts("-----\n"); { int x = 10; auto f = [&x]{ ++x; // Artık burada "capture-by-reference" yöntemi uygulanmıştır. // Derleyicinin yazdığı sınıfın veri elemanı "int&" türden olup, // "x" nesnesine bir referanstır. Ayrıca ilgili ".operator()()" // fonksiyonu hala "const" bir fonksiyondur. Fakat bizler sınıfın // veri elemanını değil, onun refere ettiği nesneyi değiştirdiğimiz // için HERHANGİ BİR SENTAKS HATASI SÖZ KONUSU DEĞİLDİR. }; f(); // Eğer buradaki çağrı ifadesi olmasaydı, "x" nesnesinin değeri // DEĞİŞMEYECEKTİ. } puts("-----\n"); { int a{}; double d{}; char c{}; float f{}; auto f1 = [&a, &d, c, f]{ // "a" ve "d" nesneleri "capture-by-reference" // "c" ve "f" neneleri "capture-by-copy" }; auto f2 = [=, &c] { // "c" nesnesi "capture-by-reference" // Görünür diğer nesneler "capture-by-copy" }; auto f3 = [=, &c, &d]{ // "c" ve "d" nesneleri "capture-by-reference" // Görünür diğer nesneler "capture-by-copy" }; auto f4 = [&]{ // Görünür bütün nesneler "capture-by-reference". }; auto f5 = [&, a, d]{ // "a" ve "d" nesneleri "capture-by-copy" // Görünür diğer nesneler "capture-by-reference" }; } } * Örnek 3, #include auto foo(int i) { /* (I) * Burada bizler "capture-by-reference" yaptığımız * için arka plandaki sınıfın veri elemanı "int&" * olacaktır. */ return [&](int x){ return i*x; }; } int main() { /* # OUTPUT # */ puts("-----\n"); { auto f = foo(31); // (II) // Fakat programın akışı buraya geldiğinde // artık ilgili sınıf yok edileceği için // "dangling reference" oluşacaktır. std::cout << f(10) << '\n'; // (III) // Artık buradaki çağrı "Tanımsız Davranış" // meydana gelecektir. } puts("-----\n"); } * Örnek 4, #include #include #include struct Nec{ int mx{}, my{}; void foo() { int z = 5; auto f = [](int val){ return val * z; // "z" ismini "capture" etmemiz gerekiyor. }; auto g = [/* this */](int val){ return val * my; // "my" ismini bir şekilde "capture" etmemiz gerekiyor. }; // Direkt olarak "[]" içerisine "my" yazarak da "capture" // edemeyiz. Dolayısıyla "this" göstericisini "capture" // etmeliyiz. Burada "this" göstericisini "capture-by-copy" // veya "capture-by-reference" ile "capture" etmemiz arasında // bir fark yoktur. auto h = [/* = */](int val){ return val * mx; // "mx" ismini bir şekilde "capture" etmemiz gerekiyor. // Yukarıdaki "this" çözümüne alternatif olarak "capture-by-copy" // yönmetini deneyebiliriz fakat bu yöntem C++20 ile "deprecated" // edildi. Çünkü aynı zamanda "this" göstericisi de kopyalanacaktır. // Dolayısıyla "++mx" ifadesi ile aslında ben sınıfın veri elemanının // değerini de değiştirmiş olacağım, "this" de kopyalandığı için. Bu // da sınıfın semantik yapısını bozacaktır. Fakat "=" yerine pekala // "&" kullanabiliriz, o "deprecated" EDİLMEDİ. }; /* Özetle; * Yukarıdaki "lambda" ifadelerinde "[]" arasında "this" yazmakla "=" ve "&" yazmak * arasında hiç bir fark yoktur. Hepsinde de "this" göstericisi "capture" edilmektedir. * Dolayısıyla "lambda" ifadesinin gövdesinde "mx" veya "my" değişkenlerinin değerini * değiştirmek, gerçekten de bu değişkenlerin değerini değiştirecektir. */ auto i = [copy_this = *this](){ copy_this.mx = 100; // Artık buradaki "copy_this", "*this" nesnesinin bir kopyası olacaktır. // Dolayısıyla bizler kopya üzerinde işlem yapmış olacağız. Fakat C++17 // ile birlikte "copy_this = *this" yerine sadece "*this" yazmamız da // yeterli gelecektir. }; } auto func() { auto f = [/* copy_this = *this */]{ //... }; /* * Şimdi böyle yaparsak, "this" göstericisi "capture" edileceği için * "Dangling Reference" oluşacaktır. Dolayısıyla bizler doğru bir şekilde * "capture" yapabilmek için "copy_this = *this" ifadesini kullanmalıyız. */ return f; } }; int main() { /* # OUTPUT # ----- dolu bos ----- */ puts("\n-----"); { auto uptr = std::make_unique(10'000, 'a'); //auto f = [uptr]{ // "std::unique_ptr" nesneleri kopyalamaya karşı // kapalı olması hasebiyle "capture-by-copy" yapılması // sentaks hatası oluşturacaktır. //}; auto g = [&uptr]{ // "uptr" nesnesi "capture-by-reference" edilmiştir. }; g(); std::cout << ( uptr ? "dolu" : "bos" ) << '\n'; auto h = [uptr = std::move(uptr)]{ // Burada ise taşıma semantiği uygulanmıştır. "std::move" // fonksiyonuna argüman olan isim "std::make_unique" // fonksiyonunun geri dönüş değerini sakladığımız isim // olurken, eşitliğin sol tarafındaki isim ise derleyicinin // yazdığı sınıfın veri elemanının ismi olacaktır. }; h(); std::cout << ( uptr ? "dolu" : "bos" ) << '\n'; } puts("\n-----"); { const int c = 41; auto f = [c = c]()mutable{ ++c; // Yukarıdaki "Lambda Init. Capture" ile artık arka // plandaki sınıfın veri elemanı "const" DEĞİL. // "mutable" anahtar sözcüğünden dolayı da yine sınıfın // ".operator()()" fonksiyonu da "const" DEĞİL. // Dolayısıyla böyle bir çağrı sentaks hatası DEĞİLDİR. }; } puts("\n-----"); { } } * Örnek 5, #include #include #include auto func() { auto f = [](auto&& ...args){ }; return f; } int main() { /* # OUTPUT # */ puts("\n-----"); { auto f = func(); f(); } puts("\n-----"); { auto f = [](int x){ return x*5; }; // "Lambda" ifadeleri, şartlara sağlıyorsa, // "constexpr" olarak betimlenirler velevki // bizler özel olarak "constexpr" anahtar // kelimesini kullanmamış olalım. C++17 ile // birlikte bu özellik dile eklenmiştir. constexpr auto val1 = f(12); // "val" değişkeninin değeri derleme zamanında hesap // edilmiş olacaktır. } puts("\n-----"); { auto g = [](int x){ static int cnt{}; ++cnt; return x*cnt; }; // Gövdesinde "static" ömürlü nesne taşıdığı için artık "constexpr" DEĞİL. constexpr auto val2 = g(12); // Dolayısıyla burada sentaks hatası meydana gelecektir. auto val3 = g(12); // Ama burası hala OK. } puts("\n-----"); { auto g = [](int x)constexpr{ static int cnt{}; ++cnt; return x*cnt; }; // "constexpr" olma koşulu çiğnendiği için artık sentaks hatası burada meydana gelecektir. auto val3 = g(12); } } * Örnek 6, #include #include int main() { puts("\n-----"); { auto fsquare = [](auto val){ return val*val; }; // I std::array a1; std::cout << a1.size() << '\n'; auto f1 = [](int x){ // II static int cnt = 0; ++cnt; return x*cnt; }; // std::array a2; // ERROR: call to non-‘constexpr’ function std::cout << f1(20) << '\n'; auto f2 = [](int x)constexpr{ // III static int cnt = 0; // ERROR: ‘cnt’ declared ‘static’ in ‘constexpr’ function ++cnt; return x*cnt; }; } puts("\n-----"); } * Örnek 7, #include #include int main() { puts("\n-----"); { auto f = [](int x){ return x*x; }; std::boolalpha(std::cout); constexpr auto b = noexcept(f(345)); // Buradaki "f" bir "constexpr" fakat "noexcept" DEĞİL. // Çünkü varsayılan durumda "noexcept" olarak NİTELENMEMEKTE. std::cout << b << '\n'; } puts("\n-----"); } /*================================================================================================================================*/ (13_06_08_2023) > "Advanced Lambda Expression" (devam): * Örnek 1, #include #include void* operator new(std::size_t sz) { std::cout << "operator new called. size: " << sz << '\n'; if(sz == 0) ++sz; if(void* ptr = std::malloc(sz)) return ptr; throw std::bad_alloc{}; } class Myclass{ public: unsigned char buffer[512]{}; }; int main() { puts("\n-----"); { auto fn = [](double d ){ return d*d+3; }; std::cout << "Sizeof : " << sizeof(decltype(fn)) << '\n'; // Sizeof: 1 std::function f = fn; // After CTAD // std::function f = fn; // Before CTAD std::cout << "Sizeof : " << sizeof(f) << '\n'; // Sizeof : 32 std::cout << fn(5.863) << ' ' << f(5.863) << '\n'; // 37.3748 37.3748 } puts("\n-----"); { Myclass m; auto fn = [m](double d ){ return d*d+.3; }; std::cout << "Sizeof : " << sizeof(decltype(fn)) << '\n'; // Sizeof : 512 } puts("\n-----"); { Myclass m; auto fn = [m](double d ){ return d*d+.3; }; std::function f = fn; // // operator new called. size: 512 std::cout << "Sizeof : " << sizeof(f) << '\n'; // Sizeof : 32 } } * Örnek 2, #include #include #include #include #include #include #include "MyUtility.h" using namespace MyUtility::Utility; int main() { puts("\n-----"); { auto get_size = [](const auto& c){ return std::size(c); }; std::vector ivec(30); std::list my_list{ 34.43, 56.65 }; int ar[20]{ 100 }; std::cout << get_size(ivec) << ' ' << get_size(my_list) << ' ' << get_size(ar) << '\n'; } puts("\n-----"); { std::vector> pvec; for(std::size_t i = 0; i < 1000; ++i){ pvec.emplace_back(std::pair{ rname(), rtown() }); } std::sort( pvec.begin(), pvec.end(), // [](const auto& x, const auto& y) { [](const std::pair& x, const std::pair& y){ return std::pair{x.second, x.first} < std::pair{y.second, y.first}; } ); for(const auto& [name, town]: pvec){ std::cout << name << ' ' << town << '\n'; } } } * Örnek 3, #include #include #include #include #include void f1(std::vector>& svec) { std::sort( begin(svec), end(svec), [](const std::shared_ptr& a, const std::shared_ptr& b){ return *a < *b; } ); for_each( begin(svec), end(svec), [](const std::shared_ptr& sp){ std::cout << *sp << '\n'; } ); std::cout << '\n' << '\n'; } void f2(std::vector>& svec) { std::sort(begin(svec), end(svec), [](const auto& a, const auto& b){ return *a < *b; }); for_each(begin(svec), end(svec), [](const auto& sp){ std::cout << *sp << '\n'; }); std::cout << '\n' << '\n'; } int main() { puts("\n-----"); { std::vector> svec; svec.emplace_back(new std::string{"yesim"}); svec.push_back(std::make_shared("berk")); svec.emplace_back(new std::string{"alican"}); f1(svec); f2(svec); } puts("\n-----"); } * Örnek 4, #include #include #include #include #include void foo(std::string&) { std::cout << "L-Value Reference\n"; } void foo(const std::string&) { std::cout << "const L-Value Reference\n"; } void foo(std::string&&) { std::cout << "R-Value Reference\n"; } int main() { puts("\n-----"); { auto fn = [](auto&& s){ foo(std::forward(s)); }; std::string name{"ali"}; fn(name); // L-Value Reference const std::string surname{"veli"}; fn(surname); // const L-Value Reference fn(std::move(name + surname)); // R-Value Reference } puts("\n-----"); } * Örnek 5, #include #include #include #include #include template void print(Args&& ...args) { std::initializer_list{((std::cout << std::forward(args) << '\n'), 0)...}; } int main() { puts("\n-----"); { print(2, 5.6, "aylin", std::string{"mustafa"}); } puts("\n-----"); { auto fn = [](auto&& ...args){ print(std::forward(args)...); }; fn(std::string{"mustafa"}, "aylin", 6.5, 2); } puts("\n-----"); } * Örnek 6, #include #include #include struct Baz{ std::string m_s; auto foo()const { return [t_s=m_s]{ std::cout << t_s << '\n'; }; } }; int main() { puts("\n-----"); { const auto f1 = Baz{"abc"}.foo(); const auto f2 = Baz{"xyz"}.foo(); f1(); f2(); } puts("\n-----"); } * Örnek 7, #include #include #include #include int main() { puts("\n-----"); { std::vector svec(1000); //... std::string name{ "ahmet" }; std::find_if(svec.begin(), svec.end(), [&name](const std::string& s){ return s == name + "can"; }); /* * Burada döngünün her bir turu için "name" nesnesi ile "can" toplanacaktır. */ std::find_if(svec.begin(), svec.end(), [name = name + "can"](const std::string& s){ return s == name; }); /* * Burada ise yukarıdaki toplama işlemi sadece bir kez yapılacaktır. Böylelikle ciddi bir verim kaybının * önüne geçilmiş olabilir. */ } puts("\n-----"); } * Örnek 8, #include #include #include #include #include template void func(F&& f) { if constexpr (std::is_nothrow_invocable_v) std::cout << "no throw!\n"; else std::cout << "may throw!\n"; } int main() { puts("\n-----"); { auto fn = [](int x){ return x*5; }; func(fn); } puts("\n-----"); { auto fn = [](int x)noexcept{ return x*5; }; func(fn); } puts("\n-----"); } * Örnek 9, #include // A C-API void foo(int(*func_ptr)(int)) { std::cout << func_ptr(30) << '\n'; } int main() { puts("\n-----"); { auto fn = [](int x){ return x*5; }; // It must be a "Stateless Lambda". int(*fn_ptr)(int) = fn; // "Stateless Lambdas" can be converted to function addresses implicitly. std::cout << fn_ptr(30) << ' ' << fn(30) << '\n'; } puts("\n-----"); { foo([](int x){ return x*5; }); } puts("\n-----"); } * Örnek 10.0, Anımsanacağı üzere işaret operatörü "+", bir ifade içerisinde kullanıldığında o ifade "L-Value" değil, "R-Value" olur. Aynı zamanda bu operatör "integral promotion" a neden olmaktadır. Bir diğer özellik ise bu operatörün operandı bir gösterici olabilmektedir. Bu da şöyle bir deyimin kapısını aralamaktadır: -> "Stateless Lambda" ifadeler, "function pointer" türüne dönüşmektedir. Eğer bizler "stateless lambda" ifadelerini işaret operatörü "+" nın operandı yaparsak, derleyici bu durumda "explicit" olmayan tür dönüştürme operatör fonksiyonunu çağırması gerekmektedir. Bu durumda da "stateless lambda" ifadesi karşılığı yazılan sınıfın fonksiyon göstericisine dönüştüren tür dönüştürme operatör fonksiyonu çağrılacaktır. Dolayısıyla işaret operatörünün operandı bir "stateless lambda" olduğunda, arka planda fonksiyon göstericisine dönüştüren tür dönüştürme operatör fonksiyonu çağrılacaktır. int main() { puts("\n-----"); { auto fn = [](int x){ return x*5; }; // "fn" is of "closure type". auto fp = +[](int x){ return x*5; }; // "fp" is a function pointer now. // int (*fp)(int) = +[](int x){ return x*5; }; } puts("\n-----"); } Böylelikle bir ifadeyi ister "closure type" ister "function pointer" olarak kullanma hakkı elde etmiş oluyoruz. Benzer mekanizma "template" ler söz konusu olduğunda da işletilmektedir. Şöyleki: #include template void func(T x) { if constexpr (std::is_same_v) std::cout << "Type: function pointer\n"; else std::cout << "Type: Closure\n"; } int main() { puts("\n-----"); { func([](int i){ return i*i; }); // Type: Closure func(+[](int i){ return i*i; }); // Type: function pointer } puts("\n-----"); } * Örnek 10.1, #include #include #include int main() { /* # OUTPUT # ----- 0x55f8181142e0, 0x55f818114300 0x55f818114380, 0x55f8181143a0 0x55f818114330, 0x55f818114350 ----- 0x7ffdb7a45f80, 0x7ffdb7a45fa0: one = 1 0x7ffdb7a45f80, 0x7ffdb7a45fa0: three = 3 0x7ffdb7a45f80, 0x7ffdb7a45fa0: two = 2 ----- 0x55f8181142e0, 0x55f818114300: one = 1 0x55f818114380, 0x55f8181143a0: three = 3 0x55f818114330, 0x55f818114350: two = 2 ----- */ puts("\n-----"); std::map numbers{ {"one", 1}, {"two", 2}, {"three", 3} }; // Print Addresses for(auto mit = numbers.cbegin(); mit != numbers.cend(); ++mit) std::cout << &mit->first << ", " << &mit->second << '\n'; puts("\n-----"); { /* Each time entry is copied from pair! * Bunun yegane sebebi, "std::map" içerisinde "std::pair" * öğeleri şu şekilde tutulmaktadır: std::pair * Fakat bizler ilgili "callable" için "const std::pair&" * bildirdiğimiz için arka planda "std::pair" için bir kopyalama * gerçekleşmektedir. */ std::for_each( std::begin(numbers), std::end(numbers), [](const std::pair& entry){ std::cout << &entry.first << ", " << &entry.second << ": " << entry.first << " = " << entry.second << '\n'; } ); } puts("\n-----"); { /* This time entries are not copied, they have the same addresses. * Artık "auto" kullandığımız için doğru şekilde tür çıkarımı * yapılmaktadır. */ std::for_each( std::begin(numbers), std::end(numbers), [](const auto& entry){ std::cout << &entry.first << ", " << &entry.second << ": " << entry.first << " = " << entry.second << '\n'; } ); } puts("\n-----"); } * Örnek 11, "IIFE: Immediately Invoked Function Expression": #include #include #include int func(int a, int b) { int value = a*b; //... return value; } int main() { puts("\n-----"); { /* * "const" nesneleri çöp değer ile hayata getiremiyoruz ve bu nesnelere * ilk değer verirken de bir takım hesaplamaların yapılmasını istiyoruz. */ int a = 34; int b = 43; const int c = [=]{ int value = a*b; //... return value; }(); std::cout << "<" << a << "," << b << "," << c << ">\n"; // <34,43,1462> } puts("\n-----"); { int a = 34; int b = 43; const int c = func(a, b); std::cout << "<" << a << "," << b << "," << c << ">\n"; // <34,43,1462> } } * Örnek 12, "IIFE: Immediately Invoked Function Expression": #include struct Foo{ int baz; Foo(int bar) : baz{ [&]{ /* Complex Init. of "baz". */ }() }{} }; int main() { //... } * Örnek 13, #include #include int main() { int x = 6, y = 12; puts("\n-----"); { const int a = std::invoke( [&]{ //... auto value = x+y; ++value; return value; } ); std::cout << "a : " << a << '\n'; } puts("\n-----"); { const int a = [&]{ //... auto value = x+y; ++value; return value; }(); std::cout << "a : " << a << '\n'; } puts("\n-----"); } * Örnek 14, #include #include class Nec{ public: Nec() { // Bu "static" değişken yalnızca sınıfın "ctor." fonksiyonu çağrıldığında // hayata gelecektir. "static" olması hasebiyle de yalnızca bir defa hayata // gelecektir. Ayrıca "thread_safe". static auto _{ [](){ std::cout << "Bu kod yalnizca bir kez calistirilmali.\n"; return 0; }() }; } }; int main() { puts("\n-----"); { Nec m1, m2, m3, m4; } puts("\n-----"); } * Örnek 15, #include #include #include #include int f(int) { putchar('i'); return 13; } int f(double) { putchar('d'); return 1.3; } int f(long) { putchar('l'); return 13l; } int main() { /* # OUTPUT # ----- ----- iiiiiiiiii ----- iiiiiiiiii ----- iiiiiiiiii ----- */ puts("\n-----"); std::vector xvec(10, 3); std::vector yvec(10); /* * Aşağıdaki "transform" fonksiyonları, ilk iki argümanın oluşturduğu "range" * içerisindeki öğeleri, dördüncü argümanındaki "callable" nesnesinde gönderip, * geri dönüş değerlerini de üçüncü argümandaki "range" içerisine yazmaktadır. */ { // I: error: no matching function for call to // ‘transform(std::vector::iterator, std::vector::iterator, std::vector::iterator, )’ // Yani "overload resolution" YAPILAMAMAKTADIR. // std::transform(xvec.begin(), xvec.end(), yvec.begin(), f); } puts("\n-----"); { // II: "callable" için, fonksiyonun adresini, çağrılmasını istediğimiz fonksiyonun // adresine dönüştürmemiz gerekmektedir. std::transform(xvec.begin(), xvec.end(), yvec.begin(), static_cast(f)); } puts("\n-----"); { // III: "callable" için, fonksiyonun adresini, çağrılmasını istediğimiz fonksiyonun // adresine dönüştürmemiz gerekmektedir. Burada ise "C-Style Casting" uygulanmıştır. std::transform(xvec.begin(), xvec.end(), yvec.begin(), (int(*)(int))f); } puts("\n-----"); { // IV: "callable" için direkt olarak bir "generic lambda" ifadesinin kullanılmasıdır. std::transform(xvec.begin(), xvec.end(), yvec.begin(), [](auto a){ return f(a); }); } puts("\n-----"); } * Örnek 16, #include #include #include #include void f(int) { putchar('i'); } void f(double) { putchar('d'); } void f(long) { putchar('l'); } int main() { /* # OUTPUT # ----- ----- iiiiiiiiiiiiiiiiiiii ----- iiiiiiiiiiiiiiiiiiii ----- iiiiiiiiiiiiiiiiiiii ----- */ puts("\n-----"); std::vector xvec(20); /* * Aşağıdaki "transform" fonksiyonları, ilk iki argümanın oluşturduğu "range" * içerisindeki öğeleri, dördüncü argümanındaki "callable" nesnesinde gönderip, * geri dönüş değerlerini de üçüncü argümandaki "range" içerisine yazmaktadır. */ { // I: error: no matching function for call to // ‘for_each(std::vector::iterator, std::vector::iterator, )’ // Yani "overload resolution" YAPILAMAMAKTADIR. // std::for_each(std::begin(xvec), std::end(xvec), f); } puts("\n-----"); { // II: "callable" için, fonksiyonun adresini, çağrılmasını istediğimiz fonksiyonun // adresine dönüştürmemiz gerekmektedir. std::for_each(std::begin(xvec), std::end(xvec), static_cast(f)); } puts("\n-----"); { // III: "callable" için, fonksiyonun adresini, çağrılmasını istediğimiz fonksiyonun // adresine dönüştürmemiz gerekmektedir. Burada ise "C-Style Casting" uygulanmıştır. std::for_each(std::begin(xvec), std::end(xvec), (void(*)(int))f); } puts("\n-----"); { // IV: "callable" için direkt olarak bir "generic lambda" ifadesinin kullanılmasıdır. std::for_each(std::begin(xvec), std::end(xvec), [](auto a){ return f(a); }); } puts("\n-----"); } * Örnek 17, C++20 ile birlikte "Stateless Lambda" ifadeleri ile oluşan sınıfların "default ctor." ve "copy assignment" fonksiyonları "delete" olmaktan çıkmıştır. Yine "Unevaluated Context" içerisinde "Stateless Lambda" kullanımı mümkün kılınmıştır. Fakat "Statefull Lambda" ifadeleri her iki kullanımda biçiminde de sentaks hatasına yol açmaktadır. #include #include #include #include #include #include int main() { /* # OUTPUT # */ puts("\n-----"); { auto f = [](int x){ return x*5; }; // A "Stateless Lambda" auto g = f; // Legal decltype(f) x; // ERROR until C++20: "Default Ctor." is "deleted". Since C++20, it is legal. f = g; // ERROR until C++20: "Copy Assignment Opt." is "deleted". Since C++20, it is legal. } puts("\n-----"); { std::set x; // std::set> x; std::set> y; } puts("\n-----"); { auto f = [](int a, int b){ return std::abs(a) < std::abs(b); }; // A "Stateless Lambda" { // Until C++20 std::set z(f); } { // Since C++20 std::set q; } /* * Arka planda "std::set" sınıfının "ctor." fonksiyonu, ilgili * "closure type" sınıfının "default ctor." fonksiyonunu çağırmakta. * Fakat C++20 öncesinde bu fonksiyon "delete" edildiğinden sentaks * hatası oluşacaktır. Dolayısıyla C++20 öncesinde "copy ctor." * fonksiyonuna çağrı yapılması gerekmektedir. C++20 ile birlikte * "closure type" sınıfının "default ctor." fonksiyonu çağrılabilir * olduğu için, "copy ctor." fonksiyonuna çağrıda bulunma zorunluluğu * ortadan kalkmıştır. Tabii bu sadece "set" sınıfı için de geçerli * değil, "map" ve benzeri sınıflar için de geçerlidir. */ } puts("\n-----"); { int x{}; //auto f = [x](int a, int b){ return std::abs(a) < std::abs(b); }; // (*) //std::set z(f); //std::set q; /* * "f" bir "Stateless Lambda" olmadığı için C++20'de bile sentaks hatasıdır. */ } puts("\n-----"); { int xxx{}; constexpr auto sz = sizeof([xxx](int x){ return x*5; }); std::cout << sz << '\n'; /* * C++20 ile birlikte yukarıdaki "Lambda" ifadesi sentaks hatası * olmaktan çıkmıştır. Burada hem "Stateless" hem de "Statefull" * olacak şekilde kullanabiliriz. */ decltype([xxx](int x){ return x*5; }) f; std::cout << f(213) << '\n'; /* * Böylesi bir kullanım ise, "Stateless Lambda" olmadığı için, C++20'de * bile sentaks hatasıdır. */ } puts("\n-----"); { // Since C++20 std::setb; })> my_set{ 4, 7, 2, 4, 1, 9, 3, 34 }; } } * Örnek 18, #include #include int main() { /* # OUTPUT # ----- 0x55f70b81e2c0 adresindeki nesne siliniyor... ----- 0x55f70b81e2c0 adresindeki nesne siliniyor... ----- */ puts("\n-----"); { // Until C++20 auto f = [](int* ptr){ std::cout << ptr << " adresindeki nesne siliniyor...\n"; delete ptr; }; std::unique_ptr u_ptr{ new int{123}, f }; } puts("\n-----"); { // Since C++20 std::unique_ptr u_ptr{ new int{123} }; } puts("\n-----"); } * Örnek 19, #include #include #include template void print(const C& con) { for(const auto& elem: con) std::cout << elem << " "; std::cout << '\n'; } int main() { /* # OUTPUT # ----- ayse bilal cemil murat nur sumeyye zehra zehra sumeyye nur murat cemil bilal ayse nur ayse zehra sumeyye -75 -8 -5 -3 9 12 40 77 -3 -5 -8 9 12 40 -75 77 ----- */ puts("\n-----"); { std::set names = { "ayse", "zehra", "murat", "bilal", "sumeyye", "nur", "cemil" }; print(names); using gset = std::set r; })>; gset gnames = { "ayse", "zehra", "murat", "bilal", "sumeyye", "nur", "cemil" }; print(gnames); using lenset = std::set; lenset namelens = { "ayse", "zehra", "murat", "bilal", "sumeyye", "nur", "cemil" }; print(namelens); std::set iset = { 9, -3, 12, -5, 40, -8, 77, -75 }; print(iset); using abs_set = std::set; abs_set abs_values = { 9, -3, 12, -5, 40, -8, 77, -75 }; print(abs_values); } puts("\n-----"); } * Örnek 20, int main() { /* # OUTPUT # */ puts("\n-----"); { auto f = [](int a) [[nodiscard]] { return a*a; }; // "attribute" nesnesini bu şekilde kullanmak hala sentaks hatasıdır. } puts("\n-----"); } * Örnek 21, int main() { /* # OUTPUT # */ puts("\n-----"); { int x{ 31 }; auto f = [x]mutable{ return ++x; }; // Since C++23 // "mutable", "trailing return type", "noexcept" vb. şeyler kullandığımız zaman // "()" çiftini yazmak zorundayız. Fakat böyle şeyler kullanmıyorsak // ve fonksiyon da argüman almayacaksa, "()" çiftini yazmamıza lüzum // yoktur. C++23 ile aslında "mutable", "trailing return type" gibi // şeyler kullanıyorsak fakat fonksiyon argüman almıyorsa, "()" yazmaya // gerek kalmamıştır. auto g = [x]()mutable{ return ++x; }; } puts("\n-----"); } * Örnek 22, "Familiar Template Syntax": C++20 ile gelen ve "Generic Lambda" ifadelerinde şablon parametresini doğrunda "template" sentaksı ile yazılabilmesidir. #include int main() { /* # OUTPUT # ----- ----- 3 3.5 3 3.5 3 ----- */ puts("\n-----"); { int x{ 31 }; // Fonksiyon şablonlarında olan özellikleri arık "lambda" ifadelerinde de kullanabiliriz. auto f = [] /* typename, class T */ ( /* T x */ ) /* constexpr, mutable, , noexcept, consteval */ {}; } puts("\n-----"); { auto f1 = [](int x, int y){ return x+y; }; std::cout << f1(2, 1.5) << '\n'; // "Narrowing Conversion" from "double" to "int". auto f2 = [](auto x, auto y){ return x+y; }; std::cout << f2(2, 1.5) << '\n'; auto f3 = [](auto x, decltype(x) y){ return x+y; }; std::cout << f3(2, 1.5) << '\n'; auto f4 = [](T x, T y){ return x+y; }; //std::cout << f4(2, 1.5) << '\n'; // ERROR: (ambiguity) no match for call to ‘(main()::) (int, double)’ std::cout << f4(2., 1.5) << '\n'; std::cout << f4(2, 1) << '\n'; } puts("\n-----"); } * Örnek 23, #include #include int main() { /* # OUTPUT # ----- 100 100 ----- */ puts("\n-----"); { auto fn = [](const std::vector& vec){ return vec.size(); }; std::vector ivec(100, 0); std::vector dvec(100, 0.); //... std::cout << fn(ivec) << '\n'; std::cout << fn(dvec) << '\n'; } puts("\n-----"); } * Örnek 24, #include #include int main() { /* # OUTPUT # ----- 100 100 ----- */ puts("\n-----"); { auto fn = [](const std::vector& vec, const std::vector& y){ return vec.size() < y.size(); }; std::vector ivec(100, 0); std::vector dvec(100, 0.); //std::cout << std::boolalpha << fn(ivec, dvec) << '\n'; // ambiguity std::vector ivecc(1000, 0); std::cout << std::boolalpha << fn(ivec, ivecc) << '\n'; // true } puts("\n-----"); } * Örnek 25, #include #include int main() { /* # OUTPUT # ----- 11 12 13 16 19 18 17 14 15 ----- */ puts("\n-----"); { auto fn = [](T(&ra)[n]){ for(auto& t : ra) t += 10; }; int a[]{ 1, 2, 3, 6, 9, 8, 7, 4, 5 }; fn(a); for(auto i : a) std::cout << i << ' '; } puts("\n-----"); } * Örnek 26, #include template void foo(T) { std::cout << typeid(T).name() << '\n'; } int main() { /* # OUTPUT # ----- d i i ----- d i i ----- */ puts("\n-----"); { auto fn = [](auto&& x){ foo(std::forward(x)); }; fn(3.1); int x{ 31 }; fn(x); fn(std::move(x)); } puts("\n-----"); { auto gn = [](T&& x){ foo(std::forward(x)); }; gn(3.1); int x{ 31 }; gn(x); gn(std::move(x)); } puts("\n-----"); } * Örnek 27, #include #include #include template void foo(Args&& ...args); int main() { /* # OUTPUT # */ puts("\n-----"); { auto fpush = [](std::vector& x, const T& value){ x.push_back(value); }; std::std::vector ivec; fpush(ivec, 20); } puts("\n-----"); { auto call_foo = [](Args&& ...args) { foo(std::forward(args)...); }; } puts("\n-----"); { auto fn = [](auto x){ return x*x; }; fn.operator()(3); // ".operator()" fonksiyonu için tür çıkarımı "double" // yönünde yapılacak fakat argüman olarak "3" değeri // gönderilecektir. } puts("\n-----"); { auto fn = [](){ int a[n]; return a; }; fn.operator()<20>(); // Çünkü burada şablon olan fonksiyon ".operator()" fonksiyonu. // Sınıfın kendisi bir şablon DEĞİLDİR. } } * Örnek 28, class Myclass{ public: void foo() { int a = 10; auto f = [=, this]{ return a * (mx + my) }; // Invalid in C++17, valid in C++20 } private: int mx, my; }; //... * Örnek 29, #include template void foo(Args ...) { std::cout << sizeof...(Args) << '\n'; } template void func(Args ...args) { auto f = [/* =, &, args..., &args... */](){ foo(args...); }; f(); } int main() { /* # OUTPUT # */ puts("\n-----"); { func( 2, 5.6, "Ali" ); } puts("\n-----"); } * Örnek 30, #include #include #include #include // Until C++20 template auto delay_invoke_foo(Args... args) { return [tup = std::make_tuple(std::move(args)...)]()->decltype(auto){ return std::apply( [const auto&... args]->decltype(auto){ return foo(args...); }, tup ); }; } // Since C++20 template auto delay_invoke_foo(Args... args) { return [...args = std::move(args)]()->decltype(auto){ return foo(args...); }; } int main() { /* # OUTPUT # */ puts("\n-----"); { func( 2, 5.6, "Ali" ); } puts("\n-----"); } Hatırlatıcı Notlar: >> C++17 ile C++20 arasındaki farklı görmek için: https://open-std.org/jtc1/sc22/wg21/docs/papers/2020/p2131r0.html /*================================================================================================================================*/ (14_12_08_2023) > Formatlı Çıkış İşlemleri: Anımsayacağınız üzere C dilinden gelen üç adet çıkış akımına yazı yazmada kullanılan fonksiyon bulunmaktadır. Bunlar "printf", "sprintf" ve "fprintf". Bu fonksiyonların prototipleri ise aşağıdaki gibidir: int printf(const char* p, ...); int sprintf(char* b, const char* p, ...); int fprintf(FILE* f, const char* p, ...); Buradaki üç fonksiyonun geri dönüş değeri, yazdığı toplam karakter adedidir. "print" direkt olarak "stdout" akımına yazarken "sprintf" ve "fprintf" ise birinci parametrelerine geçilen adrese yazmaktadır. Şimdi de bu fonksiyonların dezavantajlarına bakalım: -> Bu üç fonksiyonun, daha doğrusu bu fonksiyonlar gibi "variadic" fonksiyonların, en büyük dezavantajı "type-safe" olmamalarıdır. Çünkü bu fonksiyonlar argüman olarak hangi türden argüman alacaklarını biliyor ve programcının da bu türden argüman göndereceğine güvenmektedir. -> C dilindeki "variadic" fonksiyonların bir diğer problemi de ilgili fonksiyona toplamda kaç adet argüman gönderildiğini yine bir şekilde fonksiyona argüman göndererek belirtmem gerekmektedir. Örneğin, ilk argüman olarak toplam öğe sayısının argüman olarak gönderilmesi veya "variadic" argümanlar içerisine "sentinel" bir argüman da göndererek bu argüman ile karşılaşıldığında işlemlerin durmasını sağlatabiliriz. -> Diğer yandan bu fonksiyonlar "custom" türler için "extendable" değiller, yani bu fonksiyonları "custom" türler için düzeltemiyoruz. Bu fonksiyon grubunun avantajları ise şunlardır: -> Bu fonksiyon grubu, C++ dilindeki diğer formatlı giriş çıkış fonksiyonlarına nazaran daha hızlı çalışmaktadır. -> Argümanlar ile formatlama özelliklerini birbirinden ayırabiliyoruz. Örneğin, "print" fonksiyonunda "Conversion Specifier" ile argümanları ayrı ayrı görürüz. Şimdi de C++ dilindekilere bakalım: -> En büyük dezavantaj, "iostream" kütüphanesinin hantal ve kullanımının "verbose" olmasıdır. Fakat "custom" türler için ilgili fonksiyonlar özelleştirilebiliyor ve fonksiyonların "type-safe" dir. Dolayısıyla C dilinden kazanacağımız hıza karşılık C++ dilinde güvenlilik ve işlevsellik gelmektedir. İşte bu iki dünyanın güzel taraflarını bünyesinde barındıran sınıf ise "std::format" sınıfıdır. C++20 ile dile eklenmiştir. Bu kütüphanedeki ilk fonksiyon "std::format" fonksiyonudur. Bu fonksiyon argüman olarak "std::string_view" türünden bir nesne alır ve "std::string" türünden nesne döndürür. Ancak bu fonksiyonun aldığı ifadenin değeri derleme zamanında hesaplanabilir bir değer olmalıdır (değeri çalışma zamanında hesaplanacak olan nesneler için başka bir fonksiyon kullanacağız). Bu fonksiyonu kullanırken "format specifier" için "{}" karakterlerini kullanmalıyız. Tıpkı C dilindeki "printf" fonksiyonunda kullandığımız "%" ile başlayan karakterler gibi. Pekiyi nasıl formatlama yapacağız? İşte bunun için şu bileşimi kullanabiliriz: [[fill]align] [sign][#][0][width][.precision][type] ^ ^ ^ ^ ^ ^ ^ ^ I II III IV V VI VII VIII -> I: Doldurma karakterini belirtmektedir. Yazma alanı genişliği, yazılacak alandan büyükse, o boş kalan alana yazılacak karakterler için burada belirtilen karakter kullanılacaktır. Varsayılan değer boşluk karakteridir. -> II: Sağa dayalı, sola dayalı ya da merkezi konumlandırma bilgisini belirtmektedir. -> IIII: Tam sayıların yazılmasında işaretin yazılıp yazılmayacağını belirtmektedir. -> IV: "Number Sign", doğrudan kendisinin bir özelliği yok. En sondaki "type" ile birlikte kullanıldığında onu "modify" etmektedir. -> V: "Heading Zero", tam sayıların yazımında yazma alanı genişliği öncesinde yazılacak karakteri belirtmektedir. Yani yazma alanının başlangıç yeri ile yazı arasındaki karakterlerdir. Doldurma karakteri ise yazı ile yazma alanının bittiği yer arasındaki karakterler içindir. -> VI: Yazma alanı genişliğini belirtmektedir. -> VII: Gerçek sayılar için "." karakterinden sonra kaç basamak yazılacağını belirtmektedir. -> VIII: Yazdırılmak istenen sayının tür bilgisini belirtmektedir. Tam sayı mı gerçek sayı mı vs. Buradaki bileşenleri "{}" içerisine yazıyoruz fakat bunlardan önce ":" karakterini yazmak zorundayız. Eğer varsayılan değerleri kullanmak istiyorsak, "{}" içerisini boş bırakabiliriz. Eğer formatlamada bir hata oluşursa, bir "exception" gönderilecektir. Pekala formatlama yaparken de "{}" içerisine rakamlar yazarak "Positional Placeholder" mekanizmasını işletebiliriz. Şöyleki: * Örnek 1, #include #include #include // [[fill]align] [sign][#][0][width][.precision][type] int main() { std::string name{ "necati" }; int n{ 7 }; // "formatted_string" is of type "std::string" // The default values to format is used. auto formatted_string{ std::format("{} bugun {} adet kitap satin aldi.\n", name, n) }; std::cout << formatted_string; // "{0}" refers to "n" // "{1}" refers to "name" formatted_string = std::format("{1} bugun {0} adet kitabi {1} icin satin aldi.\n", n, name); std::cout << formatted_string; for (size_t i = 0; i < 128; i++) { // "{0}" refers to "i" // "{0:#X}" refers to the hexadecimal representation of "i". // "{0:c}" refers to the character representation of "i". formatted_string = std::format("{0:d} Index => {0:#X} {0:c}\n", i); std::cout << formatted_string; } } * Örnek 2, #include #include #include // [[fill]align] [sign][#][0][width][.precision][type] int main() { int x = 4359; auto formatted_string{ std::format("|{{{}}}|", x) }; std::cout << formatted_string << '\n'; // |{4359}| formatted_string = std::format("|{:12}|", x); // Sadece yazma alanı genişliği "12" karakter olarak // belirlendi. Diğerleri varsayılan değer olarak kullanıldı. std::cout << formatted_string << '\n'; // | 4359| formatted_string = std::format("|{:_>12}|", x); // "fill" olarak "_" karakteri, "align" için de ">" yazıldı. // Böylelikle yazı sağa dayalı olarak yazılacak ve doldurma // karakteri olarak da "_" karakteri kullanılacak. std::cout << formatted_string << '\n'; // |________4359| formatted_string = std::format("|{:$<12}|", x); // "fill" olarak "$" karakteri, "align" için de "<" yazıldı. // Böylelikle yazı sola dayalı olarak yazılacak ve doldurma // karakteri olarak da "$" karakteri kullanılacak. std::cout << formatted_string << '\n'; // |4359$$$$$$$$| formatted_string = std::format("|{:*^12}|", x); // "fill" olarak "*" karakteri, "align" için de "^" yazıldı. // Böylelikle yazı ortaya dayalı olarak yazılacak ve doldurma // karakteri olarak da "*" karakteri kullanılacak. std::cout << formatted_string << '\n'; // |****4359****| } * Örnek 3, #include #include #include #include // [[fill]align] [sign][#][0][width][.precision][type] int main() { /* # OUTPUT # Yazma alani genisligi: 8 | 76234| |_76234__| |123456789| |_123456_| */ int width; std::cout << "Yazma alani genisligi: "; std::cin >> width; int x = 76234; auto s{ std::format("|{:{}}|\n", x, width) }; // Burada formatlama varsayılan değerler ile sağlanmıştır. std::cout << s; // | 76234| s = std::format("|{:_^{}}|\n", x, width); // Burada ise doldurma karakteri olarak "_", hizalama olarak da // ortalama seçilmiştir. std::cout << s; // |_76234__| x = 123456789; s = std::format("|{:_^{}}|\n", x, width); // Burada ise doldurma karakteri olarak "_", hizalama olarak da // ortalama seçilmiştir. Yazma alanının büyüklüğü ilgili yazıdan // küçükse budamaya neden olmaz. std::cout << s; // |123456789| x = 123456; s = std::format("|{1:_^{0}}|\n", width, x); // Burada ise doldurma karakteri olarak "_", hizalama olarak da // ortalama seçilmiştir. "{0}" için "width", "{1}" için "x" değişkeni // yazılacaktır. std::cout << s; // |_123456_| } * Örnek 4, #include #include #include #include // [[fill]align] [sign][#][0][width][.precision][type] int main() { /* # OUTPUT # | 345| |XXXXXXXX 345| | +345| | 345| | -345| |XXXXXXXX-345| | -345| | -345| */ int x = 345; auto s = std::format("|{:>12}|\n", x); std::cout << s; s = std::format("|{:X> 12}|\n", x); std::cout << s; s = std::format("|{:>+12}|\n", x); std::cout << s; s = std::format("|{:>-12}|\n", x); std::cout << s; s = std::format("|{:>12}|\n", -x); std::cout << s; s = std::format("|{:X> 12}|\n", -x); std::cout << s; s = std::format("|{:>+12}|\n", -x); std::cout << s; s = std::format("|{:>-12}|\n", -x); std::cout << s; } * Örnek 5, #include #include #include #include // [[fill]align] [sign][#][0][width][.precision][type] int main() { int x = 26; std::cout << std::format("|{:07d}|", x) << '\n'; // |0000026| std::cout << std::format("|{:+07d}|", x) << '\n'; // |+000026| std::cout << std::format("|{: 07d}|", -x) << '\n'; // |-000026| std::cout << std::format("|{:-07d}|", -x) << '\n'; // |-000026| std::cout << std::format("|{:07X}|", x) << '\n'; // |000001A| std::cout << std::format("|{:#07X}|", x) << '\n'; // |0X0001A| } Şimdi de "format specifiers" olarak "type" kısmında yazabileceğimiz şeylere bakalım: Type Prefix Meaning b 0b Binary representation B 0B Binary representation c Single character d Integer or char o 0 Octal representation x 0x Hexadecimal representation X 0X Same as "x", but with upper case letters s Copy string to output, or true/false for a bool a 0x Print float as hexadecimal representation A 0X Same as "a", but with upper case letters e Print float in scientific format with precision of "6" as default E Same as "e", just the exponent is indicated with "E" f Fixed formatting of a float with precision of "6" F Same as "f", just the exponent is indicated with "E" g Standart formatting of a float with precision of "6" G Same as "g", just the exponent is indicated with "E" p 0x Pointer address as hexadecimal representation * Örnek 1, #include #include #include #include // [[fill]align] [sign][#][0][width][.precision][type] int main() { double dval = 234.432; puts("-----------------"); std::cout << std::format("|{}|\n", dval) << '\n'; // |234.432| std::cout << std::format("|{:g}|\n", dval) << '\n'; // |234.432| std::cout << std::format("|{:e}|\n", dval) << '\n'; // |2.344320e+02| std::cout << std::format("|{:E}|\n", dval) << '\n'; // |2.344320E+02| std::cout << std::format("|{:F}|\n", dval) << '\n'; // |234.432000| std::cout << std::format("|{:a}|\n", dval) << '\n'; // |1.d4dd2f1a9fbe7p+7| std::cout << std::format("|{:A}|\n", dval) << '\n'; // |1.D4DD2F1A9FBE7P+7| puts("-----------------"); std::cout << std::format("|{}|\n", 10 < 56) << '\n'; // |true| std::cout << std::format("|{}|\n", 10 > 56) << '\n'; // |false| std::cout << std::format("|{:b}|\n", 10 < 56) << '\n'; // |1| std::cout << std::format("|{:B}|\n", 10 > 56) << '\n'; // |0| std::cout << std::format("|{:#b}|\n", 10 < 56) << '\n'; // |0b1| std::cout << std::format("|{:#B}|\n", 10 > 56) << '\n'; // |0B0| std::cout << std::format("|{:d}|\n", 10 < 56) << '\n'; // |1| std::cout << std::format("|{:d}|\n", 10 > 56) << '\n'; // |0| std::cout << std::format("|{:x}|\n", 10 < 56) << '\n'; // |1| std::cout << std::format("|{:X}|\n", 10 > 56) << '\n'; // |0| std::cout << std::format("|{:#x}|\n", 10 < 56) << '\n'; // |0x1| std::cout << std::format("|{:#X}|\n", 10 > 56) << '\n'; // |0X0| puts("-----------------"); //std::cout << std::format("|{}|\n", &dval); // ERROR: Göstericiler söz konusu olduğunda varsayılan "specifier" mevcut değildir. std::cout << std::format("|{}|\n", (void*) &dval); // Adresi "void*" türüne dönüştürmeliyiz. // |0x8ff854| puts("-----------------"); int x = 56; std::cout << std::format("|{}|\n", x) << '\n'; // |56| std::cout << std::format("|{:c}|\n", x) << '\n'; // |8| std::cout << std::format("|{:d}|\n", x) << '\n'; // |56| std::cout << std::format("|{:#d}|\n", x) << '\n'; // |56| std::cout << std::format("|{:x}|\n", x) << '\n'; // |38| std::cout << std::format("|{:#x}|\n", x) << '\n'; // |0x38| std::cout << std::format("|{:X}|\n", x) << '\n'; // |d38| std::cout << std::format("|{:#X}|\n", x) << '\n'; // |0X38| std::cout << std::format("|{:o}|\n", x) << '\n'; // |70| std::cout << std::format("|{:#o}|\n", x) << '\n'; // |070| puts("-----------------"); std::string name{"tunahan"}; std::cout << std::format("|{}|\n", name) << '\n'; // |tunahan| std::cout << std::format("|{:.4}|\n", name) << '\n'; // |tuna| std::cout << std::format("|{:4.2}|\n", name) << '\n'; // |tu | std::cout << std::format("|{:24}|\n", name) << '\n'; // |tunahan | std::cout << std::format("|{:24.4}|\n", name) << '\n'; // |tuna | std::cout << std::format("|{:>24}|\n", name) << '\n'; // | tunahan| std::cout << std::format("|{:>24.4}|\n", name) << '\n'; // | tuna| std::cout << std::format("|{:^24}|\n", name) << '\n'; // | tunahan | std::cout << std::format("|{:^24.4}|\n", name) << '\n'; // | tuna | puts("-----------------"); } Şimdi de "std::format" sınıfındaki diğer fonksiyonlara örnekler üzerinden bakalım: * Örnek 1, #include #include #include #include #include // [[fill]align] [sign][#][0][width][.precision][type] int main() { int x = 2'435'345; std::format_to(std::ostream_iterator{std::cout}, "|{:^#16X}|\n", x); // | 0X252911 | std::string name; std::format_to(std::back_inserter(name), "|{:^#16X}|\n", x); std::cout << "Length: " << name.length() << ", " << name << '\n'; // Length: 19, | 0X252911 | } * Örnek 2, #include #include #include #include #include // [[fill]align] [sign][#][0][width][.precision][type] int main() { std::string name{ "Ali Aksoy" }; int id{ 78945 }; double dval{ 54.213455 }; std::string str; std::format_to(std::back_inserter(str), "|{} {} {:.2f}|", id, name, dval); std::cout << str << '\n'; // |78945 Ali Aksoy 54.21| std::list c_list; std::format_to(std::front_inserter(c_list), "|{} {} {:.2f}|", id, name, dval); for (auto c : c_list) std::cout << c; // |12.45 yoskA ilA 54987| std::cout << '\n'; } * Örnek 3, #include #include #include #include #include // [[fill]align] [sign][#][0][width][.precision][type] int main() { /* # OUTPUT # {A, 65} {B, 66} {C, 67} //... {X, 88} {Y, 89} {Z, 90} */ std::string str; for (char c = 'A'; c <= 'Z'; ++c) { std::format_to(std::back_inserter(str), "{{{0}, {0:d}}}\n", c); } std::cout << str << '\n'; } Şimdi de "local" bilgisini değiştirerek uygun dillerde metin yazdırabiliriz. * Örnek 1, #include #include #include #include #include // [[fill]align] [sign][#][0][width][.precision][type] int main() { std::cout << 123.96585 << '\n'; // 123.966 std::cout << 123 << '\n'; // 123 std::cout << std::format("{:L}\n", 123.96585) << '\n'; // 123.96585 std::cout << std::format("{:L}\n", 123) << '\n'; // 123 std::locale::global(std::locale{ "turkish" }); std::cout << 123.96585 << '\n'; // 123.966 std::cout << 123 << '\n'; // 123 std::cout << std::format("{:L}\n", 123.96585) << '\n'; // 123,96585 std::cout << std::format("{:L}\n", 123) << '\n'; // 123 std::cout << std::format(std::locale("turkish"), "{}\n", 123.96585); // 123.96585 std::cout << std::format(std::locale("turkish"), "{:L}\n", 123.96585); // 123, 96585 } Şimdi de diğer fonksiyonları inceleyelim: * Örnek 1, #include #include #include #include #include // [[fill]align] [sign][#][0][width][.precision][type] int main() { { /* * Böylelikle format bilgisinin uzunluğu elde edildikten * sonra dinamik ömürlü bir "buffer" da temin edilebilir. */ int x = 435466; auto len{ std::formatted_size("|{:#x}|", x) }; std::cout << "Length: " << len << '\n'; // Length: 9 } puts("\n---------"); { std::string name{ "ahmet kandemir" }, surname{ "pehlivanli" }; std::array ar{}; auto result = std::format_to_n( // "result" bir yapı türündendir. ar.data(), // Tamponun başlangıç adresi ar.size() - 1, // Tampona yazılacak karakter adedi. "\0" karakteri için de yer belirtmemiz gerekmektedir. "{} {}", // Formatlama Bilgileri name, surname // Formatlama Bilgileri ); std::cout << "Length: " << result.size << ", "; // Length: 25, for (auto c : ar) std::cout << c; // ahmet kandemir std::cout << '\n'; } } Diğer yandan bu sınıfı kendi sınıflarımızda kullanabilmek için "std::formatter" sınıf şablonuna bir adet "explicit/full specialization" yazmamız gerekmektedir. Daha sonra "parse" ve "format" sınıflarını da tanımlamamız gerekmektedir. * Örnek 1, class Always40 { public: int getValue()const { return 40; } }; template<> struct std::formatter { // Parse the format string for this type: constexpr std::format_parse_context::const_iterator parse(std::format_parse_context& ctx) { /* * Burada bizler "format" içerisindekileri "parse" edeceğiz. * Aşağıdaki ".begin()" fonksiyonu ":" karakteri kullanılmışsa "}" karakterinden * sonraki konumu döndürecektir. Ekstradan bi' de ".end()" fonksiyonu vardır ki o * ise yazının sonunun konumunu verecektir. */ return ctx.begin(); } // Format by always writing its value: auto format(const Always40& obj, std::format_context& ctx) const { return std::format_to(ctx.out(), "{}", obj.getValue()); } }; int main() { std::cout << std::format("|{{}}|\n", Always40{}); // |{}| auto x = Always40{}; std::cout << std::format("|{0} {0} {0}|\n", x); // |40 40 40| } * Örnek 2, #include #include #include #include #include // [[fill]align] [sign][#][0][width][.precision][type] class Person { public: Person(std::string name, int id) : m_name(std::move(name)), m_id(id) { } std::string get_name()const { return m_name; } int get_id()const { return m_id; } private: std::string m_name; int m_id; }; template<> struct std::formatter { constexpr auto parse(auto& context) { auto iter{ context.begin() }; const auto iter_end{ context.end() }; // Eğer "format specifier" belirtilmemişse // varsayılan senaryo işletilecektir. if (iter == iter_end || *iter == '}') { m_ftype = FormatType::All; return iter; } switch (*iter) { case 'n': m_ftype = FormatType::Name; break; case 'i': m_ftype = FormatType::Id; break; case 'a': m_ftype = FormatType::All; break; default: throw std::format_error{ "Person format error!" }; } ++iter; if (iter != iter_end && *iter != '}') { throw std::format_error{ "Person format error!" }; } return iter; } constexpr auto format(const Person& per, auto& context) { using enum FormatType; switch (m_ftype) { case Name: return std::format_to(context.out(), "{}", per.get_name()); case Id: return std::format_to(context.out(), "{}", per.get_id()); case All: return std::format_to(context.out(), "[{} {}]", per.get_id(), per.get_name()); } } private: enum class FormatType {Name, Id, All}; FormatType m_ftype; }; int main() { /* # OUTPUT # ahmet 678 [786 murat] */ Person p1{ "ahmet", 876 }; Person p2{ "harun", 678 }; Person p3{ "murat", 786 }; std::cout << std::format("{:n}\n{:i}\n{}", p1, p2, p3); } * Örnek 3, #include #include #include #include #include // [[fill]align] [sign][#][0][width][.precision][type] struct Point { int mx{}, my{}; }; template<> struct std::formatter { constexpr auto parse(auto& context) { auto iter{ context.begin() }; const auto iter_end{ context.end() }; // Eğer "format specifier" belirtilmemişse // varsayılan senaryo işletilecektir. if (iter == iter_end || *iter == '}') { cb_ = false; return iter; } switch (*iter) { case '#': cb_ = true; break; default: throw std::format_error{ "Person format error!" }; } ++iter; if (iter != iter_end && *iter != '}') { throw std::format_error{ "Person format error!" }; } return iter; } constexpr auto format(const Point& p, auto& context) { switch (cb_) { case true: return std::format_to(context.out(), "<{}, {}>", p.mx, p.my); default: return std::format_to(context.out(), "|{}, {}|", p.mx, p.my); } } private: bool cb_{}; }; int main() { /* # OUTPUT # <678, 876>, |876, 678|, |786, 786| */ Point p1{ 678, 876 }; Point p2{ 876, 678 }; Point p3{ 786, 786 }; std::cout << std::format("{:#}, {}, {}", p1, p2, p3); } /*================================================================================================================================*/ (15_19_08_2023) > Formatlı Çıkış İşlemleri (devam): Şimdi kendi "custom" sınıflarımızı "std::format" ile nasıl uyumlu hale getirebiliriz, ona dair örneklere bakalım: * Örnek 1, #include #include class Myint{ public: Mint() = default; explicit Mint(int x) : mx{x} {} int get()const { return mx; } private: int mx{}; }; template<> struct std::formatter { constexpr auto parse(std::format_parse_context& ctx) { /* * "std::format_parse_context" sınıfının ".begin()" fonksiyonu * formatlamanın başladığı konumu döndürmektedir. Örneğin formatlama * "{:123}" biçiminde olsaydı, "1" i gösterecekti; * "{}" biçiminde olsaydı, "}" i gösterecekti; * "{ali:<2.f}" biçiminde olsaydı, "<" i gösterecekti. * "12\n" biçiminde olsaydı, "1" i gösterecekti. Yani ":" varsa ":" den sonraki * ilk karakteri, yoksa "{" den sonraki ilk karakteri döndürmektedir. * Bu sınıfın ".end()" fonksiyonu ise yazının bittiği konumu döndürmektedir. * Örneğin, * "{:123}" biçimindeki formatlamada "}" den sonraki konumu; * "{ali:<2.f}\n" biçimindeki formatlamada "\n" dan sonraki konumunu göstermektedir. */ // formatlamada küme parantezinin içerisinin // boş olduğu varsayıldı. return ctx.begin(); } auto format(const Myint& m, std::format_context& ctx) { std::format_to(ctx.out(), "{}", m.get()); } }; int main() { /* # OUTPUT # */ Myint m{ 345 }; std::cout << std::format("|{}|", m) << '\n'; } * Örnek 2, #include #include class Myint{ public: Mint() = default; explicit Mint(int x) : mx{x} {} int get()const { return mx; } private: int mx{}; }; template<> struct std::formatter { constexpr auto parse(std::format_parse_context& ctx) { auto iter = ctx.begin(); // Burada format yazısını geziyor ve kendi belirlediğimiz // kurallara göre işliyoruz. Fakat şimdilik sadece "iter" // ötelendiğini varsayalım. while(iter != ctx.end() && *iter != '}') ++iter; // Programın akışı bu "if" deyimine girmesi demek yazının sonuna // gelindiği fakat '}' ile karşılaşılmadığı demektir. if(iter == ctx.end()) std::format_error{ "no closing brace\n" }; return iter; } auto format(const Myint& m, std::format_context& ctx) { std::format_to(ctx.out(), "{}", m.get()); } }; int main() { /* # OUTPUT # */ Myint m{ 345 }; std::cout << std::format("|{}|", m) << '\n'; } * Örnek 3, #include #include class Myint{ public: Mint() = default; explicit Mint(int x) : mx{x} {} int get()const { return mx; } private: int mx{}; }; template<> struct std::formatter { constexpr auto parse(std::format_parse_context& ctx) { auto iter = ctx.begin(); while(iter != ctx.end() && *iter != '}'){ if(*iter < '0' || *iter > '9') { // Programın akışı buraya girmişse rakam karakteri // haricinde bir karakter kullanılmış demektir. Bu // durumunda "exception throw" edebiliriz. // throw std::format_error{ "invalid width character\n" }; // Approach - I throw std::format_error{ std::format("", *iter) }; // Approach - II } // Böylelikle rakam karakteri sayıya dönüşmüş oldu. m_width = m_width * 10 + *iter - '0'; ++iter; } // Programın akışı bu "if" deyimine girmesi demek yazının sonuna // gelindiği fakat '}' ile karşılaşılmadığı demektir. if(iter == ctx.end()) std::format_error{ "no closing brace\n" }; return iter; } auto format(const Myint& m, std::format_context& ctx) { std::format_to(ctx.out(), "{:{}}", m.get(), m_width); } // Yazma alanı genişlik bilgisini tutmak // için bu değişkeni kullanacağız. int m_width{}; }; int main() { /* # OUTPUT # */ // Burada formatlama biçimi "|{:<15}|" gibi olsaydı, kodumuz yanlış // çalışacaktı. Myint m{ 345 }; std::cout << std::format("|{:15}|", m) << '\n'; } * Örnek 4.0, Yukarıdaki örneklerde "parse" işlemini her bir format için ayrı ayrı yapmamız gerekmektedir. Fakat bunun yerine halihazırda temel türler için yazılmış olan versiyonlarına çağrı yaparak da problemi çözebiliriz. #include #include class Myint{ public: Mint() = default; explicit Mint(int x) : mx{x} {} int get()const { return mx; } private: int mx{}; }; template<> struct std::formatter { /* mutable */ std::formatter mf; auto parse(std::format_parse_context& ctx) { return mf.parse(ctx); } auto format(const Myint& m, std::format_context& ctx) /* const */ { return mf.format(m.get(), ctx); } }; int main() { /* # OUTPUT # */ Myint m{ 345 }; std::cout << std::format("|{0:$>15}| |{0:_<15}|", m) << '\n'; } * Örnek 4.1, Aşağıdaki örnekte ise kalıtım mekanizmasından faydalanılmıştır. #include #include class Myint{ public: Mint() = default; explicit Mint(int x) : mx{x} {} int get()const { return mx; } private: int mx{}; }; template<> struct std::formatter : std::formatter { /* "parse" fonksiyonu taban sınıftan gelmektedir. */ auto format(const Myint& m, std::format_context& ctx) { return std::formatter::format(m.get(), ctx); } }; int main() { /* # OUTPUT # */ Myint m{ 345 }; std::cout << std::format("|{0:$>15}| |{0:_<15}|", m) << '\n'; } * Örnek 5, #include #include #include enum class Fruit{ Apple, Melon, Orange }; template<> struct std::formatter : std::formatter { auto format(Fruit f, std::format_context& ctx) { switch (f) { using enum Fruit; // C++20 std::string str; case Apple: str = "Apple"; break; case Melon: str = "Melon"; break; case Orange: str = "Orange"; break; } return std::formatter::format(str, ctx); } } int main() { /* # OUTPUT # */ Fruit f; f = Fruit::Melon; std::cout << std::format("|{:_<16}|\n", f); f = Fruit::Orange; std::cout << std::format("|{:.>16}|\n", f); f = Fruit::Apple; std::cout << std::format("|{:-^16}|\n", f); } Buraya kadar gördüğümüz bütün örneklerde format yazısı derleme zamanında değeri belli olan ve "std::string_view" sınıfına dönüştürülebilir bir türe ilişkindi. Çalışma zamanında format yazısını belirlemek istediğimizde ise karşımıza "vformat" ve "vformat_to" fonksiyonları çıkmaktadır. * Örnek 1, #include #include #include int main() { /* # INPUT # {:_>20} */ char format_string[20]{}; std::cout << "Format String: "; std::cin >> format_string; int format_value{ 123 }; auto formatted_string{ vformat(format_string, make_format_args(123)) }; std::cout << "Formatted String: " << formatted_string << '\n'; } > "Aggregate Types" : Bileşik türler olarak Türkçeleştirebiliriz. Bu türler "non-static" veri elemanlarını doğrudan kullanıma açan sınıflardır. Yani böylesi sınıfların veri elemanlarına karşı bir "data-hiding" uygulanmamaktadır. Dolayısıyla sınıfları bu noktada iki ana gruba ayırabiliyoruz: "data-hiding" uygulanan ve uygulanmayan sınıflar. Anımsanacağımız üzere bizler "data-hiding" uygulayarak sınıfın veri elemanlarının değerlerini kontrol altında tutarak sınıfın tutarlı olmasını sağlarız. Örneğin, aşağıdaki biçimde bir sınıfımız olsun: class Triangle{ //... double e1, e2, e3; //... }; Bu sınıfın yukarıdaki veri elemanları negatif olmamalı ve bu veri elemanları ile bizler üçgenleri temsil edebilmeliyiz. Dolayısıyla kontrol bizde olmalı ki temsil ettiğimiz şey bozulmasın. Misal, iki kenarın toplamının üçüncü kenardan büyük olması gerektiği gibi. Öte yandan "data-hiding" uygulamaya gerek görmediğimiz aşağıdaki örneği inceleylim: struct Point{ int x, y; }; Bu sınıfımız ise 2B düzlemde konum bilgisi tutsun. Dolayısıyla yukarıdaki "Triangle" sınıfı gibi veri elemanlarının negatif olmaması gibi şeyler söz konusu değildir. Buradaki kontrol mekanizması artık sınıfı yazanda değil, o sınıfı kullanan tarafta olacaktır. Öte yandan bu kavram sadece sınıflara has bir kavram da değildir. Örneğin, diziler doğrudan "Aggregate" türlerdir. Burada dizinin elemanlarının türlerinin "Aggregate" olup olmaması önemli değildir. Dizi türleri direkt olarak "Aggregate" kabul edilmektedir. Pekiyi "Aggregate" olma koşulları nelerdir? -> C++20 standartlarına göre "User-declared Ctor." fonksiyonları OLMAYACAK. Fakat C++17 standartlarına göre "User-Provided Ctor." fonksiyonları OLMAYACAK. Dolayısıyla C++17 ile bizler deklare edebiliriz fakat tanımlamasını yapamayız; yani ya derleyiciye yaptıracağız ya da "delete" edeceğiz. Fakat C++20 ile birlikte artık bizler bile deklare etmemeliyiz. * Örnek 1, #include struct Nec{ Nec() = default; int a, b, c; }; int main() { static_assert(std::is_aggregate_v); // ERROR: static assertion failed } -> "non-static" veri elemanlarının tamamı "public" OLMAK ZORUNDA. * Örnek 1, #include struct Nec{ int a, b; protected: int c; }; int main() { static_assert(std::is_aggregate_v); // ERROR: static assertion failed } -> Veri elemanları "Aggregate" olmayan sınıflar "Aggregate" olabilirler. * Örnek 1, #include #include struct Nec{ int a, b; std::string c; }; int main() { /* * Bir sınıfın "Aggregate" olabilmesi için * veri elemanlarının "Aggregate" olmasına * gerek yoktur. */ static_assert(std::is_aggregate_v); // OK static_assert(std::is_aggregate_v); // ERROR: static assertion failed } -> C++20 öncesinde "public" kalıtımı ile elde edilen sınıflar "Aggregate" olamıyorken, C++20 ile birlikte bu kısıtlama da kaldırıldı. Buradaki kilit nokta kalıtımın "public" olmasıdır. Eğer üst sınıf "Aggregate" olmasa bile "public" kalıtım uygulandığı için "Aggregate" kalıtımdır. Tabii burada yukarıda açıklanan koşulları sağladığı varsayılmaktadır. Fakat kalıtım "virtual" olarak yapılmışsa, "Aggregate" olma ÖZELLİĞİ DÜŞMEKTEDİR. Buna karşın "Multiple Inheritence" olması, "Aggregate" olma özelliğini KORUMAKTADIR. Son olarak "Inherited Ctor." da BULUNMAMASI gerekiyor. * Örnek 1, #include class Master{}; class Slave : public Master {}; class ProtectedSlave : protected Master {}; class PrivateSlave : private Master{}; int main() { static_assert(std::is_aggregate_v); // OK static_assert(std::is_aggregate_v); // OK static_assert(std::is_aggregate_v); // ERROR : static assertion failed static_assert(std::is_aggregate_v); // ERROR : static assertion failed } * Örnek 2, #include #include class Text : public std::string {}; int main() { static_assert(std::is_aggregate_v); // ERROR: static assertion failed static_assert(std::is_aggregate_v); // OK } * Örnek 3, "Virtual Inheritence": #include struct Myclass{ int mx{}; }; struct Text : virtual Myclass { int x{}, y{}; }; int main() { static_assert(std::is_aggregate_v); // OK static_assert(std::is_aggregate_v); // ERROR : static assertion failed } * Örnek 4, "Multiple Inheritence": #include class A{}; class B{}; class C{}; struct D : A, B, C { int x{}, y{}, z{}; }; int main() { static_assert(std::is_aggregate_v); // OK } * Örnek 5, "Inherited Ctor." da OLMAMASI gerekiyor. #include class A{ public: A(int) {} A(double) {} }; struct D : A{ using A::A; int x{}, y{}, z{}; }; int main() { static_assert(std::is_aggregate_v); // ERROR: static assertion failed static_assert(std::is_aggregate_v); // ERROR: static assertion failed } -> Veri elemanlarının referans (sağ taraf referans ya da sol taraf referans) olmaları da o sınıfın "Aggregate" olması için bir engel teşkil etmemektedir. -> İlgili sınıfta "Member Function" olması, "Aggregate" olmasına engel değildir. Bu fonksiyonların "public", "protected" veya "private" olması da sonucu DEĞİŞTİRMEMEKTEDİR. * Örnek 1, #include #include class Nec{ public: int a{}, b{}, c{}; int get_a() const { return a; } protected: int get_b() const { return b; } private: int get_c() const { return c; } }; int main() { static_assert(std::is_aggregate_v); // OK } -> Sanal fonksiyonlara sahip olmaları durumunda "Aggregate" olma özellikleri DÜŞECEKTİR. * Örnek 1, #include struct Nec{ int a{}, b{}, c{}; virtual void foo() {} }; int main() { static_assert(std::is_aggregate_v); // ERROR: static assertion failed } -> "Lambda" ifadelerinden elde edilen türler "Aggregate" DEĞİLDİR. Fakat bu sınıf türlerinden kalıtım yoluyla türetilen sınıflar "Aggregate" olabilirler. * Örnek 1, #include int main() { static_assert(std::is_aggregate_v); // ERROR: static assertion failed } * Örnek 2, #include auto f = [](int x){ return x*x; }; auto g = [](int x){ return x+x; }; struct Nec : decltype(f), decltype(g) { int mx; }; int main() { static_assert(std::is_aggregate_v); // ERROR: static assertion failed static_assert(std::is_aggregate_v); // ERROR: static assertion failed static_assert(std::is_aggregate_v); // OK } -> "operator overloading" mekanizmasından yararlanması, onların "Aggregate" olma özelliğini BOZMAMAKTADIR. * Örnek 1, #include struct Nec { int mx; Nec operator+(const Nec&)const { return Nec{}; } }; int main() { static_assert(std::is_aggregate_v); // OK } -> "static" veri elemanlarına sahip olmaları, onların "Aggregate" olmalarına engel DEĞİLDİR. Bütün bunlardan sonra şimdi sıra "Aggregate" olmanın avantajlarına gelmiştir. Yani "Aggregate" oldu da ne oldu? -> "Aggregate Init." yöntemi ile bir sınıf türünden nesneye ilk değer verebiliriz. Tıpkı C dilindeki yapı türlerinde kullandığımız yöntem gibi. * Örnek 1, #include struct Point { int x, y, z; }; int main() { static_assert(std::is_aggregate_v); // OK Point p1 = { 1, 3, 5 }; // OK Point p2{ 5, 3, 1 }; // OK // Point p3 = { 1., 3, 5. }; // ERROR: narrowing conversion of ‘1.0e+0’ from ‘double’ to ‘int’ [-Wnarrowing] } * Örnek 2, struct Neco { int x; double dval; std::string str; int ar[5]; }; int main() { static_assert(std::is_aggregate_v); // OK Neco myNec{ 15, 1.5, "on bes", { 1, 5 } }; // OK Neco myNecNec = { 30, 3., "otuz", { 3, 0 } }; // OK } * Örnek 3, #include struct Erg{ int x, y; }; struct Neco { int x; Erg e; int ar[2]; }; int main() { static_assert(std::is_aggregate_v); // OK static_assert(std::is_aggregate_v); // OK Neco myNec{ 15, { 1, 5}, { 1, 5 } }; // OK Neco myNecNec = { 15, { 1, 5}, { 1, 5 } }; // OK Neco myNecNecNec{ 15, 1, 5, 1, 5 }; // OK Neco myNecNecNecNec = { 15, 1, 5, 1, 5 }; // OK } * Örnek 4.0, İlk değer verdiğimiz ifadedeki eleman sayısı sınıfın veri elemanlarından eksikse, ilk değer vermediğimiz veri elemanları "Value Init." ile hayata gelirler. Bu durumda "primitive" türler sıfır ile sınıf türler ise "default ctor." hayata gelirler. #include #include struct Neco { bool b; int x; std::string str; }; int main() { static_assert(std::is_aggregate_v); // OK Neco my{}; // OK Neco myNec{ false }; // OK Neco myNecNec{ true, 12 }; // OK Neco myNecNecNec{ false, 21, "yirmibir"}; // OK } * Örnek 4.1, Burada hem "Default Member Init." mekanizmasından hem de "Aggregate Init." mekanizmasından faydalanılmıştır. #include #include #include struct Neco { bool b; int x{31}; std::string str; }; int main() { /* # OUTPUT # I. : 0 II. : 31 III. : */ static_assert(std::is_aggregate_v); // OK Neco myNec{ false }; std::cout << "I. : " << myNec.b << '\n'; std::cout << "II. : " << myNec.x << '\n'; std::cout << "III. : " << myNec.str << '\n'; } * Örnek 5.0, #include int main() { std::array ar = { 2, 4, 6, 8 }; // Bu kodun geçerli olmasının yegane sebebi, // "std::array" sınıfının "Aggregate" olmasıdır. // Diğer elemanlar "Value Init." ile hayata // gelmektedir. std::array arr{ 2, 4, 5 }; // Yine burada da ilgili sınıfın "Aggregate" olmasından // dolayı geçerlidir. Diğer elemanlar yine "Value Init." // ile hayata gelmektedir. } * Örnek 5.1, #include template struct Array{ T a[n]; }; int main() { Array arr{ 3, 5, 1, 6 }; } -> "Structural Binding" mekanizmasından da doğrudan faydalanabilir olmalarıdır. * Örnek 1, #include #include struct Neco{ int a, b, c; std::string str; }; int main() { Neco myNec{ 1, 2, 3, "123" }; auto [one, two, three, total] = myNec; std::cout << "<" << one << ", " << two << ", " << three << ", " << total << ">\n"; // <1, 2, 3, 123> } Şimdi de C++20 ile "Aggregate" türlere gelen yenilikleri inceleyelim. Bunlar: >> İlki, yukarıda da anlatıldığı üzere, "User-Provided Ctor." olması durumunda ilgili sınıf "Aggregate" kabul edilirken C++20 ile birlikte hiç bir "User-declared Ctor." OLMAMASI gerekmektedir ki "Aggregate" olma özelliği korunsun. >> İkincisi, C++20 ile dile eklenen bir diğer çok önemli özellik ise "Designated Init. Syntax" kuralıdır. Bu kural C diline C99 ile birlikte eklendi. Bu zamana kadar da ana akım derleyiciler tarafından "extension" olarak sunulmaktaydı. Artık standart hale getirilmiş oldu. Fakat C++20 ile eklenen halini işlemeden evvel C dilindekini bir hatırlayalım. Şöyleki: * Örnek 1, int main() { // C dilindeki aşağıdaki kullanım biçimi C++ dilindeki dizileri KAPSAMAMAKTADIR. int arr[5] = { [2] = 4, [4] = 2 }; // İki numaralı indisteki öğe "4" ile // dört numaralı indisteki öğe ise "2" ile // geri kalan diğer öğeler ise "0" değerini alır. int arr[] = { [9] = 3, [3] = 9, 56 }; // Dizi "10" elemanlı olacaktır. Çünkü "en büyük indis no + 1" // Dokuz numaralı indisteki öğe "3" ile // üç numaralı indisteki öğe "9" ile // son indisteki öğe "56" ile // geri kalanlar ise "0" değerini alır. } * Örnek 2, struct Data{ int a, b, c; char str[20]; int ar[5]; }; int main() { struct Data myData{ .a = 5, // Diğer öğeler "Zero Init." edilmektedir. }; struct Data myDataData{ .ar = { 2, 4, 6 }, // Diğer öğeler "Zero Init." edilmektedir. }; struct Data myDataDataData{ .str = "Merve", .ar = {[2] = 3}, // Dizinin diğer elemanları "Zero Init." edilmektedir. // Diğer öğeler "Zero Init." edilmektedir. }; } Şimdi de C++20 ile dile eklenen "Designated Init. Syntax" kuralını inceleyelim. Bunlar, >> "Aggregate Type" olması gerekmektedir. * Örnek 1, struct Nec{ int x, y; }; int main() { Nec myNec{ .x = 1, .y = 2 }; // OK Nec myNecNec = { .x = 2, .y = 1 }; // OK Nec myNecNecNec = { .x = 1 }; // OK: Diğer öğe "Value Init." edilecek. Nec myNecNecNec = { .y = 2 }; // OK: Diğer öğe "Value Init." edilecek. Nec myNecNecNecNec = { .y = 2, .x = 1 }; // ERROR: Bildirimdeki sıra gözetilmek zorundayız. } * Örnek 2, struct Nec{ int x, y; double dval; }; int main() { Nec myNec{ .x = 1, .y = 2 }; // OK Nec myNecNec{ .x = 1, .y = 2, 1.2 }; // ERROR: "Designated Init." ile "Normal Init." birlikte kullanılamaz. } * Örnek 3, struct Nec{ int x, y; static int s; }; int main() { Nec myNec{ .x = 1, .y = 2 }; // OK Nec myNecNec{ .s = 12 }; // "static" veri elemanlarını bu şekilde doğrudan ilk değer veremeyiz. } * Örnek 4, Aşağıdaki örnekte "=" atomu kullanılmıştır. Fakat bu atomun sağındaki ifadeyi "{}" içerisinde yazsak bile yine geçerli olacaktır. struct Time{ int min; int hour; }; struct Date{ int year; int month; int day; Time time; static int h_mode; }; int main() { Date d1 = { .h_mode = 0 }; // ERROR: static data member Date d2 = { .month = 3, .year = 1993 }; // ERROR: wrong order Date d3 = { 3, .year = 1998 }; // ERROR: mixing two "init." method Date d4 = { .time.min = 25 }; // ERROR: nested-member access is not allowed. Date d5 = { .time = { 32, 4 } }; // OK Date myDate = { .time = { .min = 23, .hour = 32 } }; // OK } >> "Default Member Init." mekanizması ile birlikte kullanabiliriz. Pekala "const" nesneler için de yine bu sentaks kuralını işletebiliriz. * Örnek 1, #include #include struct Nec{ int a; int b = 5; std::string name{ "John Doe" }; double d; }; int main() { Nec myNec{ .d = 5.6, // "a" nın değeri "Value Init." ile "0" olur. // "b" nin ve "name" nin değerleri // "Default Member Init." ile sırasıyla "5" ve // "John Doe" olur. }; std::cout << "<" << myNec.a << "," << myNec.b << "," << myNec.name << "," << myNec.d << ">" << std::endl; // <0,5,John Doe,5.6> const Nec mycNec{ .a = 5, // "d" nın değeri "Value Init." ile "0." olur. // "b" nin ve "name" nin değerleri // "Default Member Init." ile sırasıyla "5" ve // "John Doe" olur. }; std::cout << "<" << mycNec.a << "," << mycNec.b << "," << mycNec.name << "," << mycNec.d << ">" << std::endl; // <5,5,John Doe,0> } Şimdi de kullanım senaryolarını irdeleyelim: >> Kodun okunaklılığını arttırmaktadır. * Örnek 1, #include #include struct Student{ int id; std::string name; int grades[5]; }; int main() { Student s{ .id = 6144, .name = "Merve", .grades = { 75, 57, 78, 87, 100 } }; } >> Fonksiyonların geri dönüş değerinin "Aggregate" tür olması durumunda "Copy Ellision" dan faydalanabiliriz. * Örnek 1, #include #include struct Person{ int id{0}; std::string name{"NoName"}; int age{0}; }; Person create_person(void) { //... // Approach - I return Person{ .id = 7562, .name = "Merve", .age = 29 }; // Approach - II // return { .id = 7562, .name = "Merve", .age = 29 }; // Approach - III // return { 7562, "Merve", 29 }; } int main() { auto [id, name, age] = create_person(); } >> Birden fazla argüman alan fonksiyonlara parametre geçerken öğeleri karıştırabiliriz. * Örnek 1, #include #include // APPROACH - I void process_file(bool open, bool close, bool read, bool write); // APPROACH - II struct FileProp{ bool open, close, read, write; }; void process_file(const FileProp& prop); int main() { // APPROACH - I.1 process_file(true, false, false, true); // Hangi parametreye hangi değerin gönderildiğini karıştırabiliriz. // APPROACH - I.2 process_file( /* open */ true, /* close */ false, /* read */ false, /* write */ true ); // Yukarıdaki yönteme görece biraz daha iyi bir yöntem. En azından hangi parametrenin hangi değeri aldığını daha rahat görebiliyoruz. // APPROACH - II.1 FileProp fp1{ true, false, false, true }; process_file(fp1); // APPROACH - II.2 process_file(FileProp{ true, false, false, true }); // APPROACH - II.3 process_file({ true, false, false, true }); // APPROACH - II.4 process_file( .open = true, .close = false, .read = false, .write = true ); } >> "Function Overload Resolution" için de kullanılabilir. * Örnek 1, #include #include struct Point{ int x, y; }; struct Point3D{ double dx, dy, dz; }; void process(const Point&) { std::cout << "const Point&\n"; } void process(const Point3D&) { std::cout << "const Point3D&\n"; } int main() { /* # OUTPUT # const Point& const Point& const Point3D& const Point3D& const Point3D& */ process({.x = 45}); process({.x = 45, .y = 54}); process({.dx = 4.5}); process({.dx = 4.5, .dy = 5.4}); process({.dx = 4.5, .dy = 5.4, .dz = 45.54}); } >> "Deduction Guide" gereken yerlerde kullanabiliriz: * Örnek 1, Aşağıdaki örnekte "Aggregate" türler için "CTAT" dan faydalanabilmek için yine "Deduction Guide" gerekmektedir. #include template struct Nec { T x; int ival; }; // The Required deduction guide: template Nec(T, int) -> Nec; int main() { // OK Nec n1{ 1.2, 12 }; Nec { "murat", 124 }; // Require a deduction guide until C++20 Nec n3{ 1.2, 12 }; Nec n4{45L, 990}; // Since C++20 Nec n5{ 2.1, 21}; Nec n6{ 45L, 990}; } * Örnek 2, #include template struct Nec { T x; int ival; }; // The deduction guide from "const char*" to "std::string" Nec(const char*, int) -> Nec; int main() { Nec n1{ "mustafa", 12}; // "T" would be of type "const char*" // w/o the deduction guide. } >> "container" sınıflarda "Aggregate Type" lar tutulduğu durumlarda, geçini nesne ile "insert" işlemi yapılırken de kullanılabilir: * Örnek 1, #include #include #include struct Person { int id; std::string name{ "John Due" }; int age; }; int main() { /* # OUTPUT # <436,John Due,0> <0,Merve,0> <0,John Due,34> <34,Menekse,0> <34,Menekse,29> */ std::vector pvec; pvec.push_back(Person{ .id = 436 }); pvec.push_back(Person{ .name = "Merve" }); pvec.push_back(Person{ .age = 34 }); pvec.push_back(Person{ .id = 34, .name = "Menekse"}); pvec.push_back(Person{ .id = 34, .name = "Menekse", .age = 29}); for (const auto& [id, name, age] : pvec) std::cout << "<" << id << "," << name << "," << age << ">\n"; } >> "Aggregate Type" lar "Direct Init." e tabii tutulamıyorlar. Bu da bazı yerlerde problemlerin oluşmasına sebep oluyordu. C++20 ile birlikte "Aggregate Type" lar da "Direct Init." edilebilir hale getirildi. * Örnek 1, #include #include #include struct Nec { int x, y, z; }; template std::unique_ptr MakeUnique(Args&& ...args) { return std::unique_ptr(new T(std::forward(args)...)); } int main() { // ERROR: "Aggregate Types", C++20 STANDARDINA KADAR // "Dirent Init." EDİLEMİYORLAR. Yani "()" kullanılarak // ilk değer verilemiyorlar. "=" veya "{}" kullanılıyordu. // Dolayısıyla "new" ifadelerinde, "emplace" fonksiyonlarında, // "std::make_unique / std::make_shared" fonksiyonlarında // "Aggregate" türleri aşağıdaki biçimde kullanmak // sentaks hatası oluşturacaktır. auto up{ std::make_unique(3, 5, 8) }; auto sp{ std::make_shared(8, 5, 3) }; std::vector nvec; nvec.emplace_back(3, 5, 8); auto myup = MakeUnique(3, 5, 8); int a[3](1, 3, 5); Nec myNec(1, 3, 5); } Burada dikkat etmemiz gereken nokta "Aggregate" türleri "Dirent Init." etmemiz değil, "Direct Init." kullanan standart fonksiyonların artık sentaks hatasına neden olmadığıdır. /*================================================================================================================================*/ (16_20_08_2023) > "Aggregate Types" (devam): Şimdi de kullanım senaryolarını irdeleyelim (devam): >> "Aggregate Type" lar "Direct Init." e tabii tutulamıyorlar. Bu da bazı yerlerde problemlerin oluşmasına sebep oluyordu. C++20 ile birlikte "Aggregate Type" lar da "Direct Init." edilebilir hale getirildi. * Örnek 1, #include #include struct Nec { int a[5]; }; int main() { int a[3](4, 7, 9); // OK since C++20 // int b[3] = (4, 7, 9);// Still ERROR Nec n1{ 2, 6, 9 }; // OK Nec n2 = { 2, 6, 9 }; // OK // Nec n3(2, 6, 9); // Still ERROR Nec n4({ 2, 6, 9 }); // OK Nec n5{{ 2, 6, 9 }}; // OK std::array ar{1, 3, 4}; // OK std::array arr = {1, 3, 4}; // OK std::array arrr(1, 3, 4); // Still ERROR std::array arrr({ 1, 3, 4 }); // OK } * Örnek 2, Pekala "," operatörünün kullanımına da dikkat etmeliyiz. #include struct Nec { int a[5]; }; int main() { int x = 5, y = 4, z = 3; std::cout << (x, y) << ',' << (y, z) << ',' << (x, z) << '\n'; // 4,3,3 int q = (x, y, z); std::cout << q << '\n'; // 3 int j = x, y, z; // Re-definiton of 'y' and 'z'. Otherwise, they'd be "Default Init". // 'j' is equal of 'x'. } * Örnek 3, Artık "Aggregate Type" lar "Direct Init." edilebildikleri için, şablonlarla birlikte kullanıldıklarında, "Narrowing Conversion" lar sentaks hatasına yol açmayabilir. Anımsanacağı üzere "{}" ile yapılan "Init." işlemlerinde "Narrowing Conversion" sentaks hatasına yol açmaktadır. struct Point{ int x, y, z; }; int main() { /* # OUTPUT # */ auto p = new Point(1, 3, 5); // OK delete p; //p = new Point{ 1.1, 2, 3}; // ERROR: narrowing conversion //delete p; p = new Point( 1.1, 2, 3); // OK: narrowing conversion delete p; } * Örnek 4, #include struct Point{ int x, y, z; const int& ra; // Sağ taraf değerine bağlanması durumunda "Life Extension" a neden olacaktır. int&& rb; // Sağ taraf değerine bağlanması durumunda "Life Extension" a neden olacaktır. }; int foo() { return 1; } int bar() { return 2; } int main() { // int& r = foo(); // ERROR const int& cr = foo(); // OK - Life Extension int&& rr = bar(); // OK - Life Extension Point pt1 = { 3, 4, 5, foo(), bar() }; // OK - Life Extension std::cout << "<" << pt1.x << "," << pt1.y << "," << pt1.z << "," << pt1.ra << "," << pt1.rb << ">\n"; Point pt2( 3, 4, 5, foo(), bar() ); // Warning - Not A Life Extension std::cout << "<" << pt2.x << "," << pt2.y << "," << pt2.z << "," << pt2.ra << "," << pt2.rb << ">\n"; } * Örnek 5, #include // "Point" is an "Aggregate Type" struct Point{ int x, y; // "Nested" is an "Aggregate Type" struct Nested{ int xx, yy; }z; }; void printer(const Point& ptr) { std::cout << "<" << ptr.x << "," << ptr.y << "," << ptr.z.xx << "," << ptr.z.yy << ">\n"; } int main() { Point pt1 = { 1, 2, { 3, 4 } }; printer(pt1); // OK => <1,2,3,4> Point pt2 = { 1, 2, 3, 4 }; printer(pt2); // OK => <1,2,3,4> Point pt3{ 1, 2, { 3, 4 } }; printer(pt3); // OK => <1,2,3,4> Point pt4{ 1, 2, 3, 4 }; printer(pt4); // OK => <1,2,3,4> Point pt5{ 1, 2, (3, 4) }; printer(pt5); // OK(*) => <1,2,4,0> Point pt6( 2, 1, { 4, 3 } ); printer(pt6); // OK => <2,1,4,3> // Point pt7( 2, 1, ( 4, 3 ) ); // ERROR /* (*) * Çıktıdan da görüleceği üzere burada "," operatörü devreye girmiştir. Yani * ilk değer vermede kullanılan "1, 2, (3, 4)" ifadesi derleyici tarafından * "1, 2, 4" biçiminde ele alınmıştır. */ } * Örnek 6, #include struct Nec{ int a, b, c; }; void printer(const Nec& ptr) { std::cout << "<" << ptr.a << "," << ptr.b << "," << ptr.c << ">\n"; } int main() { Nec n1{ 1, 2, 3 }; // Valid C++17/C++20 printer(n1); Nec n2( 1, 2, 3 ); // Invalid at C++17, but valid at C++20 printer(n2); // Nec n3{ 1, 2.2, 3 }; // ERROR: Narrowing Conversation Nec n4( 1, 2.2, 3 ); // Warning: Narrowing Conversation Nec n5( .b = 6 ); // Invalid C++17/C++20 Nec n6{ .b = 6 }; // Invalid at C++17, but valid at C++20 } * Örnek 7, "Designated Init", "Direct Init". ile birlikte çalışmamaktadır. #include struct Nec{ int a, b, c; }; void printer(const Nec& ptr) { std::cout << "<" << ptr.a << "," << ptr.b << "," << ptr.c << ">\n"; } int main() { Nec n6{ .b = 6 }; // Invalid at C++17, but valid at C++20 Nec n5( .b = 6 ); // Invalid C++17/C++20 } Bütün bu örneklerdekileri özetleyecek olursak; -> "Direct Init." söz konusu olduğunda, referans elemanlar "Life Extension" a neden olmaz. -> "Direct Init." ile birlikte "Designated Init" kullanamayız. -> "= (x, y, z)" biçiminde ilk değer veremeyiz. -> "Nested Type" eleman için "{}" kullanmak zorundayız. > Hatırlatıcı Notlar: >> Aşağıdaki örnekleri inceleyelim: * Örnek 1.0, Aşağıdaki örnekte sınıf şablonunda "auto" kullanılması, "n" isminin "non-type" parametre olduğunu belirtmektedir. C++17 ile dile eklenmiştir. Sınıf şablonlarında kullanıldığı gibi fonksiyon şablonu ve "variable" şablonlarda da kullanılmaktadır. template class Myclass{ }; template void foo() {} template constexpr auto nec = n; int main() { Myclass<5> x; Myclass<[](){}> y; foo<5>(); std::cout << nec<5> << '\n'; } * Örnek 1.1, #include // ERROR /* template class Nec{ }; */ template class Myclass1{ }; template class Myclass2{ }; template class Myclass3{ }; template class Myclass4{ }; constexpr auto g = [](int x){ return x*x; }; int main() { Myclass1 d1; // OK Myclass1<2.1> d2; // OK Myclass2 d3; // OK Myclass2<1.2> d4; // OK Myclass3 d5; // OK: "d5" is different from "d6". Myclass3 d6; // OK: "d6" is different from "d5". auto f = [](){ return 31; }; Myclass3 d7; // OK Myclass4 d8; // "g" must be "constexpr". } * Örnek 1.2.0, "Static Init. Order Fiasco" engellemek. #include template class Lazy{ public: constexpr Lazy() = default; T& get() { static T var; return var; } }; int main() { /* * Aşağıdaki "x" ve "y" nesneleri aynı nesnelerdir. Bu iki nesneyi * ayrı iki nesne olarak kullanmak istiyorsak, yukarıdaki yöntem * yerine başka bir yöntem kullanmalıyız. */ Lazy x; Lazy y; x.get()++; x.get()++; x.get()++; x.get()++; x.get()++; std::cout << y.get() << '\n'; // 5 } * Örnek 1.2.1, Alternatif yöntemlerden birisi de yeni bir şablon parametresi eklemek ve bu parametreye karşılık "incomplete" tür kullanmaktır. #include template class Lazy{ public: constexpr Lazy() = default; T& get() { static T var; return var; } }; int main() { /* * Aşağıdaki "x" ve "y" nesneleri artık ayrı nesnelerdir. * Burada "struct X" ve "struct Y" türleri "incomplete" * tür olarak kullanılmıştır. */ Lazy x; Lazy y; x.get()++; x.get()++; x.get()++; x.get()++; x.get()++; std::cout << y.get() << '\n'; // 0 } * Örnek 1.2.2, Alternatif yöntemlerden bir diğeri ise şablon parametresi olarak "auto = [](){}" ifadesinin kullanılmasıdır. Böylelikle varsayılan tür kullanılacağı için "incomplete" tür kullanma zahmetinden kurtulmuş oluyoruz. #include template class Lazy{ public: constexpr Lazy() = default; T& get() { static T var; return var; } }; int main() { /* * Aşağıdaki "x" ve "y" nesneleri artık ayrı nesnelerdir. * Burada "struct X" ve "struct Y" türleri "incomplete" * tür olarak kullanılmıştır. */ Lazy x; Lazy y; x.get()++; x.get()++; x.get()++; x.get()++; x.get()++; std::cout << y.get() << '\n'; // 0 } > "constexpr" Fonksiyonlar: "Updates on constexpr functions from C++11 to C++20" isimli resim dosyasından da "constexpr" fonksiyonların yaşadığı değişimi görebiliriz. >> "constexpr" bir fonksiyonun derleme aşamasında çağrılmasını garanti altına almak için fonksiyona gönderilen ifade bir sabit ifadesi olmalı ve "constant expression" gereken yerde bu fonksiyonun çağrılması gerekmektedir. * Örnek 1, constexpr int foo(int x) { return x+5; } int main() { int x = foo(10); // Burada "foo" fonksiyonun derleme zamanında çağrılması // derleyicinin yaptığı bir optimizasyondur. } * Örnek 2, #include #include #include #include constexpr int foo(int x) { if(std::is_constant_evaluated()){ return x*x*x; // Sabit ifadesi gereken bir bağlamda kullanılırsa, buradaki kod çalıştırılacak. } else{ return x*x; // Aksi halde buradaki. } } int main() { int x = foo(10); // Burada "foo" fonksiyonun derleme zamanında çağrılması // derleyicinin yaptığı bir optimizasyondur. std::cout << "x : " << x << '\n'; // x : 100 const int y = foo(10); std::cout << "y : " << y << '\n'; // y : 1000 constexpr int z = foo(10); std::cout << "z : " << z << '\n'; // z : 1000 int a[foo(2)]; // int a[8]; std::array arr; // std::array arr; if constexpr (foo(5) > 40){ std::cout << "Ali\n"; // Bu kod alınacak. Ekrana "Ali" yazacaktır. } else{ std::cout << "Veli\n"; } if(foo(5) == 125){ std::cout << "Ali\n"; } else{ std::cout << "Veli\n"; // Bu kod alınacak. Ekrana "Veli" yazacaktır. } static_assert(foo(5) == 125); // "fail" OLMAYACAKTIR. std::bitset bs; // std::bitset<1000> bs; } >> "constexpr" bir fonksiyonun derleme aşamasnda çağrılmasını garanti ettiğimiz bir senaryoda tanımsız davranış varsa, derleyici sentaks hatası vermek zorundadır. * Örnek 1, int foo_ub(int idx) { int ar[] = { 2, 3, 5, 7, 9, 11, 13, 15, 17, 19 }; return ar[idx]; } constexpr int foo(int idx) { int ar[] = { 2, 3, 5, 7, 9, 11, 13, 15, 17, 19 }; return ar[idx]; } constexpr int factorial(int n) { return n < 2 ? 1 : n * factorial(n-1); } constexpr int shift(int x, int n) { return x << n; } int main() { int a = foo_ub(20); // Tanımsız Davranış oluşacaktır. Çünkü dizinin boyutu taşmıştır. // ERROR: call to non-‘constexpr’ function ‘int foo_ub(int)’ constexpr int b = foo_ub(20); // "constexpr" bir nesneye ilk değer veren ifadenin "constant expression" olması gerekmektedir. // ERROR: array subscript value ‘20’ is outside the bounds of array ‘ar’ of type ‘int [10]’ constexpr int c = foo(20); // "constexpr" bir fonksiyonu derleme aşamasında çağırmaya zorlarsak ki burada zorluyoruz, derleyici // tanımsız davranışı tespit edip, Tanımsız Davranış olması durumunda, sentaks hatası olarak değerlendirmek // zorundadır. int d = factorial(14); // Tanımsız Davranış olacaktır. Çünkü 12! itibariyle "4-byte" işaretli tam sayılarda taşma oluşmaktadır. İşaretli // tam sayılarda taşma ise Tanımsız Davranıştır. // ERROR: overflow in constant expression [-fpermissive] constexpr int e = factorial(14); // Artık bu noktada sentaks hatası oluşacaktır, yukarıdaki taşmanın getirdiği tanımsız davranıştan ötürü. constexpr int f = shift(20, 32); // Sentaks hatası oluşacaktır. Çünkü tam sayının bit adedine eşit ya da ondan büyük değerde olan sağ operand, // sağa ya da sola kaydırma fark etmeksizin, tanımsız davranış oluşturur. Yukarıdaki sebepten ötürü de sentaks // hatası oluşmaktadır. "32-bit" değerindeki bir sayıyı en fazla "31" bit kaydırabiliriz. } >> "constexpr" fonksiyonlarda "throw" ifadelerini kullanabiliyoruz fakat çalışma zamanına ilişkin bağlamda kullanmalıyız. * Örnek 1, #include #include constexpr int factorial(int n) { if (n < 0) throw std::runtime_error{"negative factorial argument!"}; int result{1}; for(int i = 1; i <= n; ++i) result *= i; return result; } int main() { int ival; std::cout << "Bir tam sayi girin: "; std::cin >> ival; try{ /* * Şimdi çalışma zamanında "ival" değişkeni negatif bir değer * alırsa, "Run Time Error" oluşacaktır. */ auto x = factorial(ival); } catch(const std::exception& ex){ std::cout << "Caught exception: " << ex.what() << '\n'; } constexpr auto x = factorial(ival); // Sentaks hatası oluşacaktır. /* * Böylelikle hem çalışma zamanında "throw" ettirme imkanı hem de * derleme zamanında sentaks hatası oluşturabilme imkanı elde etmiş * oluyoruz. */ } >> Algoritmaların tamamına yakını "constexpr". * Örnek 1, #include #include #include #include constexpr int foo(int n) { int ar[10] = { 1, 3, 5, 7, 9, 11, 13, 15 }; //... std::sort(std::begin(ar), std::end(ar), std::greater{}); return ar[n]; } constexpr int foo(int x, int y) { int ar[10] = { 1, 3, 5, 7, 9, 11, 13, 15 }; //... return std::accumulate(std::next(std::begin(ar), x), std::next(std::begin(ar), y), 0); } // Alternative Way 1 constexpr void foo(double* p) { delete []p; }; constexpr int factorial(int n) { return n < 2 ? 1 : n * factorial(n - 1); } constexpr double get_e(int n) { double* p = new double[n]; for(int i{}; i < n; ++i) p[i] = 1. / factorial(i); auto sum = std::accumulate(p, p + n, 0.); //delete p; // Bu şekilde bir "delete" işlemi tanımsız davranış olduğundan, sentaks hatası olmuştur. // ERROR: non-array deallocation of object allocated with array allocation // Eğer "deallocation" işlemini hiç yapmazsak, tanımsız davranış oluşacağından, sentaks hatası alırız. // ERROR: ‘get_e(10)’ is not a constant expression because allocated storage has not been deallocated delete []p; // OK //foo(p); // OK - Alternative Way 1 return sum; } int main() { constexpr auto val1 = foo(2); // "val1" değişkeninin değeri artık derleme zamanında hesaplanmıştır. constexpr auto val2 = foo(2, 5); // "val2" değişkeninin değeri artık derleme zamanında hesaplanmıştır. constexpr auto val3 = get_e(10); // "val3" değişkeninin değeri artık derleme zamanında hesaplanmıştır. } * Örnek 2, #include #include #include #include constexpr int foo(int n) { std::vector vec{ 1, 4, 7, 2, 3, 9, 6, 7, 5 }; std::sort(vec.begin(), vec.end()); return std::accumulate(vec.begin(), std::next(vec.begin(), n), 0); } int main() { constexpr int x = foo(3); // "x" will be evaluated in compile time. } * Örnek 3, #include #include #include #include constexpr int get_median(std::vector vec) { std::sort(vec.begin(), vec.end()); return vec[vec.size() / 2]; } int main() { constexpr auto median = get_median({ 2, 4, 6, 1, 9, 3, 12, 67, 982, 4, 6 }); } * Örnek 4, #include #include #include constexpr std::vector split(std::string_view strv, std::string_view delims = " ") { std::vector output; size_t first = 0; while(first < strv.size()){ const auto second = strv.find_first_of(delims, first); if(first != second) output.emplace_back(strv.substr(first, second - first)); if(second == std::string_view::npos) break; first = second + 1; } return output; } constexpr size_t numWords(std::string_view str) { const auto words = split(str); return words.size(); } int main() { static_assert(numWords("hello world abc xyz") == 4); // Test-case I constexpr auto val = numWords("ali veli hasan necati handan rukiye murat"); // Test-case II /* * Yine buradaki "val" değişkeninin değeri de derleme zamanında hesaplanmıştır. */ } * Örnek 5, #include struct Point{ constexpr Point& operator+=(const Point& other) noexcept { mx += other.mx; my += other.my; return *this; } double mx{}, my{}; }; constexpr bool test(int n) { std::vector vec(n); for(auto& pt : vec) pt = new Point{ 0., 1 }; Point sum{}; for(auto& pt : vec) sum += *pt; for(auto& pt : vec) delete pt; return static_cast(sum.my) == n; } int main() { static_assert(test(10)); } > "consteval" Fonksiyonlar: "constexpr" fonksiyonlardan farklı olarak, "compile time context" içerisinde çağrılmaları GARANTİ altındadır. Yani derleme zamanı bağlamında çağrılması mümkün değilse, SENTAKS HATASI oluyor. Yani sadece derleme zamanında çağrılabilir. Koşul kümesinin "constexpr" fonksiyonlarınınki ile aynı olduğunu düşünebiliriz. "Immediate Function" olarak da geçer. * Örnek 1, consteval int square(int x) { return x*x; } int main() { constexpr int x = square(5); // OK int ival = 5; square(ival); // ERROR: the value of ‘ival’ is not usable in a constant expression } > "constinit" Anahtar Sözcüğü: Bu anahtar sözcük ile tanımlanmış bir değişken, "static" olarak derleme zamanında "init." edilmiş bir değişkendir. Fakat bu konudan önce değişkenlerin hayata gelme aşamalarını irdeleyelim: -> Aynı kaynak dosyadaki "global" isim alanındaki değişkenler, bildirim sırasında göre hayata gelirler. Dil bu garantiyi vermektedir. Fakat farklı kaynak dosyalardaki "global" isim alanındaki değişkenlerden hangisinin ilk hayata geleceğini garanti eden bir mekanizma malesef yoktur. Örneğin, // ali.cpp A ax; // veli.cpp B bx; biçiminde iki adet kaynak dosyamız olsun. Bu değişkenlerden birisini, diğer sınıfın "Ctor." fonksiyonu içerisinde de pekala kullanabilirim. Dil, bu değişkenlerden hangisinin ilk hayata geleceğini garanti etmediğinden, felaket oluşacaktır. İşte bu duruma ise "Static Initialization (Order) Fiasco" denmektedir. İşte "constinit" anahtar sözcüğü bu problemin çözüm yollarından bir tanesidir. Detaylı bilgi için: https://www.jonathanmueller.dev/talk/static-initialization-order-fiasco/ -> "Storage Duration" ve "Lifetime/Life span" kavramları birbirleri ile ilişkili kavramlardır. Bunlardan ilki bir nesnenin yerinin nasıl ayrıldığını ayarlamaktadır. İkincisi ise nesne ne zaman hayata gelecek, ne zaman hayatı bitecek konularıyla alakalıdır. Burada "lifetime != storage duration". Örneğin, "static storage duration" a sahip nesneler için yer ayrılır fakat o nesnenin bulunduğu fonksiyon çağrılmadan ilgili "ctor." fonksiyonu çağrılmaz. Bir diğer deyişle "When does the lifetime of variables with static storage duration begins?" sorusunun cevabı nedir? Burada iki farklı kural devreye girmektedir. Bunlar, -> Global değişkenler iki kademe "init." ediliyorlar. Birinci kademeye "Static Init", ikinci kademeye "Dynamic Init." denmektedir. Buradaki ilk kademe derleme zamanında, ikinci kademe ise programın çalışma zamanında gerçekleştiriliyor. Buradaki ilk aşamada nesneye ilişkin bellek alanı sıfırlanıyor. Nesnenin asıl değerini aldığı süreç ise ikinci kademede gerçekleştiriliyor. Fakat öyle varlıklar var ki ikinci kademe bunlar için hiç uygulanmamaktadır. > Hatırlatıcı Notlar: >> "https://www.jonathanmueller.dev/" /*================================================================================================================================*/ (17_26_08_2023) > "constinit" Anahtar Sözcüğü (devam): "static" ömürlü bir değişkeni ki bunlar "global" isim alanındaki değişken, "static" ömürlü yerel değişken veya "static" ömürlü sınıfların veri elemanı olabilir, bu anahtar sözcük ile tanımladığımızda, değişkenin "constant init." edilmesini garanti ediyoruz. Yani yukarıdaki "Dynamic Init." uygulanmamaktadır. Eğer "Dynamic Init." uygulanması gerekiyorsa da sentaks hatası meydana gelecektir. Bunu şu denklem ile de açıklayabiliriz: " constinit = constexpr - const " Anımsanacağı üzere "constexpr" değişkenler sabit ifadesi ile ilk değer almaları zorunluluktur. Öte yandan böyle değişken aynı zamanda "const" olduğu için değiştirilemezler de. İşte yine ilk değer alırken sabit ifadesinin kullanıldığı fakat "mutable" olan değişkenler için "constinit" anahtar sözcüğünü kullanmalıyız. * Örnek 1, #include constexpr int foo(int x) { return x*5; } constexpr auto x = foo(5); constinit auto y = foo(6); int main() { std::cout << "x : " << ++x << '\n'; // ERROR: increment of read-only variable ‘x’ std::cout << "y : " << ++y << '\n'; // OK } * Örnek 2, #include #include constexpr std::array get_array_4() { return { 10, 20, 30, 40 }; } constinit auto g_array_4 = get_array_4(); int main() { for(auto i : g_array_4) std::cout << i << ' '; // 10 20 30 40 std::cout << '\n'; // "g_array_4" IS NOT const g_array_4[0]++; g_array_4[1] += 1; for(auto i : g_array_4) std::cout << i << ' '; // 11 21 30 40 std::cout << '\n'; } * Örnek 3, #include #include #include template constexpr std::array get_array() { return std::array{0}; } constinit auto g_arr = get_array<10>(); int main() { for(auto i : g_arr) std::cout << i << ' '; std::cout << '\n'; // 0 0 0 0 0 0 0 0 0 0 std::for_each( begin(g_arr), end(g_arr), [](int& r){ ++r; } ); for(auto i : g_arr) std::cout << i << ' '; std::cout << '\n'; // 1 1 1 1 1 1 1 1 1 1 } > "Attributes" : Derleyiciyi yönlendiren, aynı zamanda da kodu okuyanı bilgilendiren şeylerdir. Her zaman için "[[]]" biçiminde, en içteki parantezin içerisine yazılır. En önemlilerinden birisi "nodiscard" olandır. >> Tipik olarak fonksiyonların geri dönüş değeri ıskartaya çıkartıldığında, derleyicinin uyarı vermesini teşvik eder. Ek olarak o fonksiyonu kullanan kişiyi de fonksiyonun geri dönüş değerini kullanması konusunda disipline eder. Bu "attribute" şu senaryolarda kullanılmalıdır: -> Fonksiyonun geri dönüş değerinin kullanılmaması bir "Logic Error ise. -> Fonksiyonun geri dönüş değerinin kullanılmaması bir "Logic Error" değil fakat çok büyük bir risk içeriyorsa. Bu "attribute", "[[nodiscard]]" veya "[[nodiscard (Explanation)]]" biçiminde kullanılır. * Örnek 1, #include [[discard]] // C++17 int foo(int x) { return x; } [[discard ("The return value of foo should not be discarded!")]] // C++20 int bar(int x) { return x; } int main() { foo(313); // WARNING: ‘discard’ attribute directive ignored [-Wattributes] bar(313); // WARNING: The return value of foo should not be discarded! (void)foo(313); // OK : The return value of foo is discarded intentionally (void)bar(313); // OK : The return value of bar is discarded intentionally } Pekala sınıfların üye fonksiyonlarında da kullanılabilir. * Örnek 1, #include class Myclass{ public: [[nodiscard ("Myclass::Myclass return value is discared!")]] Myclass(int x) { std::cout << x << '\n'; } [[nodiscard]] int foo(int x) { return x; } }; int main() { Myclass{5}; // WARNING: ignoring return value of ‘Myclass::Myclass(int)’, declared with attribute ‘nodiscard’: ‘Myclass::Myclass return value is discared!’ static_cast(7); // WARNING: ignoring return value of ‘Myclass::Myclass(int)’, declared with attribute ‘nodiscard’: ‘Myclass::Myclass return value is discared!’ Myclass{10}.foo(50); // WARNING: ignoring return value of ‘int Myclass::foo(int)’, declared with attribute ‘nodiscard’ } Diğer yandan "class-type" tanımlarında da kullanılır ki bu durumda geri dönüş değeri türü bu sınıf olan fonksiyonlar çağrıldığında, fonksiyonun bu "attribute" kullanmasına bakılmaksızın, yine bir uyarı mesajı verecektir. Tıpkı fonksiyonların geri dönüş değerini ıskartaya çıkardığımız gibi. * Örnek 1, #include class [[nodiscard]] Myclass{}; class [[nodiscard ("Neco cannot be discarded!")]] Neco{}; Myclass foo() { return Myclass{}; } Neco bar() { return Neco{}; } int main() { foo(); // WARNING: ignoring returned value of type ‘Myclass’, declared with attribute ‘nodiscard’ bar(); // WARNING: ignoring returned value of type ‘Neco’, declared with attribute ‘nodiscard’: ‘Neco cannot be discarded!’ } * Örnek 2, #include enum [[nodiscard]] ErrorCode{ Passed, Failed, Unknown }; enum class [[nodiscard ("Neco cannot be discarded!")]] WarningCode{}; ErrorCode foo() { return ErrorCode{}; } WarningCode bar() { return WarningCode{}; } int main() { foo(); // WARNING: ignoring returned value of type ‘ErrorCode’, declared with attribute ‘nodiscard’ bar(); // WARNING: ignoring returned value of type ‘WarningCode’, declared with attribute ‘nodiscard’: ‘WarningCode cannot be discarded!’ } * Örnek 3, #include class [[Myclass]] Myclass{}; Myclass foo() { return Myclass{}; }; Myclass& bar() { static Myclass m; return m; }; int main() { foo(); // WARNING: ‘Myclass’ attribute directive ignored [-Wattributes] bar(); // OK } Burada bizim dikkat etmemiz gereken nokta, bu "attribute" kullanmakla birlikte derleyicileri uyarı mesajı vermeye teşvik etmiş oluyoruz. Fakat uyarı mesajı vermek gibi bir zorunlulukları yoktur. > "concepts" : Buradaki amaç şablon kodlarını "constraint" etmektir. Yani şablon argümanını belirli bir miktarda kısıtlamak için kullanılır. Böylelikle yanlış bir şablon argümanı kullanıldığında ya direkt sentaks hatası oluşacak ya ilgili şablonun belli bir kısmı kullanılacak ya da derleyici nokta atışı mesaj vererek hatanın tam olarak nereden kaynaklandığını belirtecektir. Burada kendi kısıtlamalarımızı kendimiz oluşturabildiğimiz gibi, standart kütüphanenin bize hazır olarak verdiği kısıtlamaları da kullanabiliriz. * Örnek 1, #include #include template void foo(T x) { std::cout << "Tam sayi\n"; } template void foo(T x) { std::cout << "Gercek sayi\n"; } void bar(std::integral auto) { std::cout << "Tam sayi\n"; } void bar(std::floating_point auto) { std::cout << "Gercek sayi\n"; } int main() { /* # OUTPUT # Tam sayi Tam sayi Gercek sayi Gercek sayi Tam sayi Tam sayi Gercek sayi Gercek sayi */ foo(23); foo(23L); foo(2.3f); foo(2.3); bar(23); bar(23L); bar(2.3f); bar(2.3); } Diğer yandan C++20 ile "concepts" kavramının ile eklenmesiyle birlikte toplamda beş adet şablon bulunmaktadır. Sınıf şablonu, fonksiyon şablonu, "variable templates", "allias template" ve "concepts". Aynı zamanda da "concept" bir anahtar sözcüktür. * Örnek 1, "concept" olmasaydı, aşağıdaki gibi bir çözüm sergilemek zorundaydı. #include #include template>* = nullptr> void foo(T) { std::cout << "foo\n"; } template std::enable_if_t, T> bar(T) { std::cout << "bar\n"; return true; } template void baz(T, std::enable_if_t>* p = nullptr) { std::cout << "baz\n"; } int main() { /* # OUTPUT # */ foo(340); // OK foo(3.4); // ERROR: no matching function for call to ‘foo(double)’ bar(340); // OK bar(3.4); // ERROR: no matching function for call to ‘bar(double)’ baz(340); // OK baz(3.4); // ERROR: no matching function for call to ‘baz(double)’ } * Örnek 2, "concept" olmasaydı, aşağıdaki sentaks hataları oluşacaktı. #include #include #include template void print(const T& t) { std::cout << t << '\n'; } template void convert_to_int(const T& t) { int x = t; } int main() { /* # OUTPUT # */ print(12); print(1.2); print(std::string{"Merve"}); std::vector vec{ 45, 5, 7, 2 }; print(vec); // ERROR: no match for ‘operator<<’ (operand types are ‘std::ostream’ {aka ‘std::basic_ostream’} and ‘const std::vector >’) convert_to_int(std::string{"Merve"}); // ERROR: cannot convert ‘const std::__cxx11::basic_string’ to ‘int’ in initialization } Pekiyi bizler nasıl "constraint" ederiz? Burada üç farklı araç karşımıza çıkmaktadır. Bunlar "requires clause", "requires expression", "named constraints(concept)". >> "requires clause" : Genel sentaks şu şekildedir: " requires ("boolean" olarak yorumlanacak "Compile Time Expression") " İki biçimde kullanılır. Bunlar "prefix" ve "trailing" biçimde. >>> "prefix" yönteminde fonksiyonun parametre değişkeninin ismini kullanamayız. Çünkü bu yöntemde, yukarıdaki ifadeyi, fonksiyonun geri dönüş değerinin türünden önce yazmaktayız. * Örnek 1, Aşağıdaki "requires clause" kullanımında "prefix" biçimi uygulanmıştır. #include // "T" türünün "sizeof" değerinin "2" bayttan büyük olması gerekmektedir. template requires (sizeof(T) > 2) void foo(T x) { std::cout << "sizeof x : " << sizeof(x) << '\n'; } int main() { // foo('A'); // ERROR: no matching function for call to ‘foo(char)’ foo(45); // OK : sizeof x : 4 } >>> "trailing" yönteminde ise fonksiyonun parametre değişkeninin ismini kullanabiliriz. Çünkü bu yöntemde yukarıdaki ifadeyi fonksiyonun imzası ile "{" arasındaki kısma yazmaktayız. * Örnek 1, #include // "x" değişkeninin "sizeof" değerinin "4" bayttan büyük olması gerekmektedir. template void foo(T x) requires (sizeof(x) > 4) { std::cout << "sizeof x : " << sizeof(x) << '\n'; } int main() { //foo(45); // ERROR: no matching function for call to ‘foo(int)’ foo(45L); // OK : sizeof x : 8 } Pekala bu iki biçimi de aynı anda kullanabiliriz. * Örnek 1, #include #include template requires std::is_integral_v void foo(T x) requires std::is_signed_v { std::cout << "sizeof x : " << sizeof(x) << '\n'; } int main() { foo(45); foo(4.5); // ERROR: no matching function for call to ‘foo(double)’ foo(45u); // ERROR: no matching function for call to ‘foo(unsigned int)’ } Tabii bu biçimleri böyle aynı anda kullanmak yerine, "logic" operatörleriyle de birleştirerek kullanabiliriz. * Örnek 1, #include #include template requires std::is_integral_v && (sizeof(T) > 2) void foo(T x) { std::cout << "sizeof x : " << sizeof(x) << '\n'; } int main() { foo('A'); // ERROR: no matching function for call to ‘foo(char)’ foo(45); foo(4.5); // ERROR: no matching function for call to ‘foo(double)’ foo(45u); } * Örnek 2.0, #include #include template requires !(sizeof(T) > 2) // ERROR: expression must be enclosed in parentheses void foo(T x) { std::cout << "sizeof x : " << sizeof(x) << '\n'; } int main() { /* * "!" operatörünü kullanmak istiyorsak, "()" içerisinde kullanmalıyız. */ } * Örnek 2.1, #include #include template requires (!(sizeof(T) > 2)) // ERROR: expression must be enclosed in parentheses void foo(T x) { std::cout << "sizeof x : " << sizeof(x) << '\n'; } int main() { foo('a'); foo(65); // ERROR: no matching function for call to ‘foo(int)’ } * Örnek 3, #include #include #include #include template requires (sizeof(T) > 2) && // "requires clause" => "T" türünün "sizeof" değerinin "2" den büyük olması requires { typename T::size_type; } && // "requires expression" => "T" türü "size_type" isminde "nested type" a sahip olması std::input_iterator // "concept" => "input_iterator" konseptinin "T" türüne açılabilir olması class Myclass{ }; int main() { //... } * Örnek 4, #include #include #include template requires std::is_pointer_v || std::is_reference_v class Myclass{ }; int main() { Myclass m1; // ERROR: template constraint failure for ‘template requires (is_pointer_v) || (is_reference_v) class Myclass’ } * Örnek 5, template // template requires (N > 10) class Neco{ }; int main() { Neco<5> n1; // ERROR: template constraint failure for ‘template requires N > 10 class Neco’ Neco<15> n2; // OK } Buradaki önemli nokta, derleme zamanında "constant expression" olabilmesidir. >> "concept" : İsimlendirilmiş "constraint" lerdir. Tekrar tekrar belirtmek yerine, vermiş olduğumuz ismi kullanıyoruz. Yine derleme zamanında "boolean" değer üretmesi gerekmektedir. * Örnek 1, #include #include #include template concept Integral = std::is_integral_v; // Bu noktada diğer "concept", "requires clause" ve "requires expression" // kullanarak istediğimiz kombinasyonları yapabiliriz. template requires Integral void foo(T) {} template void func(T) {} // Burada fonksiyon şablonu kullanabildiğimiz gibi sınıf şablonu da kullanabilirdik. // Hakeza diğer şablon türlerini de. Buradaki şart, "Integral" şartını sağlamasıdır. // Yani "std::is_integral_v" ifadesinin "true" değer üretmesi gerekiyor. template class Myclass{ }; int main() { foo(12); // OK func(1.2); // ERROR: no matching function for call to ‘func(double)’ (*) func(12); // OK func(1.2); // ERROR: no matching function for call to ‘func(double)’ (*) /* (*) * Burada bizler "CTAT" yararlandık. Özel olarak türü belirtsek bile yine * sentaks hatası olabilirdi. Çünkü buradaki kıstas, yukarıdaki "std::is_integral_v" * ifadesinin "true" değer üretmesidir. */ Myclass m1; // Burada herhangi bir sentaks hatası yoktur. Çünkü "T" ve "U" isminde iki farklı // şablon parametresi vardır. Bura bizim sağlamamız gereken şart ise // "std::is_integral_v && std::is_integral_v" ifadesinin "true" değer // üretmesidir. Myclass m2; // ERROR: template constraint failure for ‘template requires (Integral) && (Integral) class Myclass’ Myclass m3; // ERROR: template constraint failure for ‘template requires (Integral) && (Integral) class Myclass’ } * Örnek 2, #include #include #include template concept Integral = std::is_integral_v; template concept SignedIntegral = Integral && std::is_signed_v; template concept UnsignedIntegral = Integral && !SignedIntegral; int main() { //... } * Örnek 3, #include #include #include template concept as_int = std::integral || std::is_convertible_v; template requires as_int void foo(void) {} struct Neco{}; struct Myclass{ operator int()const; }; int main() { foo(); // OK foo(); // OK foo(); // OK foo(); // OK foo(); // ERROR: no matching function for call to ‘foo()’ foo(); // OK } * Örnek 4, #include #include #include template concept as_int = std::integral || std::is_convertible_v; template class Myclass{ }; struct Nec{}; // Way - I // template // void foo (T x); // Way - II void foo(as_int auto x) {} int main() { //Myclass m1; // ERROR : template constraint failure for ‘template requires as_int class Myclass’ Myclass m_i; // OK foo(15); // OK } * Örnek 5, #include #include #include #include void foo(std::convertible_to auto x) { std::cout << "x: " << x << '\n'; } void foo(std::integral auto x) { std::cout << "x: " << x << '\n'; } // II /* Alternative - I template void foo(T x) { std::cout << "x: " << x << '\n'; } Alternative - II template requires std::integral void foo(T x) { std::cout << "x: " << x << '\n'; } */ int main() { foo(std::string{ "Merve" }); // x: Merve foo(31); // x: 31 foo(3.1); // ERROR: no matching function for call to ‘foo(double)’ } * Örnek 6, #include #include #include #include std::integral auto bar(int x) { return x * 1.1; } // ERROR: deduced return type does not satisfy placeholder constraints int foo() { return 1; } double func() { return 1.1; } int main() { std::integral auto x = foo(); std::integral auto y = func(); // ERROR: deduced initializer does not satisfy placeholder constraints } * Örnek 7, #include #include #include #include #include int main() { // 0 0 0 0 0 std::vector ivec(5); for(std::integral auto x : ivec){ std::cout << x << ' '; } } * Örnek 8, #include #include #include #include #include #include template void func(T x) { if constexpr (std::integral){ std::cout << "Tam sayi turler...\n"; } else if constexpr (std::floating_point){ std::cout << "Gercek sayi turler...\n"; } else { std::cout << "Diger turler...\n"; } } int main() { /* # OUTPUT # Tam sayi turler... Gercek sayi turler... Diger turler... */ func(12); func(1.2); func("Merve"); } * Örnek 9, #include #include #include #include #include #include template concept additive = requires (T x, T y){ x+y; // "T" türünden iki nesne "+" operatörünün operandı olduğunda, geçerli bir ifade olacak. x-y; // "T" türünden iki nesne "-" operatörünün operandı olduğunda, geçerli bir ifade olacak. }; template void func(T x) { if constexpr (additive){ std::cout << "necati ergin...\n"; } else { std::cout << "eray goksu...\n"; } } int main() { /* # OUTPUT # necati ergin... eray goksu... */ func(12); int x = 10; int* p = &x; func(p); } * Örnek 10, #include #include // Sadece tek bir "bit" i "1" olacak ve // "N" değeri de "32" den büyük olacak. template requires (std::has_single_bit(N) && N > 32) class Myclass{ }; int main() { /* # OUTPUT # */ Myclass<5> m1; // ERROR: template constraint failure for ‘template requires std::has_single_bit(N) && N > 32 class Myclass’ Myclass<45> m2; // ERROR: template constraint failure for ‘template requires std::has_single_bit(N) && N > 32 class Myclass’ Myclass<64> m3; // OK } * Örnek 11, #include constexpr bool is_prime(int x) { if (x < 2) return false; if (x % 2 == 0) return x == 2; if (x % 3 == 0) return x == 3; if (x % 5 == 0) return x == 5; for(int i{7}; i * i <= x; i += 2) if (x % i == 0) return false; return true; } template requires (is_prime(N)) class Myclass{}; template concept IsPrime = is_prime(N); template requires IsPrime void func() {} int main() { /* # OUTPUT # */ Myclass<17> m1; // OK Myclass<18> m1; // ERROR: template constraint failure for ‘template requires is_prime()(N) class Myclass’ func<19>(); // OK } >> "requires expression" : Diğerleri gibi derleme zamanında "boolean" değer üretmesi gerekmektedir. Kendi içerisinde üç aşama barındırır. Bunlar: "Simple Requiretments", "Type Requiretments", "Compound Requiretments" ve "Nested Requirements". >>> "Simple Requiretments" : Belirtilen ifade, derleyici tarafından geçerli bir ifade olmalıdır. Örneğin, "T" herhangi bir tür olmak şartıyla, bu türden "x" değişkenimiz olsun. "x++;" ifadesi geçerli bir ifade olmalıdır. Bir diğer deyişle "x" değişkeni "++ son ek" operatörünün operandı olmalıdır. Burada "x" değişkeninin değeri bir arttırılmamaktadır. * Örnek 1, #include #include template concept Nec = requires (T x) { // Simple Requiretments x++; }; struct X{}; struct Y{ Y operator++(int){ return this; } }; int main() { /* # OUTPUT # */ static_assert(Nec); // "int" türden nesne "++" operatörünün operandı olabilir. static_assert(Nec); // "int*" türden nesne "++" operatörünün operandı olabilir. static_assert(Nec::iterator>); // "std::vector::iterator" nesnesi "++" operatörünün operandı olabilir. static_assert(Nec); // "X" türünden nesne "++" operatörünün operandı olamayacağı için, sentaks hatası olacaktır. static_assert(Nec); // "Y" türünden nesne "++" operatörünün operandı olabilir. } * Örnek 2, #include #include template concept Nec = requires (T x) { // Simple Requiretments *x; // "dereference" operatörünün operandı olabilecek. x[0]; // "[]" operatörünün operandı olabilecek. }; int main() { /* # OUTPUT # */ static_assert(Nec); // "int" türünden nesne içerik operatörünün operandı olamaz. static_assert(Nec); // "int*" türünden nesne hem içerik hem de "[]" operatörünün operandı olabilir. } * Örnek 3, #include #include #include #include template concept Neco = requires(T p) { p == nullptr; // "p" değişkeninin "nullptr" ile karşılaştırılabilir olması gerekmektedir. }; int main() { /* # OUTPUT # a: false b: true c: true */ std::boolalpha(std::cout); constexpr auto a = Neco; std::cout << "a: " << a << '\n'; constexpr auto b = Neco; std::cout << "b: " << b << '\n'; constexpr auto c = Neco>; std::cout << "c: " << c << '\n'; } * Örnek 4, #include #include #include #include template concept Neco = requires(T x /*, T y */) { x < x; // "x" değişkeninin "<" operatörünün operandı olabilmelidir. // x < y; // "T" türünden değişkenlerin, "<" operatörünün operandı olabilmeleri gerekmektedir. }; struct X{}; struct Y{ bool operator<(Y)const { return true; } }; int main() { std::boolalpha(std::cout); std::cout << Neco << ' ' << Neco << ' ' << Neco << '\n'; // true false true } * Örnek 5, #include #include #include #include #include template concept Neco = requires(T) { std::is_integral_v; // "std::is_integral_v" biçiminde yazmak SENTAKS HATASI OLMAMALI. // Yoksa buradaki "std::is_integral_v" ifadesinin değerinin ne olduğu // önemli değildir. }; int main() { constexpr auto a = Nec; // "a" is "true" } * Örnek 6, #include #include #include #include #include template concept Neco = requires(T p) { *p; **p; }; int main() { constexpr auto a = Nec; // "a" is "false" constexpr auto b = Nec; // "b" is "true" } * Örnek 7, #include #include #include #include #include template concept Neco = requires(T p) { ++**p; }; int main() { constexpr auto a = Nec; // "a" is false constexpr auto b = Nec; // "b" is "true" } * Örnek 8, #include #include #include #include #include template concept Printable = requires(T p) { std::cout << p; // "std::cout << p;" ifadesini yazmak SENTAKS HATASI OLMAMALIDIR. p.print(); // "p.print();" ifadesini yazmak SENTAKS HATASI OLMAMALIDIR. Bir diğer // deyişle "print" isminde üye fonksiyonunun olması gerekmektedir. }; struct A{}; struct B{ friend std::ostream& operator<<(std::ostream& os, const B& other) { return os; } }; struct C{ friend std::ostream& operator<<(std::ostream& os, const C& other) { return os; } void print() { } }; int main() { /* # OUTPUT # false false false false true */ constexpr auto a = Printable; // "a" is "false" constexpr auto b = Printable; // "b" is "false" constexpr auto c = Printable; // "c" is "false" constexpr auto d = Printable; // "d" is "false" constexpr auto e = Printable; // "e" is "true" std::boolalpha(std::cout); std::cout << a << '\n' << b << '\n' << c << '\n' << d << '\n' << e << '\n'; } * Örnek 9, #include #include #include #include #include template requires // start of "requires clause" requires(T x){ // start of "requires expression" x+x; // "T" türünden nesnelerin toplanabilir x-x; // ve çıkartılabilir olması gerekmektedir. }// end of "requires expression" && std::integral // "concepts" : "T" türünün bir tam sayı olması gerekmektedir. // end of "requires clause" class Myclass{}; int main() { Myclass m1; // OK Myclass m2; // ERROR: template constraint failure for // ‘template requires requires(T x) {x + x;x - x;} && (integral) class Myclass’ } * Örnek 10, #include #include #include #include #include template requires requires(T x){ std::cout << x; // "std::cout << x;" ifadesinin SENTAKS HATASI OLUŞTURMAMASI GEREKİYOR. *x; // "*x;" ifadesinin SENTAKS HATASI OLUŞTURMAMASI GEREKİYOR. sizeof(x) > 100; // "sizeof(x) > 100;" ifadesini yazmak, hiçbir zaman için, SENTAKS HATASI oluşturmaz. } class Myclass{}; template requires requires(T x){ std::cout << x; // "std::cout << x;" ifadesinin SENTAKS HATASI OLUŞTURMAMASI GEREKİYOR. *x; // "*x;" ifadesinin SENTAKS HATASI OLUŞTURMAMASI GEREKİYOR. } && (sizeof(T) > 4) // Artık "T" türünün "sizeof" değerinin de dörtten büyük olması gerekmektedir. class Neco{}; template concept my_custom_constraint = requires { sizeof(int) > 100; // "sizeof(int) > 100;" ifadesi hiç bir zaman sentaks hatası oluşturmaz. Yani "Always True". std::is_integral_v; // "std::is_integral_v;" ifadesi de hiç bir zaman sentaks hatası oluşturmaz. Yani "Always True". }; // Yani aslında hiç bir "constraints" mevcut değildir. int main() { constexpr bool a = my_custom_constraint; std::cout << "a: " << std::boolalpha << a << '\n'; constexpr bool b = Neco; // ERROR: template constraint failure for ‘template requires requires(T x) {std::cout << x;*x;} && sizeof (T) > 4 class Neco’ constexpr bool c = Myclass; // ERROR: template constraint failure for ‘template requires requires(T x) {std::cout << x;*x;sizeof x > 100;} class Myclass’ } * Örnek 11, #include #include #include #include #include template concept my_custom_constraint = requires { requires (sizeof(T) > 100); // "sizeof(int) > 100" ifadesinin "true" değer döndürmesi gerekmektedir. requires (!std::is_integral_v); // "!std::is_integral_v" ifadesinin "true" değer döndürmesi gerekmektedir. }; struct Buffer{ char m_buffer[2000]{}; }; int main() { std::cout << std::boolalpha << my_custom_constraint << '\n'; // true } >>> "Type Requiretments": Derleyici bir türün var olduğunu ve o türün kullanımının geçerli olduğunu sınamak zorunda. Burada "typename" anahtar sözcüğünü kullanmak zorundayız. Örneğin, "T" herhangi bir tür olmak şartıyla, "typename T::value_type;" ifadesi demek "T" türünün alt türü olarak "value_type" türünün olması gerekmektedir. Burada "using" bildirimi de kullanılabilir, "sub-class" olarak da. Önemli olan "value_type" a karşılık bir türün olmasıdır. * Örnek 1, #include #include #include #include #include template concept my_custom_constraint = requires { typename T::value_type; }; struct Eray{}; struct Neco{ using value_type = int; }; int main() { constexpr auto a = my_custom_constraint; // "int" türü alt tür olarak "value_type" türüne // sahip olmadığından, "a" değişkeni "false" değerdedir. constexpr auto b = my_custom_constraint>; // "std::vector" türü alt tür olarak // "value_type" türüne sahip olduğundan dolayı // "b" değişkeni "true" değerdedir. constexpr auto c = my_custom_constraint::iterator>; // "std::vector" türü alt tür olarak // "value_type" türüne sahip olduğundan dolayı // "c" değişkeni "true" değerdedir. constexpr auto d = my_custom_constraint; // "Eray" türü "value_type" türünden alt türe sahip değildir. Bu yüzden // "d" değişkeni "false" değerdedir. constexpr auto e = my_custom_constraint; // "Neco" türü "value_type" türünden alt türe sahiptir. Dolayısıyla // "e" değişkeni "true" değerdedir. } * Örnek 2, #include #include #include #include #include template class Myclass{ // Burada "T" türü herhangi bir tür olabilir. }; template class Neco{ // Burada "T" türü tam sayı türlerinden birisi olmak zorundadır. }; template concept Eray = requires{ typename Myclass; // "T" türü ne ise "Myclass" sınıf şablonunun // o tür nezdinde açılabilir olması gerekiyor. }; template concept Deniz = requires{ typename Neco; // "T" türü ne ise "Neco" sınıf şablonunun // o tür nezdinde açılabilir olması gerekiyor. }; int main() { constexpr auto a = Eray; // "T" için "int" gelmekte. "Myclass" geçerli // olması hasebiyle, "a" değişkeni "true" değerde. constexpr auto b = Deniz; // "T" için "double" gelmekte. "Neco" geçerli // değildir çünkü "Neco" için "T" ye karşılık sadece // tam sayı türleri gelebilir. Dolayısıyla "b" değişkeni // "false" değerdedir. } * Örnek 3, #include #include #include #include #include // Way - I template concept check_if_moveable_v1 = requires { requires (std::is_move_constructible_v); }; // Way - II template concept check_if_moveable_v2 = (std::is_move_constructible_v); int main() { //... } /*================================================================================================================================*/ (18_27_08_2023) > "concepts" (devam): >> "requires expression" (devam): Aşağıda hatırlatıcı örnekler bulunmaktadır. * Örnek 1, #include #include template requires (sizeof(T) > 64) class Myclass {}; int main() { // Myclass m1; // ERROR: the associated constraints are not satisfied std::cout << "sizeof mt19937 : " << sizeof(std::mt19937) << '\n'; // sizeof mt19937 : 5000 Myclass eng; // OK } * Örnek 2, #include #include #include template requires std::is_pointer_v || std::same_as void foo(T x) {} template requires std::is_pointer_v || std::is_same_v void func(T x) {} int main() { int ival{}; foo(&ival); foo(nullptr); double dval{}; func(&dval); func(nullptr); } * Örnek 3, #include #include #include template requires (!std::convertible_to) void foo(T x) {} template requires std::convertible_to void func(T, U) {} template requires std::is_convertible_v void bar(T, U) {} int main() { foo("Merve"); // ERROR: the associated constraints are not satisfied foo(4.5); // OK func(2, 5.6); // OK int x{}; func(x, &x); // ERROR: the associated constraints are not satisfied } * Örnek 4, #include #include #include template requires std::integral())>> void foo(T) {} /* * (I) Buradaki "std::integral())>>" ifadesi, * "true" olması sentaks hatası oluşmayacaktır. * (II) Bunun için de "std::remove_reference_t())>" ifadesinin "std::integral" * "concept" ini karşılaması gerekmektedir. * (III) "std::declval()" ifadesi ise "T" türünden bir nesne oluşturma ifadesidir. Tıpkı "T{}" veya * "T()" gibi. Burada "declval" fonksiyon şablonunun kullanılma amacı, "T{}" veya "T()" biçimindeki kullanımda * ilgili tür için "Default Ctor." olması gerekmesidir. * (IV) "*std::declval()" ifadesi ise "std::declval()" ifadesinin "dereference" edilebilir olması * demektir. * (V) "decltype(*std::declval())" ifadesi ise "std::declval()" ifadesi "dereference" edildiğinde * oluşan ifadenin türü demektir. * (VI) "std::remove_reference_t())>" ifadesi ise yukarıda "decltype" ile elde * ettiğimiz türün referans olması halinde, referanslığını atmaktadır. * (VII) Böylelikle "std::integral())>>" ifadesiyle biz, * "T" türünden bir nesnenin "dereference" edildiğinde "std::integral" konseptini karşılayıp karşılamadığını * sorgulamış oluyoruz. */ int main() { int x{}; foo(&x); // "T" türü "int*" oldu. "dereference" edildiğinde "int" türü elde edildi. // "std::integral" konseptini karşılamış oldu. double dval{}; foo(&dval); // "T" türü "double*" olacaktır. "dereference" edildiğinde "double" türü elde // edilecektir. "std::integral" konsepti KARŞILANMAMIŞ olacaktır. std::optional op{ 45 }; foo(op); // "op" nesnesi "dereference" edildiğinde "int&" türü elde edilmekte. Referanslık // sıyrılacak ve "int" türü elde edilecek. Bu da "std::integral" konseptini karşılamış // oldu. } * Örnek 5.0, template concept my_custom_concept = requires(T x, U y) { x.foo() || y.bar(); // "x.foo() || y.bar();" ifadesinin yazılması sentaks hatası // OLUŞTURMAMALIDIR. }; struct A { void foo() {} }; struct B {}; struct C { void bar() {} }; int main() { constexpr auto a = my_custom_concept; // "a" is "false". Çünkü "B" sınıfında ".bar()" // fonksiyonu olmadığı için normalde sentaks // hatası oluşacaktır. Fakat burada konsept // karşılanmadığı için "false" değerini almıştır. // Dolayısıyla "||" operatörünü kullanırken dikkatli // olmalıyız. Normal şartlarda "||" operatörünün sol // operandı "true" ise sağ operandına bakılmaz. constexpr auto b = my_custom_concept; // Artık "b" değişkeninin değeri "true" olacaktır. Çünkü // her iki sınıf da bünyesinde ilgili fonksiyonları // barındırmaktadır. } * Örnek 5.1, #include #include template concept my_custom_concept = requires(T x) { x.foo(); } || requires (T x) { x.bar(); }; struct A { void foo() {} }; struct B {}; int main() { constexpr auto a = my_custom_concept; // "a" is "true". Çünkü artık // "requires(T x) { x.foo(); } || requires (T x) { x.bar(); }" // ifadesinin "true" olması halinde sonuç "true" olacaktır. Yani // "constraint" ifadelerinin kendilerinin "||" ile birleştirilmesi // gerekmektedir. } * Örnek 5.2, #include #include template concept has_foo = requires(T x) { x.foo(); }; template concept has_bar = requires(T x) { x.bar(); }; template requires has_foo || has_bar class Myclass {}; struct A { void foo() {} }; struct B {}; struct C { void bar() {} }; int main() { Myclass m1; Myclass m2; Myclass m3; } * Örnek 6, #include #include template concept my_concept_v1 = requires{ std::declval().foo(); }; template concept my_concept_v2 = requires (T x){ x.foo(); }; int main() { //... } >>> "Type Requiretments" (devam): * Örnek 1, #include #include #include struct Master { struct value_type { using first_type = int; using second_type = double; using third_type = std::pair; }; }; struct Master_v1 { struct value_type { using first_type = int; using second_type = double; }; }; struct Master_v2 { struct value_type { using first_type = int; }; }; struct Master_v3 { struct value_type { }; }; struct Master_v4 { }; template requires requires{ typename C::value_type::first_type; typename C::value_type::second_type; } class Necati{}; template concept my_custom_concept_v1 = requires{ typename C::value_type::first_type; typename C::value_type::second_type; }; template concept my_custom_concept_v2 = requires(typename T::value_type::third_type) { true; // Bu şekilde kullanırsak, buraya bir şey yazmak zorundayız. }; template concept my_custom_concept_v3 = requires(typename T::value_type::third_type x) { std::cout << x; }; struct A{}; using my_a_type = std::vector; int main() { { Necati n1; // OK Necati n2; // OK Necati n3; // ERROR: the associated constraints are not satisfied Necati n4; // ERROR: the associated constraints are not satisfied Necati n5; // ERROR: the associated constraints are not satisfied } { constexpr auto a = my_custom_concept_v1; // "a" is "true". constexpr auto b = my_custom_concept_v1; // "b" is "true". constexpr auto c = my_custom_concept_v1; // "c" is "false". constexpr auto d = my_custom_concept_v1; // "d" is "false". constexpr auto e = my_custom_concept_v1; // "e" is "false". } { constexpr auto a = my_custom_concept_v2; // "a" is "true". constexpr auto b = my_custom_concept_v2; // "b" is "false". constexpr auto c = my_custom_concept_v2; // "c" is "false". constexpr auto d = my_custom_concept_v2; // "d" is "false". constexpr auto e = my_custom_concept_v2; // "e" is "false". } { constexpr auto a = my_custom_concept_v3; // "a" is "false". constexpr auto b = my_custom_concept_v3; // "b" is "false". constexpr auto c = my_custom_concept_v3; // "c" is "false". constexpr auto d = my_custom_concept_v3; // "d" is "false". constexpr auto e = my_custom_concept_v3; // "e" is "false". } { constexpr auto a = my_custom_concept_v3; // "a" is "false". constexpr auto b = my_custom_concept_v3; // "b" is "false". constexpr auto c = my_custom_concept_v3; // "c" is "false". constexpr auto d = my_custom_concept_v3; // "d" is "false". constexpr auto e = my_custom_concept_v3; // "e" is "false". } } >>> "Compound Requirements" : "{}" kullandığımız durumlardır. Üç farklı yöntemi vardır. >>>> Sadece "{}" kullanılması: * Örnek 1, #include #include template concept Nec = requires (T x){ // Compound Requirement {x.foo()}; // x.foo(); }; struct A{}; struct B{ void foo(void) {} }; struct C{ void foo(void) noexcept {} }; int main() { std::boolalpha(std::cout); std::cout << Nec << '\n'; // false std::cout << Nec << '\n'; // true std::cout << Nec << '\n'; // true } >>>> "{}" ile birlikte "noexcept" gibi şeylerin kullanılması: * Örnek 1, #include #include template concept Nec = requires (T x){ // Compound Requirement {x.foo()}; // x.foo(); {*x}noexcept; // "*x" ifadesinin yürütülmesi "noexcept" garantisi vermektedir. }; struct A{}; struct B{ void foo(void) {} }; struct C{ void foo(void) noexcept {} }; struct D{ void foo(void) {} D& operator*() { return *this; } }; struct G{ void foo(void) {} G& operator*() noexcept { return *this; } }; struct E{ void foo(void) noexcept {} E& operator*() { return *this; } }; struct F{ void foo(void) noexcept {} F& operator*() noexcept { return *this; } }; int main() { std::boolalpha(std::cout); std::cout << Nec << '\n'; // false std::cout << Nec << '\n'; // false std::cout << Nec << '\n'; // false std::cout << Nec << '\n'; // false std::cout << Nec << '\n'; // false std::cout << Nec << '\n'; // true std::cout << Nec << '\n'; // true } >>>> "{}" ile birlikte "->" operatörünün kullanılması. Bu kullanım ile amaç "{}" içerisindeki ifadeden elde edilmesi gereken türün ne olması gerektiği belirlenmektedir. Fakat türü doğrudan yazamıyoruz. Buradaki türü bir "concept" olarak belirmemiz gerekmektedir. Dolayısıyla "std::same_as" konseptini kullanmalıyız. Buradaki çalışma mekanizması da aşağıdaki gibidir: //... {x.foo()} -> std::same_as; //... Normal şartlarda "std::same_as" konsepti iki parametrelidir. Buradaki parametrelerden birisi "decltype(x.foo())" ile otomatik olarak elde edilirken, ikinci parametre ise bizim belirttiğimiz "int" türüdür. Benzer şekilde "{}" içerisindeki ifadenin türünün tam sayı olmasını istiyorsak; //... {x.foo()} -> std::integral; //... Buradaki "std::integral" in tek parametresi ise "decltype(x.foo())" ile elde edilmiş olduğundan, nihai hedefe ulaşmış oluyoruz. * Örnek 1, #include #include template concept Nec = requires (T x){ // Compound Requirement {x.foo()} noexcept -> std::integral; }; struct A{}; struct B{ void foo(void) {} }; struct C{ void foo(void) noexcept {} }; struct D{ int foo(void) noexcept { return 1; } }; int main() { std::boolalpha(std::cout); std::cout << Nec << '\n'; // false std::cout << Nec << '\n'; // false std::cout << Nec << '\n'; // false std::cout << Nec << '\n'; // true } * Örnek 2, #include #include template concept any_concept = requires (T x, U y){ { x+y } -> std::common_with; // "x+y" ifadesinin türü "std::common_type" // ifadesinin türü olmalıdır. }; struct A{}; struct B{ B() = default; B(int) {} }; struct C{ C() = default; C(int) {} operator int() { return 1; } }; int main() { std::boolalpha(std::cout); std::cout << any_concept << '\n'; // true std::cout << any_concept << '\n'; // true std::cout << any_concept << '\n'; // true std::cout << any_concept << '\n'; // true std::cout << any_concept << '\n'; // true std::cout << any_concept << '\n'; // false std::cout << any_concept << '\n'; // false std::cout << any_concept << '\n'; // true } >>> "Nested Requirements" : "Require Expression" içerisinde "Require Closure" kullanılmasıdır. * Örnek 1, #include #include /* (IV) template concept Reference = std::is_reference_v; */ template concept Nec = /* (I) => */ /* std::is_reference_v && */ requires (T x){ std::is_reference_v; // "std::is_reference_v;" ifadesini yazmak sentaks hatası // OLUŞTURMAMALI. Burada "T" türünün referanslığı sorgulanmamaktadır. // Eğer "T" türünün referanslığının sorgulanmasını istiyorsak // ya "I" ya "II" ya da "III" nolu yöntemi kullanabiliriz. // (II) // requires std::is_reference_v; // (III) // requires requires (T) { std::is_reference_v; } // Hatta referans sorgulamasını ayrı bir konsept altında da yapabiliriz. (IV) // requires Reference; }; int main() { //... } Şimdi de kritik noktaları gösteren örnekleri inceleyelim: * Örnek 1, #include #include template concept c1 = std::integral; template concept c2 = requires { std::integral; // !!! }; template concept c3 = requires { requires std::integral; }; template class Myclass{}; template concept c4 = requires { typename Myclass; }; int main() { /* # OUTPUT # true false true true true false true false */ std::boolalpha(std::cout); std::cout << c1 << ' ' << c1 << '\n'; std::cout << c2 << ' ' << c2 << '\n'; std::cout << c3 << ' ' << c3 << '\n'; std::cout << c4 << ' ' << c4 << '\n'; } * Örnek 2, #include #include // constraint: // *p && p[] // WAY - I template concept Nec_v1_1 = requires (T p){ *p; p[0]; }; // WAY - II template concept Dereferencable_v1 = requires (T x) { *x; }; template concept Subscriptable_v1 = requires (T x) { x[0]; }; template concept Nec_v1_2 = Dereferencable_v1 && Subscriptable_v1; // constraint: // *p || p[] // WAY - I template concept Dereferencable_v2 = requires (T x) { *x; }; template concept Subscriptable_v2 = requires (T x) { x[0]; }; template concept Nec_v2_1 = Dereferencable_v2 || Subscriptable_v2; // WAY - II template concept Nec_v2_2 = requires (T x) { *x; } || requires (T x) { x[0]; }; // WAY - III: Different Meaning!!! template concept Nec_v2_3 = requires (T x){ *x || x[0]; }; int main() { /* # OUTPUT # */ //... } * Örnek 3, #include #include template concept hashable = requires { // typename std::hash; // "typename std::hash" ifadesi "std::hash" türü oluşturulabilir mi demektir. // "std::hash" in herhangi bir türden açılımı oluşturulabilir. std::hash{}; // "std::hash" in "T" açılımı türünden bir nesneyi oluşturabiliriz, demektir. }; /* // WAY - I template requires hashable void foo(T) { std::hash x; //... } */ /* // WAY - II template void bar(T) { std::hash x; //... } */ // WAY - III void func(hashable auto) { //... } struct A{}; template<> struct std::hash{ std::size_t operator()(const A&)const { return 3; } }; int main() { func(A{}); // OK: Çünkü yukarıda "std::hash" için kendi sınıfımız için // açılabilir olmasını kıldık. Eğer "template<>" kısmı // olmasaydı, bu kod parçası sentaks hatası oluşturacaktı. // Böylelikle bizler "custom" türlerimiz için "std::hash" açılımının // olup olmadığını sorgulayabiliriz. } * Örnek 4, #include #include template concept HasFoo = requires (T x) { { x.foo() } noexcept -> std::convertible_to; // "x.foo()" ile elde ettiğimiz tür, bool" türüne dönüştürülebilir olmalıdır. { x.bar() } noexcept -> std::same_as; // "x.bar()" ile elde ettiğimiz tür ise "bool" türü olmalıdır. }; struct Nec{ int foo()const noexcept { return 1; } int bar()const noexcept { return 11; } }; struct Erg{ int foo()const noexcept { return 1; } bool bar()const noexcept { return 11; } }; int main() { std::cout << HasFoo << '\n'; // false std::cout << HasFoo << '\n'; // true } * Örnek 5, #include #include // LEVEL - I template concept HasTheFunctions = requires (T x) { { x.foo() } noexcept -> std::convertible_to; // "x.foo()" ile elde ettiğimiz tür, bool" türüne dönüştürülebilir olmalıdır. { x.bar() } noexcept -> std::same_as; // "x.bar()" ile elde ettiğimiz tür ise "bool" türü olmalıdır. }; template requires HasTheFunctions class StepOne{}; template requires HasTheFunctions T FuncOne(T x) { return x; } template T FooOne(T x) requires HasTheFunctions { return x; } // LEVEL - II template concept IsItFinal = std::integral && HasTheFunctions; template requires IsItFinal class StepTwo{}; template requires IsItFinal T FuncTwo(T x) { return x; } template T FooTwo(T x) requires IsItFinal { return x; } // LEVEL - III template // "Constrained Template Param." class Last{}; template T FuncThree(T x) { return x; } // LEVEL - IV auto FuncFour(IsItFinal auto x) { return x; } // "Aggregate Template Syntax" IsItFinal auto FooFour(IsItFinal auto x) { return x; } // "Aggregate Template Syntax" int main() { //... } * Örnek 6, #include #include int main() { auto f = [](T x) {}; f(12); auto g = [](T x){}; g(12); auto h = [](T x)-> typename T::value_type {}; h(12); auto i = [](T x)-> double{ return 1.; }; i(12); auto j = [](std::integral auto)->std::integral auto { return 1; }; j(12); auto k = [](T x)-> T {}; k(12); } * Örnek 7, #include #include template concept nec = requires (T x) { {++x}noexcept->std::convertible_to; requires std::integral; }; int main() { //... } * Örnek 8, #include #include template concept Necible = requires (T t, U u, W w){ ++t; --u; w.foo(); }; void foo(Necible auto) {} // Buradaki fonksiyon parametresi olarak diğeri derleyici tarafından yazıldığı için yazılmamıştır. int main() { } Bir fonksiyonun sadece belirli bir argümanla çağrılmasına izin vermek; örneğin, sadece "int" ile çağrılabilen bir fonksiyon: * Örnek 1.0, Klasik yöntem. #include #include template void func(T) = delete; void func(int) {} int main() { /* * Yukarıdaki iki "func" fonksiyonu da birbirini * "overload" etmektedir. Dilin kuralına göreyse; * Argümanla parametre uyuyorsa, gerçek fonk. * Argümanla parametre uymuyorsa, o şablondan bir * "specialization" oluşturulacak. Yukarıda da bu * "specialization" "delete" edildiği için sentaks * hatası oluşacak. */ func(12); // OK func(1.2); // ERROR: use of deleted function ‘void func(T) [with T = double]’ func(1.2f); // ERROR: use of deleted function ‘void func(T) [with T = float]’ func('f'); // ERROR: use of deleted function ‘void func(T) [with T = char]’ func(12u); // ERROR: use of deleted function ‘void func(T) [with T = unsigned int]’ } * Örnek 1.1, "Substitution Failure is not an error" / "SFINAE": #include template> * = nullptr> void func(T); int main() { func(12); // OK func(1.2); // ERROR: no matching function for call to ‘func(double)’ func(1.2f); // ERROR: no matching function for call to ‘func(float)’ func('f'); // ERROR: no matching function for call to ‘func(char)’ func(12u); // ERROR: no matching function for call to ‘func(unsigned int)’ } * Örnek 1.2.0, #include #include template requires std::same_as void func(T x) {} int main() { func(12); // OK func(1.2); // ERROR: no matching function for call to ‘func(double)’ func(1.2f); // ERROR: no matching function for call to ‘func(float)’ func('f'); // ERROR: no matching function for call to ‘func(char)’ func(12u); // ERROR: no matching function for call to ‘func(unsigned int)’ } * Örnek 1.2.1, #include #include template T> void func(T x) {} int main() { func(12); // OK func(1.2); // ERROR: no matching function for call to ‘func(double)’ func(1.2f); // ERROR: no matching function for call to ‘func(float)’ func('f'); // ERROR: no matching function for call to ‘func(char)’ func(12u); // ERROR: no matching function for call to ‘func(unsigned int)’ } * Örnek 1.2.2, #include #include void func(std::same_as auto x) {} int main() { func(12); // OK func(1.2); // ERROR: no matching function for call to ‘func(double)’ func(1.2f); // ERROR: no matching function for call to ‘func(float)’ func('f'); // ERROR: no matching function for call to ‘func(char)’ func(12u); // ERROR: no matching function for call to ‘func(unsigned int)’ } Standart konseptleri inceleyelim: * Örnek 1, #include #include // WAY - I template void func1(F fn) { fn(100); } // WAY - II template requires std::invocable void func2(F fn) { fn(100); } // WAY - III template void func3(F fn) requires std::invocable { fn(100); } // WAY - IV template F> void func4(F fn) { fn(100); } // WAY - V void func5(std::invocable auto f) { f(100); } void func5(std::invocable auto f) { f(100, 100.001); } auto f = [](int x){ std::cout << x*x << '\n'; }; auto g = [](int x, double y){ std::cout << x+x * y+y<< '\n'; }; int main() { func1(f); func2(f); func3(f); func4(f); func5(f); func5(g); } * Örnek 2, #include #include #include void func(std::regular auto x) {} // "regular" olabilmesi için "Default Init.", // "Copyable", "Moveable", "Swapable" ve // "Equality Comparison" işlevlerini desteklemeli. void func(std::semiregular auto x) {} // "semiregular" supports "Default Init.", "Copy", // "Move" and "Swapable". struct Merve{}; struct Menekse{ bool operator==(const Menekse&) const = default; }; int main() { func(12); // OK: Regular func(std::string{"Merve"}); // OK: Regular func(Merve{}); // OK: Semiregular func(Menekse{}); // OK: Regular } "concepts" kullanmanın ilave bir avantajı ise "overloading" tarafıdır. Fakat buna yönelik de bir takım kurallar vardır. * Örnek 1, #include #include #include void foo(std::integral auto) { std::cout << "integral\n"; } void foo(std::unsigned_integral auto) { std::cout << "unsigned_integral\n"; } int main() { foo(12); // integral foo(12u); // unsigned_integral foo(21L); // integral foo(21uL); // unsigned_integral } * Örnek 2, #include #include #include template concept Nec = requires (T x) { x.foo(); }; template concept Erg = Nec && requires (T x) { x.bar(); }; void func(Nec auto) { std::cout << "Nec auto\n"; } void func(Erg auto) { std::cout << "Erg auto\n"; } struct A{ void foo() {} }; struct B{ void foo() {} void bar() {} }; int main() { func(A{}); // Nec auto func(B{}); // Erg auto } * Örnek 3, #include #include #include template concept Nec = requires (T x) { x.foo(); }; template concept Erg = Nec || requires (T x) { x.bar(); }; void func(Nec auto) { std::cout << "Nec auto\n"; } void func(Erg auto) { std::cout << "Erg auto\n"; } struct A{ void foo() {} }; struct B{ void foo() {} void bar() {} }; int main() { func(A{}); // Nec auto func(B{}); // Nec auto } Şimdi de "concepts" ler arasındaki kısıtlama ilişkisine değinelim: * Örnek 1, #include #include template concept integral_or_floating = std::integral || std::floating_point; template concept integral_and_char = std::integral && std::same_as; void f(std::integral auto) { std::cout << "std::integral\n"; } // 1 void f(integral_or_floating auto) { std::cout << "std::integral_or_floating\n"; } // 2 void f(std::same_as auto) { std::cout << "same_as\n"; } // 3 // void f(integral_and_char auto) { std::cout << "integral_and_char\n"; } // 4 int main() { f(5); // Burada daha kısıtlayıcı hangisi ise o seçilecektir. // Yani ya "1" ya "2" seçilecektir. "2" için "||" operatörü // kullanıldığı için, "1" den daha kısıtlayıcı DEĞİLDİR. Bu // durumda "1" daha kısıtlayıcıdır. f(2.3); // Sadece "2" uygundur. O çağrılacaktır. f('A'); // "1", "2" ve "3" uymaktadır. Fakat buradaki konseptler arasında bir // kısıtlayıcı etki YOKTUR. Dolayısıyla şu anda "ambiguous" oluşacaktır. // Bu hatayı gidermek için "4" numara gibi bir fonksiyon yazmalıyız. } > Hatırlatıcı Notlar: >> "std::declval" : Derleme zamanında çağrılan bir fonksiyondur. Geri dönüş değeri, o türe ilişkin bir referanstır. "R-Value" veya "L-Value" referans olması fark etmemektedir. * Örnek 1, class Necati{ public: Myclass(); int foo() const { return 1; } }; class Ergin{ public: Myclass(int); int foo() const { return 2; } }; int main() { decltype(Necati{}.foo()); // Bu ifade sentaks hatası oluşturmayacaktır. decltype(Ergin{}.foo()); // Bu ifade sentaks hatası oluşturacaktır. Çünkü sınıfın "Default Ctor." YOKTUR. decltype(std::declval().foo()); // Artık sentaks hatası oluşmayacaktır. Fakat bu fonksiyonu sadece // "Unevaluated Context" içerisinde çağırmak zorundayız. } >> Uzun fonksiyonların nasıl daha kısa gale getirildiğine ilişkin örnek: * Örnek 1, #include #include #include // WAY - I void aggregateAndDisplay_v1(std::map const& source, std::map const& destination) { auto aggregatedMap = destination; for(auto const& sourceEntry: source){ auto destinationPosition = aggregatedMap.find(sourceEntry.first); if(destinationPosition == aggregatedMap.end()) aggregatedMap.insert(std::make_pair(sourceEntry.first, sourceEntry.second)); else aggregatedMap[sourceEntry.first] = sourceEntry.second + " or " + destinationPosition->second; } for(auto const& entry: aggregatedMap){ std::cout << "Available translation for " << entry.first << " : " << entry.second << '\n'; } } // WAY - II void aggregateAndDisplay_v2(std::map const& source, std::map const& destination) { const auto aggregatedMap = [&]() { auto aggregatedMap = destination; for(auto const& sourceEntry: source){ auto destinationPosition = aggregatedMap.find(sourceEntry.first); if(destinationPosition == aggregatedMap.end()) aggregatedMap.insert(std::make_pair(sourceEntry.first, sourceEntry.second)); else aggregatedMap[sourceEntry.first] = sourceEntry.second + " or " + destinationPosition->second; } return aggregatedMap; }(); for(auto const& entry: aggregatedMap){ std::cout << "Available translation for " << entry.first << " : " << entry.second << '\n'; } } // WAY - III void aggregateAndDisplay_v3(std::map const& source, std::map const& destination) { const auto aggregatedMap = [](std::map const& destination, std::map const& source) { auto aggregatedMap = destination; for(auto const& sourceEntry: source){ auto destinationPosition = aggregatedMap.find(sourceEntry.first); if(destinationPosition == aggregatedMap.end()) aggregatedMap.insert(std::make_pair(sourceEntry.first, sourceEntry.second)); else aggregatedMap[sourceEntry.first] = sourceEntry.second + " or " + destinationPosition->second; } return aggregatedMap; }(source, destination); for(auto const& entry: aggregatedMap){ std::cout << "Available translation for " << entry.first << " : " << entry.second << '\n'; } } // WAY - IV namespace{ auto aggregateAndDisplay(std::map const& destination, std::map const& source) { auto aggregatedMap = destination; for(auto const& sourceEntry: source){ auto destinationPosition = aggregatedMap.find(sourceEntry.first); if(destinationPosition == aggregatedMap.end()) aggregatedMap.insert(std::make_pair(sourceEntry.first, sourceEntry.second)); else aggregatedMap[sourceEntry.first] = sourceEntry.second + " or " + destinationPosition->second; } return aggregatedMap; } }; void aggregateAndDisplay_v4(std::map const& source, std::map const& destination) { auto aggregatedMap = aggregateAndDisplay(destination, source); for(auto const& entry: aggregatedMap){ std::cout << "Available translation for " << entry.first << " : " << entry.second << '\n'; } } int main() { std::map m1{ { 10, "CPU" }, { 15, "GPU" }, { 20, "RAM" } }; std::map m2; /* # OUTPUT # Available translation for 10 : CPU Available translation for 15 : GPU Available translation for 20 : RAM */ aggregateAndDisplay_v4(m1, m2); /* # OUTPUT # Available translation for 10 : CPU Available translation for 15 : GPU Available translation for 20 : RAM */ aggregateAndDisplay_v3(m1, m2); /* # OUTPUT # Available translation for 10 : CPU Available translation for 15 : GPU Available translation for 20 : RAM */ aggregateAndDisplay_v2(m1, m2); /* # OUTPUT # Available translation for 10 : CPU Available translation for 15 : GPU Available translation for 20 : RAM */ aggregateAndDisplay_v1(m1, m2); } /*================================================================================================================================*/ (19_02_09_2023) > "concepts" (devam): * Örnek 1.0, #include template concept has_foo = requires (T t) { t.foo(); }; template concept has_foo_bar = requires (T t) { t.foo(); t.bar(); }; void func(has_foo auto) {} void func(has_foo_bar auto) {} struct Nec { void foo() {} }; struct Erg{ void foo() {} void bar() {} }; int main() { func(Erg{}); // Görünürde daha kısıtlayıcı olan "has_foo_bar" olandır. // Fakat bu iki konsept arasında "subsumption" olmadığından // "ambigous" hatası oluşacaktır. Alttaki konseptin üsttekini // "&&" operatörü ile birlikte kapsaması gerekmektedir. } * Örnek 1.1, #include template concept has_foo = requires (T t) { t.foo(); }; template concept has_foo_bar = has_foo && requires (T t) { t.bar(); }; void func(has_foo auto) {} void func(has_foo_bar auto) {} struct Nec { void foo() {} }; struct Erg{ void foo() {} void bar() {} }; int main() { func(Erg{}); // Alttaki olan üsttekini "&&" operatörü ile "subsume" etmiştir. // Artık "has_foo_bar" parametreli fonksiyon çağrılacaktır. } * Örnek 2.0, #include template concept Integral = requires (T x) { requires std::is_integral_v; }; template concept SignedIntegral = requires (T x) { requires std::is_integral_v; requires std::is_signed_v; }; void f(Integral auto) {} void f(SignedIntegral auto) {} int main() { f(12); // Yine iki konsept arasında kapsayıcılık ilişkisi // olmadığından, "ambigous" hatası oluşacaktır. } * Örnek 2.1, #include template concept Integral = requires (T x) { requires std::is_integral_v; }; template concept SignedIntegral = Integral && requires (T x) { requires std::is_signed_v; }; void f(Integral auto) {} void f(SignedIntegral auto) {} int main() { f(12); // Alttaki, üstekini "subsume" edeceğinden ilgili // sentaks hatası ortadan kalkacaktır. } * Örnek 2.2, #include template concept Integral = requires (T x) { requires std::is_integral_v; }; template concept SignedIntegral = Integral || requires (T x) { requires std::is_signed_v; }; void f(Integral auto) {} void f(SignedIntegral auto) {} int main() { f(12); // Bu durumda "subsumtion" ilişkisi farklı // olacaktır. Artık "Integral" parametreli // fonksiyon çağrılacaktır. Eğer "&&" opt. // kullanılsaydı, "SignedIntegral" parametreli // fonksiyon çağrılacaktır. } * Örnek 3, #include #include void foo_v1(const std::convertible_to auto& x) { std::string str = x; //... } void foo_v2(const auto& x) requires std::convertible_to { std::string str = x; //... } template T> void foo_v3(const T& x) { std::string str = x; //... } template requires std::convertible_to void foo_v4(const T& x) { std::string str = x; //... } int main() { //... } * Örnek 4, #include #include #include #include template T> void foo(T x) { std::cout << static_cast(x) << '\n'; } int main() { std::boolalpha(std::cout); int x{ 43 }; foo(x); // "int" türünden "bool" türüne dönüşüm geçerlidir. foo(&x); // Gösterici türlerden "bool" türüne dönüşüm geçerlidir. foo(nullptr); // "nullptr" -> "bool" türüne örtülü dönüşüm mevcut değildir. foo("murat"); // Gösterici türlerden "bool" türüne dönüşüm geçerlidir. "T" is "const char*". foo(std::string{"murat"}); // "std::string" -> "bool" türüne örtülü dönüşüm mevcut değildir. foo(std::make_unique(43)); // İlgili tür dönüştürme operatör fonksiyonu "explicit" olduğu için // sentaks hatası meydana gelmektedir. } * Örnek 5, #include template class Myclass {}; struct A {}; struct B { B() = default; B(const B&) = default; B& operator=(const B&) = default; }; struct C { C() = delete; C(const C&) = delete; C& operator=(const C&) = delete; }; struct D { D() = default; D(const D&) = default; D& operator=(const D&) = default; D(D&&) = delete; D& operator=(D&&) = delete; }; int main() { Myclass m1; Myclass m2; Myclass m3; Myclass m4; // "C" is not copyable. Myclass m5; // "D" is not copyable. } * Örnek 6, #include #include #include #include template void bar(F fn, Iter iter) requires std::indirect_unary_predicate { std::cout << fn(*iter) << '\n'; } int main() { const auto pred = [](int x) { return x % 5 == 0; }; std::vector ivec{ 4, 50, 12, 20, 35, 7, 9, 40 }; std::boolalpha(std::cout); for (auto iter = ivec.begin(); iter != ivec.end(); ++iter) { bar(pred, iter); } } * Örnek 7, #include #include #include #include void func(std::invocable auto f, int x) { f(x); } void func(std::invocable auto f, int x) { f(x); } int main() { f( []() {}, 20 ); // ERROR f( [](int x) { return x * x; }, 20 ); // OK f( [](int x, double y) { return x * y; }, 40, 40. ); // OK } * Örnek 8, #include #include void foo(std::totally_ordered auto x) { //... } struct Nec {}; struct Erg { auto operator<=>(const Erg&) const = default; }; struct Cpp { bool operator==(const Cpp&) const = default; }; int main() { foo(1); foo(std::string{ "Merve" }); foo(Nec{}); // ERROR foo(Erg{}); foo(Cpp{}); // ERROR } > C++20 ile gelen irili/ufaklı değişiklikler: >> C++20 öncesinde negatif değerli işaretli tam sayıları sola kaydırmak "Tanımsız Davranış" iken, C++20 ile birlikte artık geçerli hale getirildi. C++20 öncesinde negatif değerli işaretli tam sayıları sağa kaydırırken soldan eklenecek rakamın "0" mı "1" olacağı implementasona bağlıyken, C++20 ile birlikte bu eklenen rakam "1" rakamı olacaktır. * Örnek 1, #include #include int main() { int x = -1; std::cout << std::format("{0:032b} {0}\n", x, x); // -0000000000000000000000000000001 -1 x <<= 1; // İşaretli tam sayılardan, değeri negatif olanları, // "bitsel sola kaydırma" işlemine tabii tutmak // "Tanımsız Davranış" a neden olmaktadır, C++17 itibariyle. // C++20 ile artık geçerli hale getirildi. Sağdan eklenen // rakamlar "0" olacaktır. C++20 ile negatif tam sayıların // temsilinin ikiye tümleyen aritmatiği ile yapılmasını garanti // altına alınmıştır. std::cout << std::format("{0:032b} {0}\n", x, x); // -0000000000000000000000000000010 -2 } * Örnek 2, #include #include int main() { int x = -2; std::cout << std::format("{0:032b} {0}\n", x, x); // -0000000000000000000000000000010 -2 x >>= 1; // Negatif tam sayıları sağa kaydırdığımız zaman, soldan // eklenecek rakamların "0" ya da "1" olması implementasona // bağlıdır, C++20 öncesinde. C++20 itibariyle iş bu işlem // sonucunda, soldan eklenecekler rakam "1" rakamı olacaktır. std::cout << std::format("{0:032b} {0}\n", x, x); // -0000000000000000000000000000001 -1 } >> İşaretli tam sayılar ile işaretsiz tam sayıların karşılaştırılmasında derleyiciler tipik olarak uyarı verirler. C++20 itibariyle "ssize" isimli global bir fonksiyon eklendi. Geri dönüş değeri işaretli tam sayı türüdür. Dolayısıyla işaretli tam sayı ile işaretsiz tam sayıları karşılaştırırken bu global fonksiyonu kullanmamız halinde derleyici uyarı mesajı vermeyecektir. * Örnek 1, #include int main() { std::vector ivec{ 3, 6, 1, 3, 2, 9 }; for (int i{}; i < size(ivec); ++i) { // warning C4018: '<': signed/unsigned mismatch //... } } * Örnek 2, #include int main() { std::vector ivec{ 3, 6, 1, 3, 2, 9 }; for (int i{}; i < ssize(ivec); ++i) { //... } } >> C++20 ile birlikte yapıların "bitfield" veri elemanları, sınıfların diğer elemanları gibi, "Default Member Init." ya da "In-class Init". ile ilk değer alabilmektedir. * Örnek 1, struct Nec { int x : 4{7}; // "Default Member Init." int y : 3 = 5; // "Default Member Init." int z : 1; }; int main() { //... } >> "bit" başlık dosyası ve fonksiyonları: Buradaki fonksiyonlar "constexpr" fonksiyonlardır. İşaretli tam sayılar ile bu başlık dosyasındaki fonksiyonların kullanılması sentaks hatasıdır. * Örnek 1, #include #include #include int main() { /* # OUTPUT # 0001100000011000 6168 -Base X 0110000001100000 24672 -Rotate X Left by 2 0000011000000110 1542 -Rotate X Left by -2 0000011000000110 1542 -Rotate X Right by 2 0110000001100000 24672 -Rotate X Right by -2 0001000000000000 4096 -Floor of X 0010000000000000 8192 -Ceil of X */ uint16_t x = 0b0001'1000'0001'1000; std::cout << std::format("{0:016b}\t{0:8}\t-Base X\n", x); std::cout << std::format("{0:016b}\t{0:8}\t-Rotate X Left by 2\n", std::rotl(x, 2)); std::cout << std::format("{0:016b}\t{0:8}\t-Rotate X Left by -2\n", std::rotl(x, -2)); std::cout << std::format("{0:016b}\t{0:8}\t-Rotate X Right by 2\n", std::rotr(x, 2)); std::cout << std::format("{0:016b}\t{0:8}\t-Rotate X Right by -2\n", std::rotr(x, -2)); std::cout << std::format("{0:016b}\t{0:8}\t-Floor of X\n", std::bit_floor(x)); std::cout << std::format("{0:016b}\t{0:8}\t-Ceil of X\n", std::bit_ceil(x)); } * Örnek 2, #include #include #include #include int main() { /* # OUTPUT # 00011111 00011110 00000000 00000000 Has single bit: false 01000000 00000001 Has single bit: true */ constexpr uint8_t x = 31; std::cout << std::bitset<8>(x) << '\n'; std::cout << std::bitset<8>(x & (x - 1)) << '\n'; std::cout << std::bitset<8>(!(x & (x - 1))) << '\n'; std::cout << std::bitset<8>(x && !(x & (x - 1))) << '\n'; constexpr auto power_of_two_of_x{ x && !(x & (x - 1)) }; // false std::cout << "Has single bit: " << std::boolalpha << std::has_single_bit(x) << '\n'; puts("\n"); constexpr uint8_t y = 64; std::cout << std::bitset<8>(y) << '\n'; std::cout << std::bitset<8>(y && !(y & (y - 1))) << '\n'; constexpr auto power_of_two_of_y{ y && !(y & (y - 1)) }; // true std::cout << "Has single bit: " << std::boolalpha << std::has_single_bit(y) << '\n'; } * Örnek 3, #include #include #include #include int main() { /* # OUTPUT # # of left zero : 3 # of right zero: 3 # of left one : 0 # of right one : 0 # of set bit : 2 */ std::boolalpha(std::cout); constexpr uint8_t x = 0b0001'1000; std::cout << "# of left zero : " << std::countl_zero(x) << '\n'; std::cout << "# of right zero: " << std::countr_zero(x) << '\n'; std::cout << "# of left one : " << std::countl_one(x) << '\n'; std::cout << "# of right one : " << std::countr_one(x) << '\n'; std::cout << "# of set bit : " << std::popcount(x) << '\n'; } * Örnek 4.0, #include #include #include #include int main() { auto x = static_cast(e); // "e" is "2". auto y = reinterpret_cast(e); // ERROR auto y = *reinterpret_cast(&e); // "Undefined Behaviour" except for; // casting between "signed int" and "unsigned int", // casting to "char" types, // casting between C-Style struct's address and its first member. union FloatingInt { float f; int i; }; FloatingInt fi = { e }; // "Undefined Behaviour" auto ival = fi.i; // Birliklerin aktif olmayan elemanlarının bu şekilde kullanılması; // C dilinde geçerli, fakat C++ dilinde "Tanımsız Davranış". int my_value{}; std::memcpy(&my_value, &e, sizeof(e)); // "memcpy" is NOT a "consexpr" function. So, it is costly. // Does not check the types, may cause "Undefined Behavior". // But, a valid way until "std::bit_cast". auto my_second_value = std::bit_cast(e); // A NEW WAY SINCE C++20 } * Örnek 4.1, #include #include #include #include struct Nec { std::uint16_t a; std::uint16_t b; }; int main() { float e = 2.71828; auto my_second_value = std::bit_cast(e); // ERROR Nec myNec{ 123u, 456789u }; auto my_value = std::bit_cast(myNec); // OK } * Örnek 5.0, #include #include #include #include int main() { // Old School: Run-time Checking int x = 1; if (*(char*)(&x)) std::cout << "little endian\n"; else std::cout << "big endian\n"; // C++20: Compile-time Checking static_assert(std::endian::native == std::endian::little); } * Örnek 5.1, #include #include #include #include int main() { if constexpr (std::endian::native == std::endian::little) { // In case of "Little Endian" } else if constexpr (std::endian::native == std::endian::big) { // In case of "Big Endian" } else { // In case of "Mixed Endian". } } >> "Dependant Type" : Şablon parametresi "T" ye bağlı olan tür demektir. C++20'ye kadar bu türü tanıtmak için "typename" anahtar sözcüğünü kullanmak zorundaydık. * Örnek 1, template void func(typename T::value_type) { } Buradaki "value_type" işte "Dependant Type" olarak geçer. Burada "typename" kullanarak aslında bunun bir tür olduğunu belirtmekteyiz. C++20 ile birlikte bunun tür olmasından başka bir şey olması mümkün değilse, öyle bağlamlarda "typename" kullanmaya gerek yoktur. Öylesi bağlamlarda kullanmamız ise sentaks hatası oluşturmayacaktır. * Örnek 1, Global fonksiyonun geri dönüş değerinin yazıldığı yerlerde "typename" anahtar sözcüğünü kullanmaya gerek yoktur. // Until C++20 template typename T::Erg foo() {} // Since C++20 template T::Erg foo() {} int main() { //... } * Örnek 2, Fakat fonksiyonun parametre parantezinde hala "typename" anahtar sözcüğünü kullanmak zorundayız. Aksi halde ilgili sınıfın "static" veri elemanı olarak değerlendirilecektir. Bunun istisnası sınıfların üye fonksiyonlarıdır. Öylesi fonksiyonlarda yine "typename" anahtar sözcüğüne gerek yoktur. // ERROR template void foo(T::Erg) {} int main() { //... } * Örnek 3, Sınıflarda aşağıdaki biçimde kullanılmasında "typename" gerekmemektedir. template struct PointerTrait { using Pointer = void*; }; template struct Ergin { using PtrNew = PointerTrait::Pointer; // Since C++20 using PtrOld = typename PointerTrait::Pointer; // Until C++20 T::Nec my_member_func(T::Nec) { //... } }; int main() { //... } * Örnek 4, "static_cast" operatörününde de "typename" anahtar sözcüğüne gerek yoktur. template T::Nec my_func(typename T::Nec p) { return static_cast(p); } int main() { //... } * Örnek 5, "concepts" oluştururken de "typename" anahtar sözcüğünü kullanmak mecburi değildir. Fakat C++20 öncesinde "concepts" zaten mevcut DEĞİLDİR. template concept my_concept = requires (T::Nec x) { std::cout << x; }; int main() { //... } >> "type_traits" başlık dosyasına, işaretli tam sayı ile işaretsiz tam sayıları karşılaştırmada kullanmak üzere, bir takım fonksiyonlar eklenmiştir. * Örnek 1.0, #include #include template constexpr bool CmpLess(T t, U u) noexcept { using UT = std::make_unsigned_t; using UU = std::make_unsigned_t; if constexpr (std::is_signed_v == std::is_signed_v) { return t < u; } else if constexpr (std::is_signed_v) { return t < 0 ? true : UT(t) < u; } else { return u < 0 ? false : t < UU(u); } } int main() { //... } * Örnek 1.1, #include #include int main() { int x = -1; unsigned ux = 2; if (x > ux) // x is greater than y { std::cout << "x is greater than y\n"; } else { std::cout << "x is less than y\n"; } if (std::cmp_greater(x, ux)) // x is less than y { std::cout << "x is greater than y\n"; } else { std::cout << "x is less than y\n"; } } >> "[]" operatörü içerisinde "," operatörü ile oluşturulan ifadeyi, C++23 itibariyle, doğrudan kullanamayacağız. Fakat aynı ifadeyi "()" ile sarmalayarak da kullanabiliriz. * Örnek 1, #include int main() { int a[5]{0}; a[1, 2] = 31; // warning: top-level comma expression in array subscript is deprecated [-Wcomma-subscript] for(auto i : a) std::cout << i << ' '; // 0 0 31 0 0 std::cout << '\n'; a[(2,3)] = 31; // OK for(auto i : a) std::cout << i << ' '; // 0 0 31 31 0 std::cout << '\n'; } >> C++20 itibariyle adres türlerinden "bool" türlerine dönüşüm "Narrowing Conversion" kategorisine eklenmiştir. Tabii sadece "{}" ile ilk değer verilmesi durumunda geçerlidir. * Örnek 1, int main() { void* vp; bool b = vp; bool c(vp); bool d{vp}; // narrowing conversion of ‘vp’ from ‘void*’ to ‘bool’ [-Wnarrowing] } >> C++20 itibariyle dinamik ömürlü dizi oluştururken, dizinin boyutunu belirtmeden, "{}" ile ilk değer verdiğimiz zaman sentaks hatası olurken, artık dizinin boyutunu belirtmeye lüzum kalmamıştır. * Örnek 1, int main() { char* ptr = new char[]{"eray goksu\n"}; } >> C++20 ile birlikte "Alias Template" kullanırken "CTAT" dan da artık faydalanabiliriz. * Örnek 1, #include template using first_int_pair = std::pair; int main() { first_int_pair my_pair{ 1, 1.1 }; // alias template deduction only available with ‘-std=c++20’ or ‘-std=gnu++20’ } >> C++20 ile birlikte "inline" isim alanlarını direkt olarak bildirim sırasında kullanabiliriz. * Örnek 1, #include // Until C++20 namespace A{ namespace B{ inline namespace C{ int c; } } } // Since C++20 : Artık "inline" isimalanlarını bu şekilde kullanabiliriz. namespace AA::BB::inline CC{ // C++17: error: nested identifier required before ‘inline’ int c; } int main() { A::B::C::c = 1; // Until C++20 AA::BB::c = 2; // Since C++20 } >> "source_location" isimli yeni bir başlık dosyası daha geldi. C dilinden gelen "__LINE__", "__FILE__" ve "__func__" sembolik sabitleri yerine bu başlık dosyası ile gelen "source_location" türünü kullanabiliriz. * Örnek 1, #include #include int main() { /* # OUTPUT # File Name: main.cpp Func Name: int main() Line No : 6 Column No: 54 */ constexpr auto s1 = std::source_location::current(); constexpr auto pf_name = s1.file_name(); constexpr auto pf_func = s1.function_name(); constexpr auto pf_line = s1.line(); constexpr auto pf_colmn = s1.column(); std::cout << "File Name: " << pf_name << '\n' << "Func Name: " << pf_func << '\n' << "Line No : " << pf_line << '\n' << "Column No: " << pf_colmn << '\n'; } * Örnek 2, #include #include void foo(std::source_location s1 = std::source_location::current()) { std::cout << "File Name: " << s1.file_name() << '\n' << "Func Name: " << s1.function_name() << '\n' << "Line No : " << s1.line() << '\n' << "Column No: " << s1.column() << '\n'; } std::source_location bar(void) { return std::source_location::current(); } int main() { /* # OUTPUT # File Name: main.cpp Func Name: std::source_location bar() Line No : 14 Column No: 41 */ foo(bar()); } * Örnek 3, #include #include #include std::vector sl_vec; void foo(void) { //... auto sl = std::source_location::current(); sl_vec.push_back(sl); //... } void bar(void) { //... auto sl = std::source_location::current(); sl_vec.push_back(sl); //... } void baz(void) { //... auto sl = std::source_location::current(); sl_vec.push_back(sl); //... } void func(std::source_location s1 = std::source_location::current()) { std::cout << "File Name: " << s1.file_name() << '\n' << "Func Name: " << s1.function_name() << '\n' << "Line No : " << s1.line() << '\n' << "Column No: " << s1.column() << "\n\n"; } int main() { /* # OUTPUT # File Name: main.cpp Func Name: int main() Line No : 45 Column No: 56 File Name: main.cpp Func Name: void foo() Line No : 10 Column No: 44 File Name: main.cpp Func Name: void bar() Line No : 18 Column No: 48 File Name: main.cpp Func Name: void baz() Line No : 26 Column No: 52 */ auto sl = std::source_location::current(); sl_vec.push_back(sl); foo(); bar(); baz(); for(auto const& sl : sl_vec) func(sl); } >> "using enum" bildirimi de C++20 ile gelmiştir. * Örnek 1 #include enum class Color{ white, yellow, blue, black }; void white(){} void yellow(){} void blue(){} void black(){} void func(Color c) { switch(c) { using enum Color; // Since C++20: usage of "using declaration" case white: white(); break; case yellow: yellow(); break; case blue: blue(); break; case black: black(); break; } } int main() { } > Hatırlatıcı Notlar: >> "unspecified behavior", "implementation defined" ın kapsayan kümesidir. İkisi arasındaki fark ise "implementation defined" olması durumunda derleyicinin bunu dökümante etmesi ve ilgili kod farklı yerlerde tekrar tekrar çağrıldığında aynı sonucun alınması gerekiyor. Halbuki "unspecified behavior" için bunlar geçerli değildir. /*================================================================================================================================*/ (20_09_09_2023) > "Attributes" (devam): >> "deprecated" : Nitelediği şeyin artık kullanılmaması gerektiğini, ilerleyen zamanlarda kaldırılacağını belirtir. Genelde kütüphane tasarlayanlar tarafından kullanılır. Derleyici, bu "attribute" ile nitelenen türlerin/fonksiyonların kullanılması durumunda, uyarı veya hata kodu verecektir. * Örnek 1, struct [[deprecated("use ErgNect")]] Nec { int a, b, c; }; int main() { Nec mynec; // error C4996: 'Nec': use ErgNect } * Örnek 2, [[deprecated]] int x = 10; [[deprecated]] typedef int MyInt; using MyInt32 [[deprecated]] = int; enum Color { White, Blue, DarkBlue [[deprecated]], Yellow }; int main() { x = 5; // error C4996: 'x': was declared deprecated MyInt y = 11; // error C4996: 'MyInt': was declared deprecated MyInt32 z = 12; // error C4996: 'MyInt32': was declared deprecated auto c = DarkBlue; // error C4996: 'DarkBlue': was declared deprecated } * Örnek 3, [[deprecated ("Call Func")]] void CallableFunc() { } int main() { CallableFunc(); // error C4996: 'CallableFunc': Call Func } * Örnek 4, struct Data { int a, b, c; [[deprecated]] double d; }; int main() { Data myData; myData.a = 1; myData.b = 2; myData.c = 3; myData.d = 123.321; // error C4996: 'Data::d': was declared deprecated } * Örnek 5, namespace [[deprecated]] old_version { void func() {} } int main() { old_version::func(); // error C4996: 'old_version': was declared deprecated } >> "likely" / "unlikely" : Hangi "path" in oluşma ihtimalinin daha yüksek, hangisinin daha düşük olduğunu derleyiciye ve okuyucuya söylemektedir. Böylelikle derleyiciler daha iyi kod üretebilme şansına sahip olabilmektedir. Buradaki "path" ile kastedilen "if-else", "switch" deyimlerinde programın akışının nereden devam edeceğidir. * Örnek 1, enum class Color { white, blue, black, red, magenta }; void f_white() {} void f_blue() {} void f_black() {} void f_red() {} void f_magenta() {} void func_color(Color c) { switch (c) { case Color::white: f_white(); break; case Color::blue: f_blue(); break; [[likely]] case Color::black: f_black(); break; // "c" nin "black" değerinde olma ihtimali daha yüksek. case Color::red: f_red(); break; [[unlikely]] case Color::magenta: f_magenta(); break; // "c" nin "magenta" değerinde olma ihtimali daha düşük. } } int main() { //... } * Örnek 2, #include constexpr double pow(double x, long long n) noexcept { if (n > 0) [[likely]] return x * pow(x, n - 1); else [[unlikely]] return 1; } constexpr long long fact(long long n) noexcept { if (n > 1) [[likely]] return n * fact(n - 1); else return 1; } constexpr double cosin(double x) noexcept { constexpr long long precision{ 16LL }; double y{}; for (auto n{0LL}; n < precision; n += 2LL) [[likely]] y += pow(x, n) / (n & 2LL ? -fact(n) : fact(n)); return y; } int main() { //... } >> "maybeunused" : Kodda var olan bazı öğelerin kullanılmaması şüpheli bir durumdur. Çoğu derleyici de böylesi senaryolarda uyarı mesajı vermektedir. Fakat öyle durumlar var ki "debug" modunda kullandığımız öğeleri, "release" modunda kullanmıyoruz. * Örnek 1, #include // İş bu fonksiyon iç bağlantıya açık bir fonksiyondur. // Dolayısıyla dışarıdan bu fonksiyona erişim söz konusu // değildir. Eğer bu fonksiyonun kullanılmadığı görülürse, // derleyici uyarı mesajı verebilir. static int func() { return 1; } // İş bu fonksiyon da yine iç bağlantıya açıktır fakat // "[[maybe_unused]]" kullanımından dolayı derleyici uyarı // vermeyebilir. [[maybe_unused]] static int foo() { return 2; } int main() { //... auto a = func(); auto b = foo(); int c = 32; // "c" değişkeni için derleyici kullanılmadığına dair uyarı mesajı verebilir. [[maybe_unused]] int d = 23; // Fakat "d" için artık uyarı mesajı vermeyecektir. } * Örnek 2, #include [[maybe_unused]] void func([[maybe_unused]] bool param1, [[maybe_unused]] bool param2) { [[maybe_unused]] bool result = param1 && param2; assert(result); } int main() { //... } * Örnek 3, #include struct [[maybe_unused]] Nec { int a, b, c; }; [[maybe_unused]] typedef Nec* Necptr; using NecPtr [[maybe_unused]] = Nec*; struct Erg { int a, b, c; [[maybe_unused]] double d; }; int main() { [[maybe_unused]] auto [i, d, sptr] = std::make_tuple(45, 4.5, "FHAM"); } >> "noreturn" : Fonksiyonun geri dönüş değerinin olmadığını değil, programın çalışma zamanındaki akışının "return" deyimine ulaşmayacağını garanti etmektedir. Eğer programın akışı bir şekilde ilgili "return" deyimine ulaşırsa, "Tanımsız Davranış" meydana gelecektir. * Örnek 1, [[noreturn]] void foo() {} void bar() {} int main() { foo(); // Burada bir lojik hata vardır. Programın akışı // "foo" fonksiyonundan gelmeyeceği için, "bar" // fonksiyonu çağrılmayacaktır. bar(); } * Örnek 2, #include #include [[noreturn]] void foo /* [[noreturn]] */ () { //... throw std::runtime_error("void bar()\n"); } int main() { try { foo(); std::cout << "Unreachable code\n"; // warning C4702 : unreachable code } catch (const std::exception&) { // Reachable code... } } * Örnek 3, Aşağıdaki programda Tanımsız Davranış vardır. [[noreturn]] void foo /* [[noreturn]] */ () { //... if (true) return; // warning C4645: function declared with 'noreturn' has a return statement } int main() { foo(); } >> "fallthrough" : "switch" deyiminde kullanılır. Okuyuca ve derleyiciye, ilgili "break;" ifadesinin kasıtlı olarak yazılmadığını söylemek için de kullanılır. * Örnek 1, void f1() {} void f2() {} void f3() {} void f4() {} void foo(int x) { /* * Aşağıdaki deyimde "x" eğer "1" değerinde * olursa hem "f1" hem "f2" fonksiyonu çağrılacaktır. * Pekiyi burada istenen şey bu mudur yoksa ikisinin * ayrı ayrı çağrılması mıdır? Eğer niyetimiz ilki ise * bunu hem derleyiciye hem de okuyucuya iletmek için * "fallthrough" ifadesi kullanılır. */ switch (x) { case 1: f1(); /* [[fallthrough]]; */ case 2: f2(); break; case 3: f3(); break; case 4: f4(); break; } } int main() { foo(1); } * Örnek 2, constexpr bool isleap(int y) { return y % 4 == 0 && (y % 100 != 0 || y % 400 == 0); } constexpr int day_of_year(int d, int m, int y) { int sum = d; switch (m - 1) { case 11: sum += 30; [[fallthrough]]; case 10: sum += 31; [[fallthrough]]; case 9: sum += 30; [[fallthrough]]; case 8: sum += 31; [[fallthrough]]; case 7: sum += 31; [[fallthrough]]; case 6: sum += 30; [[fallthrough]]; case 5: sum += 31; [[fallthrough]]; case 4: sum += 30; [[fallthrough]]; case 3: sum += 31; [[fallthrough]]; case 2: sum += 30; isleap(y) ? 29 : 28; [[fallthrough]]; case 1: sum += 31; } return sum; } int main() { constexpr auto day = day_of_year(23, 9, 2023); } * Örnek 3, #include void comment_place(int place) { switch (place) { case 1: std::cout << "very "; [[fallthrough]]; case 2: std::cout << "well\n"; break; default: std::cout << "OK\n"; break; } } int main() { comment_place(1); } > "std::iota" algoritmasının incelenmesi: "numeric" başlık dosyasında bulunur. Parametre olarak bir "range" ki minimal "forward_iterator" olacak, ve bir değer alır. Bu "range" içerisindeki öğeleri, almış olduğu değerden başlayarak ardışık bir sonraki öğeyi, o değerin bir fazlası olarak "set" edeceğim. Yani ilk öğenin yeni değeri, almış olduğu değer olacak. İkinci öğe ise, almış olduğu değerin bir fazlası olacak vs. Tabii bu "set" işlemi için "toplama" işleminin öğeler bazında desteklenmesi gerekmektedir. * Örnek 1, #include #include #include int main() { std::vector ivec(10); for (auto i : ivec) std::cout << i << ' '; // 0 0 0 0 0 0 0 0 0 0 std::cout << '\n'; std::iota(ivec.begin(), ivec.end(), 0); for (auto i : ivec) std::cout << i << ' '; // 0 1 2 3 4 5 6 7 8 9 std::cout << '\n'; } * Örnek 2, Aşağıdaki Date sınıfının fonksiyonlarının tanımı olmadığından, sadece derlenebilmektedir. Burada gösterilmek istenen şey ise sınıfların "operator++" fonksiyonlarının bulunması gerekmesidir. #include #include #include class Date { public: Date(); Date(int, int, int); Date& operator++(); // Ön ek ++ operatörü Date operator++(int); // Son ek ++ operatörü }; int main() { std::vector dvec(5); Date dx{24, 9, 2023}; std::iota(dvec.begin(), dvec.end(), dx); } * Örnek 3, #include #include #include #include "MyUtility.h" using namespace MyUtility; int main() { /* # OUTPUT # 24 Eylul 2023 Pazar 25 Eylul 2023 Pazartesi 26 Eylul 2023 Sali 27 Eylul 2023 Carsamba 28 Eylul 2023 Persembe 29 Eylul 2023 Cuma 30 Eylul 2023 Cumartesi 01 Ekim 2023 Pazar 02 Ekim 2023 Pazartesi 03 Ekim 2023 Sali */ std::vector dvec(10); Date::Date dx{24, 9, 2023}; std::iota(dvec.begin(), dvec.end(), dx); for (const auto& d : dvec) std::cout << d << '\n'; } > "ranges" kütüphanes: Eski STL kütüphanesinin 2.0 versiyonudur diyebiliriz. Tavsiye edilen, aksi yönde karar almamızı gerektiren bir senaryo olmadığı müddetçe bu 2.0 versiyonunu kullanmamızdır. Çünkü yenisini kullanmak; -> Daha pratik. -> Daha güvenli. -> Daha verimli. -> İlave avantajlar kazandırıyor. Fakat yenisini kullanabilmek için de öğrenmemiz gerekmektedir. Bu kütüphane ile "ranges" kütüphanesinin farkları ise şu şekildedir: -> "ranges" ile gelen algoritmalar, "constraint" edilmişlerdir. -> "ranges" ile gelen algoritmalar ile "STL" içerisindeki algoritmaların parametrik yapılarında da farklılık bulunmaktadır. Yani "STL" içerisindeki algoritmalar takribi şu şekildedir: template void algo(Iter begin, Iter end); Fakat "ranges" ile gelen algoritmalar ise artık şu şekildedir: template void algo(Iter begin, Sentinel end); Buradan da görüleceği üzere "ranges" algoritma fonksiyonlarına geçilen argümanlar farklı türden olabilirler. Anektod: "ranges" içerisindeki algoritma fonksiyonlarına geçilen iş bu argümanlar aynı türden ise, "common-range" ismi ile nitelenirler. -> Diğer yandan "ranges" ve STL kütüphanelerindeki benzer isimdeki algoritma fonksiyonlarının büyük çoğunluğunun geri dönüş değerleri farklılık göstermektedir. Artık "ranges" kütüphanesindeki versiyonlar, daha fazla bilgi içermesi hasebiyle, başka türden değerler döndürmektedir. -> Öte yandan "ranges" ile gelen algoritmalar "projection" denilen önemli bir niteliğe de sahiptirler.Pekiyi nedir bu "projection" mevzusu? Konuyu incelemek adına "find" fonksiyonunun STL içerisindeki versiyonunu inceleyelim. Anımsanacağı üzere aşağıdaki gibi implemente edilmektedir: template InIter Find(InIter beg, InIter end, const T& value) { while (beg != end) { if (*beg == value) return beg; ++beg; } return end; } Fakat şimdide ilgili fonksiyonun "Projection" isminde bir şablon parametresine ve ilgili fonksiyonun da bu türden bir parametreye sahip olduğunu, yani aşağıdaki gibi implemente edildiğini, varsayalım: template InIter Find(InIter beg, InIter end, const T& value, Projection pr) { while (beg != end) { if (pr(*beg) == value) return beg; ++beg; } return end; } Artık "*beg" değeri yerine, "pr(*beg)" çağrısı ile elde edilen değer kullanılacaktır. Fakat bu şekilde bırakılırsa, "pr" nin bir "data member pointer" olması durumunda sorun çıkacaktır. İşte bu sorunu gidermek adına "std::invoke" çağrısını kullanabiliriz. Şöyleki: template InIter Find(InIter beg, InIter end, const T& value, Projection pr) { while (beg != end) { if (std::invoke(pr, *beg) == value) return beg; ++beg; } return end; } Artık "Find" fonksiyonunun üçüncü parametresine hem bir "callable" hem de "data member pointer" geçebiliriz. Şimdi karşımıza şöyle bir sorun çıkmıştır: artık "Find" fonksiyonu için üçüncü bir argüman GEÇMEK ZORUNDAYIZ. İşte bu problemi de giderebilmek adına o parametre için varsayılan argüman belirtmeliyiz. Bunun için "std::identity" türünü kullanabiliriz. Bu tür takribi olarak aşağıdaki biçimde implemente edilmiştir: struct Identity { template T operator()(T x) const { return x; } }; Görüleceği üzere argüman olarak aldığı ifadeyi geri döndürmektedir. İşte bu türü de "Find" fonksiyonumuzda aşağıdaki gibi kullanabiliriz: struct Identity { template T operator()(T x) const { return x; } }; template InIter Find(InIter beg, InIter end, const T& value, Projection pr = {}) { while (beg != end) { if (std::invoke(pr, *beg) == value) return beg; ++beg; } return end; } Artık "Find" fonksiyonunun üçüncü parametresine argüman geçilmediğinde, "Identity" türü kullanılacak. Şimdi de bu kütüphanedeki fonksiyonları tanıyabileceğimiz temel örneklere bakalım: * Örnek 1, #include "MyUtility.h" #include #include #include int main() { /* # OUTPUT # 03 Subat 1999 Carsamba 17 Aralik 2009 Persembe 20 Agustos 1993 Cuma 03 Subat 1999 Carsamba 17 Aralik 2009 Persembe 20 Agustos 1993 Cuma */ using namespace MyUtility; std::vector dvec; Utility::rfill(dvec, 3, Date::Date::random); std::sort(dvec.begin(), dvec.end()); std::copy(dvec.begin(), dvec.end(), std::ostream_iterator{std::cout, "\n"}); puts(""); std::ranges::sort(dvec); std::ranges::copy(dvec, std::ostream_iterator{std::cout, "\n"}); } * Örnek 2, #include #include #include #include int main() { std::list iList; for(auto i{0}; i < 5; ++i) iList.push_back(i); std::ranges::sort(iList); // ERROR: note: constraints not satisfied // "Random Access Iterator" olması gerekmektedir. return 0; } * Örnek 3, #include #include #include #include /* * "ranges" is a namespace. * "range" is the name of a concept. */ // WAY - I template void algo_i(T&& coll) {} // WAY - II void algo_ii(std::ranges::range auto&&) {} /* * So, "MyRange" is just like a "range". */ template concept MyRange = requires(T& t){ std::ranges::begin(t); std::ranges::end(t); }; // WAY - I template void MyAlgo_i(T&& coll) {} // WAY - II void MyAlgo_ii(std::ranges::range auto&&) {} int main() { { algo_i(12); // ERROR: constraints not satisfied int a[10]; algo_i(a); // OK algo_i(std::string{"Merve"}); // OK algo_i(std::string_view{"Menekse"}); // OK } puts("\n"); { algo_ii(12); // ERROR: constraints not satisfied int a[10]; algo_ii(a); // OK algo_ii(std::string{"Merve"}); // OK algo_ii(std::string_view{"Menekse"}); // OK } puts("\n"); { MyAlgo_i(12); // ERROR: constraints not satisfied int a[10]; MyAlgo_i(a); // OK MyAlgo_i(std::string{"Merve"}); // OK MyAlgo_i(std::string_view{"Menekse"}); // OK } puts("\n"); { MyAlgo_ii(12); // ERROR: constraints not satisfied int a[10]; MyAlgo_ii(a); // OK MyAlgo_ii(std::string{"Merve"}); // OK MyAlgo_ii(std::string_view{"Menekse"}); // OK } return 0; } * Örnek 4.0, #include #include #include #include #include #include void algo(std::ranges::range auto) { std::cout << "range\n"; } void algo(std::ranges::input_range auto) { std::cout << "input_range\n"; } // Subsumes above range concept. void algo(std::ranges::forward_range auto) { std::cout << "forward_range\n"; } // Subsumes above range concept. void algo(std::ranges::bidirectional_range auto) { std::cout << "bidirectional_range\n"; } // Subsumes above range concept. void algo(std::ranges::random_access_range auto) { std::cout << "random_access_range\n"; } // Subsumes above range concept. void algo(std::ranges::contiguous_range auto) { std::cout << "contiguous_range\n"; } // Subsumes above range concept. int main() { algo(std::vector{}); // contiguous_range algo(std::deque{}); // random_access_range algo(std::list{}); // bidirectional_range algo(std::forward_list{}); // forward_range return 0; } * Örnek 4.1, #include #include #include void algo(std::ranges::range auto) { std::cout << "range\n"; } void algo(std::ranges::sized_range auto) { std::cout << "sized_range\n"; } // Derleme zamanında çağrılabilir ".size()" fonksiyonu // bulunması gerekmektedir. "std::forward_list" bu şartı // sağlamamaktadır. Ek olarak sadece ve sadece "range" // isimli konsepti "subsume" ETMEKTEDİR. int main() { algo(std::vector{}); // sized_range return 0; } * Örnek 5, #include #include #include #include #include #include #include "MyUtility.h" int main() { /* # OUTPUT # 100 65 35 -34 -9 -59 -32 -78 -68 72 8 97 48 -42 79 -23 -56 -98 18 84 ----------------------------------------------------------------------------- -98 -78 -68 -59 -56 -42 -34 -32 -23 -9 8 18 35 48 65 72 79 84 97 100 ----------------------------------------------------------------------------- 100 97 84 79 72 65 48 35 18 8 -9 -23 -32 -34 -42 -56 -59 -68 -78 -98 ----------------------------------------------------------------------------- 8 -9 18 -23 -32 -34 35 -42 48 -56 -59 65 -68 72 -78 79 84 97 -98 100 ----------------------------------------------------------------------------- 8 -9 18 -23 -32 -34 35 -42 48 -56 -59 65 -68 72 -78 79 84 97 -98 100 ----------------------------------------------------------------------------- */ std::vector ivec; MyUtility::Utility::rfill(ivec, 20, MyUtility::Random::Irand{-100, 100}); MyUtility::Utility::print(ivec); // "std::ranges::sort" w/ default arguments: std::ranges::sort(ivec); MyUtility::Utility::print(ivec); // "std::ranges::sort" w/ a custom argument instead of "std::less", but a default argument for the Projection param: std::ranges::sort(ivec, [](int a, int b) { return a > b; }); MyUtility::Utility::print(ivec); // "std::ranges::sort" with "std::less" as a default and a lambda expression for the Projection param: std::ranges::sort(ivec, {}, [](int a) { return std::abs(a); }); MyUtility::Utility::print(ivec); // "std::ranges::sort" w/ a custom argument instead of "std::less", but a default argument for the Projection param: std::ranges::sort(ivec, [](int a, int b) { return std::abs(a) < std::abs(b); }); MyUtility::Utility::print(ivec); } * Örnek 6, #include #include #include #include #include #include #include "MyUtility.h" struct Point { Point() = default; Point(int a, int b) : m_a{a}, m_b{b} {} int m_a{}; int m_b{}; friend std::ostream& operator<<(std::ostream& os, const Point& p) { return os << "[" << p.m_a << "," << p.m_b << "]"; } }; Point create_random_point() { MyUtility::Random::Irand my_rand{0, 99}; return Point{ my_rand(), my_rand() }; } int main() { /* # OUTPUT # [1,2] [7,49] [9,46] [15,0] [15,69] [16,47] [17,25] [31,12] [38,60] [39,28] [44,89] [56,86] [58,59] [60,57] [67,61] [69,16] [70,67] [82,79] [85,53] [88,11] */ std::vector pvec(20); std::ranges::generate(pvec, create_random_point); std::ranges::sort(pvec, {}, &Point::m_a); // Burada yapılacak olan karşılaştırma, ilgili "Point" // nesnelerinin "m_a" veri elemanlarına göre yapılacaktır. // Tabii burada "Projection" yerine ikinci parametreye "custom" // bir karşılaştırma fonksiyonu yazarak veya ikinci ve üçüncü // parametreler için varsayılan argümanı kullanmak adına, "Point" // sınıfımıza bir "küçüktür operator fonksiyonu" da yazabilirdik. std::ranges::copy(pvec, std::ostream_iterator{std::cout, "\n"}); } * Örnek 7, #include #include #include #include #include "MyUtility.h" int main() { /* # OUTPUT # tevfik poturgeli sabriye aksakal nihat sefiloglu mert cubukay bilal silahdar sadegul tepecik dilber uslu deniz mertek ceyhun ormanci taci issiz necmettin yilgin recep soysalan sidre unalmis sezen erim nalan kesman ata beyaz murat yelden niyazi simsek samet alemdar cezmi kurban Found => sabriye aksakal */ std::vector svec; MyUtility::Utility::rfill(svec, 20, [] { return MyUtility::Utility::rname() + ' ' + MyUtility::Utility::rfname(); }); std::ranges::copy(svec, std::ostream_iterator{std::cout, "\n"}); // Artık iteratör konumundaki nesne değil, o nesnenin uzunluğunu "15" ile karşılaştıracak. if (auto iter = std::ranges::find(svec, 15, [](const std::string& s) { return s.size(); }); iter != svec.end()) std::cout << "Found => " << *iter << '\n'; else std::cout << "Not found\n"; } * Örnek 8, Aşağıdaki örnekte ise "ranges" parametreli "overload" çağrıldığında, "iterator" parametreli fonksiyona çağrı yapmaktadır. Böylelikle "ranges" parametreleri bir nevi "iterator" parametresi olarak kullanmış olduk. template SenType, typename Init = std::iter_value_t, typename Op = std::plus<>, typename Proj = std::identity> Init Accumulate(Iter beg, SenType end, Init init = Init{}, Op op = {}, Proj proj = {}) { while(beg != end){ init = std::invoke(op, std::move(init), std::invoke(proj, *beg)); ++beg; } return init; } template, typename Op = std::plus<>, typename Proj = std::identity> Init Accumulate(R&& r, Init init = Init{}, Op op = {}, Proj proj = {}) { return Accumulate(std::ranges::begin(r), std::ranges::end(r), std::move(init), std::move(op), std::move(proj)); } int main() { //... } > Hatırlatıcı Notlar: >> C++ dilinde "void" geri dönüş değerine sahip fonksiyonların kodlarında yalın "return;" ifadesini kullanarak da programın akışınının o fonksiyondan çıkmasını sağlatabiliriz. Benzer şekilde geri dönüş değeri yine "void" olan başka bir fonksiyonu da "return" deyimi içerisinde çağırarak da yine o fonksiyondan çıkabiliriz. Fakat bu durum C dilinde geçerli değildir. * Örnek 1, #include #include void bar() {} void foo() { //... if(true) return bar(); // C dilinde geçerli değil, fakat C++ // dilinde geçerlidir. else return; } int main() { foo(); } >> Şablon parametresi "T" için tür çıkarım mekanizması ile "auto" tür çıkarım mekanizması arasında neredeyse bir fark yoktur. Tek istisnası "std::initializer_list" için geçerlidir. "T" için tür çıkarımı yapılması SENTAKS HATASI olurken, "auto" için yapılan tür çıkarımı "std::initializer_list" yönüne olacaktır. * Örnek 1, #include template void foo(T x) {} // Buradaki tür çıkarımı "T" için, "x" için DEĞİL. template void bar(T& x) {} // Buradaki tür çıkarımı "T" için, "x" için DEĞİL. template void zar(T&& x) {} // Buradaki tür çıkarımı "T" için, "x" için DEĞİL. int main() { foo(10); // "T" is "int". So, "x" is also "int". auto val = 10; // int val = 10; int a[20]{}; // "T" is "int[20]". So, "x" is "int (&)[20]". No "array-decay". bar(a); auto& ref_val = a; // int (&ref_val)[20] = a; int x = 5; zar(x); // "T" is "int&". "x" is "int&" because of the reference-collapsing. zar(10); // "T" is "int". "x" is "int&&". No reference-collapsing. //////////////// foo({ 4, 7, 9, 1 }); // SENTAKS HATASI auto init_list = { 4, 7, 9, 1 }; // std::initializer_list init_list = { 4, 7, 9, 1 }; } * Örnek 2.0, Tür çıkarımının nasıl yapıldığını derleyiciye sormak için: #include template class TypeTeller; template void func(T&&) { TypeTeller x; } // Burada "TypeTeller" için bir tanım olmadığından, // "T" için hangi türün çıkarıldığını öğrenebiliriz. int main() { func(10); // "T" is "int". int x{}; func(x); // "T" is "int&". } * Örnek 2.1, #include template class TypeTeller; template void func(T&) { TypeTeller x; } int foo(void) { return 1; } int main() { func(foo); // "T" is "int(void)". No function-to-pointer conversation. // "x" is "int(&)(void)". } >> Standart kütüphanenin bize verdiği "type_identity" meta fonksiyonunun temsili implementasyonu: * Örnek 1, #include /////// BİZİMKİ template struct TypeIdentity { using type = T; }; template using TypeIdentity_t = TypeIdentity::type; template void foo(T, TypeIdentity_t) {} /////// STANDART KÜTÜPHANEDEKİ template void bar(T, std::type_identity_t) {} int main() { TypeIdentity_t x{}; // int x{}; foo(1.4, 5); // Normal şartlarda bu sentaks hatası oluşturur. bar(5, 1.4); // Normal şartlarda bu sentaks hatası oluşturur. } >> "namespace alias" : * Örnek 1, #include int main() { namespace chr = std::chrono; // namespace alias. } * Örnek 2, namespace Nec { namespace Erg { namespace CppCourse { int x; //... } } } int main() { namespace advanced_cpp_course = Nec::Erg::CppCourse; // namespace alias. advanced_cpp_course::x = 100; } * Örnek 3, #include int main() { namespace rng = std::ranges; namespace rng_v = std::ranges::views; } >> "Sentinel" kavramı için ön bilgilendirme: Aslında buradaki amaç o dizinin nerede bittiğinin belirtilmesidir. Dizinin boyutunun bir döngü ile öğrenilmesi yükümlülüğünü kaldırmaktadır. * Örnek 1, #include struct NullChar{ bool operator==(auto x)const { return *x == '\0'; } // Parametre "auto" olduğu için herhangi bir için // çağrılabilir. Yeterki "*" operatörünün operandı // olabilsin. }; template void Print(Iter beg, Sentinel end) { while(beg != end) // Derleyici "operator!=" çağrısını "operator==" çağrısına dönüştürmektedir. C++20 std::cout << *beg++ << ' '; } int main() { char name[100] = "Merve Nur Menekse"; Print(name, NullChar{}); // M e r v e N u r M e n e k s e /* * Eski STL olsaydı, "Print" fonksiyonuna "name" dizisinin * büyüklük bilgisini geçmek zorundaydık. Bu bilgiyi de yine * bir "for" döngüsü ile elde edecektik. Daha sonra "Print" * içerisinde ikinci bir "for" ile "name" dizisinin elemanlarını * ekrana yazdıracaktır. * Fakat artık ilgili dizinin büyüklük bilgisini öğrenmemize * gerek kalmadı. Sadece sonlandırıcı karakterin ne olduğunu * belirliyoruz. */ return 0; } * Örnek 2, #include #include #include #include template struct EnderSentinel { bool operator==(auto pos) const { return *pos == ENDVAL; } }; int main() { /* # OUTPUT # 5 3 */ std::vector ivec{ 1, 5, 7, 9, 2, 3, 6, 79, 90 }; auto iter = std::ranges::find(ivec.begin(), EnderSentinel<3>{}, 5); // "ivec" dizisi içerisinde "5" karakteri aranacak. // Fakat dizinin bittiği konum "3" değerinin bulunduğu // konum olacak. std::cout << *iter << '\n'; iter = std::ranges::find(ivec.begin(), EnderSentinel<3>{}, 90); // Şimdi de dizi içerisinde "90" karakteri aranacak fakat // dizinin bittiği konum "3" değerinin bulunduğu konum olacak. // "3" değerinin bulunduğu konum daha önce geleceğinden, "90" // karakterini bulamamış olacaktır. std::cout << *iter << '\n'; } * Örnek 3, #include #include #include #include template struct EnderSentinel { bool operator==(auto pos) const { return *pos == ENDVAL; } }; int main() { /* # OUTPUT # 1 5 7 9 2 3 6 79 90 1 2 5 7 9 3 6 79 90 */ std::vector ivec{ 1, 5, 7, 9, 2, 3, 6, 79, 90 }; for (auto i : ivec) std::cout << i << ' '; std::cout << '\n'; std::ranges::sort(ivec.begin(), EnderSentinel<3>{}); // "3" karakterinin bulunduğu konuma olanları sıralayacaktır. for (auto i : ivec) std::cout << i << ' '; std::cout << '\n'; } >>> "Unreachable Sentinel" kavramı hakkında ön bilgilendirme: * Örnek 1, Eskiden karşılaştırma yapılırken hem aranan değer ilgili dizinin öğeleri ile tek tek karşılaştırılıyor hem de ilgili dizinin o anki indeks değeri ile dizinin boyutu karşılaştırılıyordu. "std::unreachable_sentinel" kullanılması ile sadece aranan değer ile dizideki öğeler birbiri ile karşılaştırılmaktadır. FAKAT BU YÖNTEMİ SAĞLIKLI KULLANABİLMEK İÇİN ARANAN DEĞERİN İLGİLİ DİZİ İÇERİSİNDE BULUNDUĞUNU BİLİYOR OLMALIYIZ. AKSİ HALDE ÇALIŞMA ZAMANI HATASI ALIRIZ. #include #include #include #include #include #include #include "MyUtility.h" int main() { std::vector ivec(10); std::mt19937 eng{ std::random_device{}() }; std::uniform_int_distribution dist{ 0, 100 }; std::ranges::generate(ivec, [&]() { return dist(eng); }); std::ranges::copy(ivec, std::ostream_iterator{std::cout, " "}); // 3 75 4 100 62 23 78 1 1 20 puts("\n"); std::ranges::shuffle(ivec, eng); std::ranges::copy(ivec, std::ostream_iterator{std::cout, " "}); // 3 75 4 100 62 23 78 1 1 20 puts("\n"); std::ranges::iota(ivec, 0); MyUtility::Utility::print(ivec); int searched_value; std::cout << "Value to search: "; std::cin >> searched_value; auto iter{ std::ranges::find(ivec.begin(), std::unreachable_sentinel, searched_value) }; // Şimdi sadece "*iter" öğesinin "searched_value" değerine eşitliği sınanmaktadır. // Eğer "ivec.end()" kullanılsaydı, döngünün her turunda ayrıca "ivec.end()" karşılaştırması da yapılacaktı. std::cout << "Found at " << iter - ivec.begin() << '\n'; } >> "std::invoke" : "functional" başlık dosyası içerisindedir. * Örnek 1, #include #include int func(int x, int y) { return x * y + 5; } int main() { auto val = std::invoke(func, 10, 20); std::cout << "val: " << val << '\n'; // val: 205 } * Örnek 2, #include #include class Myclass { public: int foo(int x) { return x * x + 5; } }; int main() { Myclass m; int ival{45}; auto val = std::invoke(&Myclass::foo, m, ival); std::cout << "val: " << val << '\n'; // val: 2030 } * Örnek 3, #include #include class Myclass { public: static int func() { return 100; } int foo(int x) { return x * x + 5; } }; int main() { // A member function pointer to static member function. int(*s_ptr)() = &Myclass::func; std::cout << "*s_ptr: " << s_ptr() << '\n'; // *s_ptr: 100 // A member function pointer to non-static member function. Myclass m; int(Myclass::*ptr)(int) = &Myclass::foo; // auto ptr = &Myclass::foo; std::cout << "*ptr: " << (m.*ptr)(s_ptr()) << '\n'; // *ptr: 10005 // A member function pointer to non-static member function using "auto" auto m_fp = &Myclass::foo; auto d_m = new Myclass; std::cout << "*d_m: " << ((*d_m).*m_fp)(s_ptr()) << '\n'; // *d_m: 10005 delete d_m; // Using "std::invoke" instead of directly using the member function pointers. auto m_fp2 = &Myclass::foo; auto result = std::invoke(m_fp2, m, s_ptr()); std::cout << "result: " << result << '\n'; // result: 10005 auto result2 = std::invoke(m_fp2, d_m, s_ptr()); std::cout << "result2: " << result2 << '\n'; // result2 : 10005 } * Örnek 4, #include #include struct Myclass { int x{10}; int y{100}; }; int main() { Myclass m; // A normal pointer: auto ptr_x = &m.x; // int* ptr_x = &m.x; // Data member pointer: auto ptr_y = &Myclass::y; // int Myclass::*ptr_y = &Myclass::y; // Using these pointers directly: std::cout << "Myclass::x : " << *ptr_x << '\n'; // Myclass::x : 10 std::cout << "Myclass::y : " << m.*ptr_y << '\n'; // Myclass::y : 100 // Using "std::invoke" via the data member pointer: std::cout << "Myclass::y : " << std::invoke(ptr_y, m) << '\n'; // Myclass::y : 100 } >> "std::accumulate" : * Örnek 1, #include #include #include #include #include "MyUtility.h" int main() { /* # OUTPUT # suheyla muslum efecan ciler esen sade muruvvet azmi fahri gurbuz ----------------------------------------------------------------------------- toplam = suheylamuslumefecancileresensademuruvvetazmifahrigurbuz */ std::vector svec; MyUtility::Utility::rfill(svec, 10, MyUtility::Utility::rname); MyUtility::Utility::print(svec); auto x = std::accumulate(svec.begin(), svec.end(), std::string{"toplam = "}); std::cout << x << '\n'; } * Örnek 2, #include #include #include #include #include "MyUtility.h" int main() { /* # OUTPUT # 270351 */ std::vector ivec{ 9, 17, 19, 93 }; auto x = std::accumulate(ivec.begin(), ivec.end(), 1, [](int a, int b){ return a*b; }); std::cout << x << '\n'; } * Örnek 3, #include #include #include #include #include "MyUtility.h" int main() { /* # OUTPUT # nuri mukerrem eda melek yavuz necmettin aslihan nazli baran nazli ----------------------------------------------------------------------------- 56 */ std::vector svec; MyUtility::Utility::rfill(svec, 10, MyUtility::Utility::rname); MyUtility::Utility::print(svec); const auto f = [](std::size_t len, const std::string& s){ return s.length() + len; }; auto x = std::accumulate(svec.begin(), svec.end(), 0u, f); std::cout << x << '\n'; } * Örnek 4, template< typename Iter, typename Init > Init Accumulate1(Iter beg, Iter end, Init init) { while(beg != end){ init = std::move(init) + *beg; ++beg; } return init; } template< typename Iter, typename SenType, typename Init, typename Op = std::plus<> > Init Accumulate2(Iter beg, SenType end, Init init, Op op = {}) { while(beg != end){ init = op(std::move(init), *beg); ++beg; } return init; } template< typename Iter, typename SenType, typename Init, typename Op = std::plus<> > requires std::input_iterator && std::sentinel_for Init Accumulate3(Iter beg, SenType end, Init init, Op op = {}) { while(beg != end){ init = op(std::move(init), *beg); ++beg; } return init; } template< std::input_iterator Iter, std::sentinel_for SenType, typename Init, typename Op = std::plus<> > Init Accumulate4(Iter beg, SenType end, Init init, Op op = {}) { while(beg != end){ init = op(std::move(init), *beg); ++beg; } return init; } template< std::input_iterator Iter, std::sentinel_for SenType, typename Init, typename Op = std::plus<> > Init Accumulate5(Iter beg, SenType end, Init init, Op op = {}) { while(beg != end){ init = std::invoke(op, std::move(init), *beg); ++beg; } return init; } template< std::input_iterator Iter, std::sentinel_for SenType, typename Init = std::iter_value_t, typename Op = std::plus<> > Init Accumulate6(Iter beg, SenType end, Init init = Init{}, Op op = {}) { while(beg != end){ init = std::invoke(op, std::move(init), *beg); ++beg; } return init; } template< std::input_iterator Iter, std::sentinel_for SenType, typename Init = std::iter_value_t, typename Op = std::plus<>, typename Proj = std::identity > Init Accumulate7(Iter beg, SenType end, Init init = Init{}, Op op = {}, Proj proj = {}) { while(beg != end){ init = std::invoke(op, std::move(init), std::invoke(proj, *beg)); ++beg; } return init; } template< std::ranges::input_rage R, class Init = std::ranges::range_value_t, typename Op = std::plus<>, typename Proj = std::identity > Init Accumulate7(R&& r, Init init = Init{}, Op op = {}, Proj proj = {}) { return Accumulate7( std::ranges::begin(r), std::ranges::end(r), std::move(init), std::move(op), std::move(proj) ); } /*================================================================================================================================*/ (21_10_09_2023) > "ranges" kütüphanesi (devam): Anımsanacağı üzere, yukarıdaki örnekte kullandığımız ve "ranges" isim alanı içerisinde bulunan "sort" gibi işlevler aslında bir fonksiyon değildir. Fakat eski STL fonksiyonları birer şablonlardı. Artık yeni STL'dekiler bir sınıf türünden nesnelerdir. "sort()" ifadesiyle de bizler, o sınıfın ".operator()()" fonksiyonuna çağrı yapmaktayız. Yani aslında ".operator()()" fonksiyonları şablon halindedir. Pekiyi arkadaki esas sınıf nedir? * Örnek 1, #include #include #include int main() { auto name{ typeid(std::ranges::sort).name() }; std::cout << name << ' '; // class std::ranges::_Sort_fn } * Örnek 2, Possible Implementation taken by "https://en.cppreference.com/w/cpp/algorithm/ranges/sort". struct sort_fn { template< std::random_access_iterator I, std::sentinel_for S, class Comp = ranges::less, class Proj = std::identity > requires std::sortable< I, Comp, Proj > constexpr I operator()(I first, S last, Comp comp = {}, Proj proj = {}) const { if (first == last) return first; I last_iter = ranges::next(first, last); ranges::make_heap(first, last_iter, std::ref(comp), std::ref(proj)); ranges::sort_heap(first, last_iter, std::ref(comp), std::ref(proj)); return last_iter; } template< ranges::random_access_range R, class Comp = ranges::less, class Proj = std::identity > requires std::sortable< ranges::iterator_t, Comp, Proj > constexpr ranges::borrowed_iterator_t< R > operator()(R&& r, Comp comp = {}, Proj proj = {}) const { return (*this)(ranges::begin(r), ranges::end(r), std::move(comp), std::move(proj)); } }; inline constexpr sort_fn sort {}; Pekiyi neden böyle bir şey yapılmış? Anımsanacağı üzere "ADL" mekanizmasının devreye girmesi için çağrılan şeyin doğrudan fonksiyon olması gerekmektedir. Bir "function object" vasıtasıyla yapılan fonksiyon çağrılarında "ADL" devreye GİRMEYECEKTİR. Böylelikle isim arama ile ilgili bazı problemler de ortadan kaldırılmış olur. Özetle bizim yeni algoritmalarımız doğrudan fonksiyon DEĞİLLERDİR. Şimdi de "view" kavramı üzerinde duralım. >> "view" kavramı: "ranges" kütüphanesinin asıl önemli avantajlarından birisi, fonksiyonel programlama paradigmasına uyum göstermesini sağlayan, bünyesinde barındırdığı "view" kavramıdır. "view" da bir "range" olarak geçmektedir fakat hafif siklet bir "range". "range" yerine "view" oluşturduğumuzda; -> Sabit zamanda, "in constant time", kopyalanma ya da taşınma garantisi elde ediyoruz. -> "view" konseptini ve başka diğer konseptini karşılayan varlıklar, özel bazı işlemlerde kullanılabiliyorlar. Örneğin, "range-based for-loop" da kullanılabiliyorlar. Böylece bir "range" den hareketle bir "view" oluşturabiliyor, bu "view" ları fonksiyonlara argüman olarak geçebiliyor, bu "view" ları "range" ler ile ya da başka "view" lar ile birlikte bir "Composition" ilişkisi içerisinde kullanabiliyorum. Yine arka planda "view" lar da birer "function object" tir ve "Range Adaptors" dediğimiz varlıklar, bize, fonksiyon çağrı operatörleri ile "view" konseptini destekleyen öğeler döndürüyorlar. Yani bir "view" oluşturmanın yolu ya doğrudan o "view" türünden bir nesne oluşturmak ya da "function object" kullanarak fonksiyon çağrı operatörü ile ilgili "view" ı elde etmektir. Bu "view" kavramı içerisindeki varlıklar, "std::ranges" içerisindeki "view" isim alanı içerisindedir. Bir "range" in "view" niteliğinde olması için, "ranges::view" konseptini desteklemesi gerekmektedir. * Örnek 1, #include #include #include #include "MyUtility.h" int main() { /* # OUTPUT # 41 946 347 714 695 172 222 791 705 669 394 401 490 7 367 371 619 846 440 273 ----------------------------------------------------------------------------- 41 946 347 714 695 172 222 791 705 669 695 705 490 440 */ std::vector ivec; MyUtility::Utility::rfill(ivec, 20, MyUtility::Random::Irand{0, 999}); MyUtility::Utility::print(ivec); // namespace allias kullanıldığı için bu şekilde yazılmıştır. for(auto i : std::views::take(ivec, 10)){ // Buradaki "range-based for loop", "std::views::take(ivec, 10)" // ifadesi ile elde edilen "range" i dolaşacaktır. Bu "range" ise // "ivec" içerisindeki ilk on öğeden meydana gelmektedir. std::cout << i << ' '; } puts("\n"); // namespace allias kullanıldığı için bu şekilde yazılmıştır: for(auto i : std::views::filter(ivec, [](int x){ return x%5==0; })){ // Buradaki "range-based for loop", "std::views::filter(ivec, [](int x){ return x%5==0; })" // ifadesi ile elde edilen "range" i dolaşacaktır. Bu "range" ise // "ivec" içerisindeki elemanlardan beşe tam bölünenlerden // oluşmaktadır. std::cout << i << ' '; } } * Örnek 2, #include #include #include #include #include "MyUtility.h" int main() { /* # OUTPUT # 511 407 807 315 781 143 366 161 899 196 925 71 990 323 557 76 957 645 514 40 ----------------------------------------------------------------------------- 511 407 807 315 781 143 366 161 899 196 511 407 807 315 781 143 366 161 899 196 511 407 807 315 781 143 366 161 899 196 511 407 807 315 781 143 366 161 899 196 511 407 807 315 781 143 366 161 899 196 511 407 807 315 781 143 366 161 899 196 */ std::vector ivec; MyUtility::Utility::rfill(ivec, 20, MyUtility::Random::Irand{0, 999}); MyUtility::Utility::print(ivec); for(auto i : /* std::views::take(ivec, 10) */ ivec | std::views::take(10)) std::cout << i << ' '; puts(""); for(auto i : std::ranges::take_view(ivec, 10)) std::cout << i << ' '; puts("\n"); std::ranges::for_each( std::views::take(ivec, 10), [](int x){ std::cout << x << ' '; } ); puts(""); std::ranges::for_each( std::ranges::take_view(ivec, 10), [](int x){ std::cout << x << ' '; } ); puts("\n"); auto vw = std::views::take(ivec, 10); for(auto i : vw) std::cout << i << ' '; puts(""); std::ranges::for_each( vw, [](int x){ std::cout << x << ' '; } ); puts("\n"); } * Örnek 3, #include #include #include #include #include "MyUtility.h" int main() { /* # OUTPUT # 954 605 118 462 156 834 78 905 397 594 214 638 176 715 133 265 515 587 79 278 ----------------------------------------------------------------------------- 954 605 118 462 156 834 78 905 397 594 156 462 118 605 954 156 462 118 954 */ std::vector ivec; MyUtility::Utility::rfill(ivec, 20, MyUtility::Random::Irand{0, 999}); MyUtility::Utility::print(ivec); // std::view::reverse(ivec) // Bu ifade bir "view", aynı zamanda da bir "range". for(auto i : std::views::take(ivec, 10)) // İş bu "range" içerisinde, "ivec" içerisindeki ilk on öğe vardır. std::cout << i << ' '; puts("\n"); // İş bu "range" içerisinde, yukarıdaki 10 öğenin baştan beş tanesinin ters sıralanmış hali vardır. for(auto i : std::views::reverse(std::views::take(ivec, 5))) std::cout << i << ' '; puts("\n"); // İş bu "range" içerisinde, yukarıdaki beş öğeden ikiye tam bölünenler vardır. for(auto i: std::views::filter(std::views::reverse(std::views::take(ivec, 5)), [](int x){ return x%2==0; })) std::cout << i << ' '; puts("\n"); } * Örnek 4, #include #include #include #include #include "MyUtility.h" int main() { /* # OUTPUT # 615 663 279 487 212 659 201 366 175 459 973 551 249 97 32 967 207 335 840 674 ----------------------------------------------------------------------------- 212 366 */ std::vector ivec; MyUtility::Utility::rfill(ivec, 20, MyUtility::Random::Irand{0, 999}); MyUtility::Utility::print(ivec); // Yukarıdaki "ivec" dizisinin ilk 10 öğesinden ikiye tam bölünenleri aldık: for(auto i : ivec | std::views::take(10) | std::views::filter( [](int x){ return x%2==0; } )) // Pipe-line mechanism. std::cout << i << ' '; puts("\n"); } /*================================================================================================================================*/ (22_16_09_2023) > "ranges" kütüphanesi (devam): "range" LER BİR TÜR BELİRTMEZLER, BİR "concept" TİRLER. Yani "range concept" ini "satisfy" eden varlıklara "range" denmektedir. >> "view" kavramı (devam): Anımsanacağı üzere "view" lar "light-weight range" olarak da geçer ve bir "view" oluşturabilmek için birden fazla imkana sahibiz. Bunlardan ilki doğrudan o "view" türünden bir nesne oluşturmak ki bu durumda şablon argümanlarını bizim yazmamız gerekmektedir. Bu da hem okuma hem de yazma zahmeti doğurmaktadır. Diğer yöntemler ise "Range Adaptors" ve "Range Factories" nesnelerini kullanmaktır. >>> "Range Adaptors" ve "Range Factories": Öyle fonksiyon nesneleridir ki argüman olarak bir ya da birden fazla "range" alıp, belirli niteliğe sahip bir "range" döndürürler. Bu niteliklerden en önemlisi, geri döndürdükleri "range" in "view" konseptini desteklemesidir. Bir diğer deyişle; bunlar öyle adaptörlerdir ki argüman olarak "range" alır. Geri dönüş değeri olarak da "view" kavramına adapte edilmiş bir "range" döndürürler. Yani "view" niteliğinde bir "range" döndürürler. Pekiyi bu nesneleri kullanmanın avantajları nelerdir? Şöyleki; >>> Aşağıdaki örnekte de görüldüğü üzere "take" ile elde ettiğimiz nesne de bir "range" fakat "view" için adapte edilmiş, yani "view" özelliği olan, bir "range". * Örnek 1, #include #include #include int main() { std::vector vec{ 4, 6, 7, 9, 4, 2, 9, 1 }; auto vw = std::views::take(vec, 4); static_assert(std::ranges::view); return 0; } >>> Böylesi adaptörler ile elde edilen "range" ler daha az yer kapladıklarından, kopyalama maliyetleri de düşüktür. * Örnek 1, #include #include #include int main() { /* # OUTPUT # sizeof vec: 24 => 4 6 7 9 4 2 9 1 sizeof vec_range: 24 => 4 6 7 9 sizeof vw: 16 => 4 6 7 9 */ std::vector vec{ 4, 6, 7, 9, 4, 2, 9, 1 }; std::cout << "sizeof vec: " << sizeof(vec) << " => "; for(auto i : vec) std::cout << i << ' '; std::cout << '\n'; std::vector vec_range(vec.begin(), vec.begin() + 4); std::cout << "sizeof vec_range: " << sizeof(vec_range) << " => "; for(auto i : vec_range) std::cout << i << ' '; std::cout << '\n'; auto vw = std::views::take(vec, 4); std::cout << "sizeof vw: " << sizeof(vw) << " => "; for(auto i : vw) std::cout << i << ' '; std::cout << '\n'; return 0; } >>> "Lazy Evaluation" olmalarıdır. Yani biz o "range" den öğe talep ettikçe adapte işlemi yapılmaktadır. * Örnek 1, #include #include #include int main() { /* # OUTPUT # === === C++ C++ === */ std::vector vec{ 4, 6, 7, 9, 4, 2, 9, 1 }; std::cout << "===\n"; auto filter = std::views::filter(vec, [](int a){ std::cout << "C++\n"; return a%3==0; }); // Bu aşamada ekrana "C++" yazısının çıkmadığı görülmektedir. std::cout << "===\n"; auto filter_begin = filter.begin(); // Bu aşamada ekrana iki adet "C++" yazısının çıktığı görülmektedir. std::cout << "===\n"; return 0; } >>> Bir "Range Adaptor" ü ile elde ettiğim "range" i, yine başka bir "Range Adaptor" üne argüman olarak da geçebilirim. Yani "composability". Bu özellik "range" kütüphanesinin birincil avantajıdır. Çünkü böyle bir özellik olmasaydı, bir çok durumda kod yazma zahmeti oluşacak ve ilave kopyalamalar yapmak gerekecekti. * Örnek 1.0, STL 1.0 sürümü ile yapılış. #include #include #include #include int main() { /* # OUTPUT # 4 6 7 9 4 2 9 1 8 12 7 4 6 4 2 8 12 16 36 16 4 64 144 */ std::vector vec{ 4, 6, 7, 9, 4, 2, 9, 1, 8, 12, 7 }; for(auto i : vec) std::cout << i << ' '; std::cout << '\n'; // I. "vec" nesnesi içerisindeki çift olan öğeleri alacağız: std::vector temp; std::copy_if( vec.begin(), vec.end(), std::back_inserter(temp), [](int x){ return x%2==0; } ); for(auto i : temp) std::cout << i << ' '; std::cout << '\n'; // II. Almış olduğumuz çift sayıların karelerini bir "destination" // öğesine yazacağız. std::vector dest; std::transform( temp.begin(), temp.end(), std::back_inserter(dest), [](int x){ return x*x; } ); for(auto i : dest) std::cout << i << ' '; std::cout << '\n'; return 0; } * Örnek 1.1, STL 2.0 sürümü ile yapılış. (https://wandbox.org/permlink/jb2Uo9DJdT1U5m7q) #include #include #include #include #include namespace rng = std::ranges; namespace vw = std::views; void filter_and_transform(const std::vector& vec) { for(auto i : vec) std::cout << i << ' '; std::cout << '\n'; auto filter_and_transform = vw::transform( vw::filter( vec, [](int x){ return x%2==0; } ), [](int x){ return x*x; } ); std::cout << std::format("Filtered & Transformed Elements: {}\n", filter_and_transform); } void filter_and_transform_with_piping(const std::vector& vec) { for(auto i : vec) std::cout << i << ' '; std::cout << '\n'; auto filter_and_transform = vec | vw::filter([](int x){ return x%2==0; }) | vw::transform([](int x){ return x*x; }); std::cout << std::format("Filtered & Transformed Elements: {}\n", filter_and_transform); } int main() { /* # OUTPUT # 4 6 7 9 4 2 9 1 8 12 7 Filtered & Transformed Elements: [16, 36, 16, 4, 64, 144] 4 6 7 9 4 2 9 1 8 12 7 Filtered & Transformed Elements: [16, 36, 16, 4, 64, 144] */ std::vector vec{ 4, 6, 7, 9, 4, 2, 9, 1, 8, 12, 7 }; // I. "vec" nesnesi içerisindeki çift olan öğeleri alacağız. // II. Almış olduğumuz çift sayıların karelerini bir "destination" // öğesine yazacağız. filter_and_transform(vec); filter_and_transform_with_piping(vec); return 0; } Pekiyi "Range Adaptors" ile "Range Factories" arasındaki temel fark nedir? "Range Adaptors" nesneleri bir "range" i alıp bir "range" döndürmektedir. Fakat "Range Factories", bizden bir "range" almadan bir "range" döndürmektedir. Yani o "range" i kendisi oluşturmaktadır. * Örnek 1, #include #include int main() { auto vw = std::views::iota(10); auto rng = std::views::take(5); for(auto i : vw | rng) std::cout << i << ' '; // 10 11 12 13 14 std::cout << '\n'; return 0; } * Örnek 2, #include #include #include #include "MyUtility.h" using namespace MyUtility; int main() { /* # INPUT # iki sayi girin: 1 100 */ /* # OUTPUT # 97 89 83 79 73 71 67 61 59 53 47 43 41 37 31 29 23 19 17 13 11 7 5 3 2 */ namespace rng = std::ranges; namespace vw = std::views; int start_point, end_point; std::cout << "iki sayi girin: "; std::cin >> start_point >> end_point; auto theFilter = vw::iota(start_point, end_point) | vw::reverse | vw::filter(Utility::isprime); for (auto i : theFilter) std::cout << i << ' '; std::cout << '\n'; } Buradaki en önemli "Range Adaptors" / "Range Factories" nesneleri olarak "common" ve "subrange" nesnelerini örnek gösterebiliriz. Bu adaptörler ise şu işlevleri yerine getirmektedir: >>> "std::ranges::views::common" : Bir adet "range" i argüman olarak göndeririz. İş bu argümanın niteliğine göre bize üç farklı "range" geri döndürmektedir. Şöyleki; -> Eğer gönderdiğimiz "range", bir "Common Range" niteliğindeyse, aynısını geri döndürmektedir. -> Eğer "view" olmayan bir "range" ise ama "begin" ve "end" in geri dönüş değerleri aynıysa, "ref_view" geri döndürmektedir. "ref_view" konusu ileride ele alınacaktır. -> Yukarıdaki ikisi haricinde bir "range" ise, yani "begin" ve "end" in geri dönüş değerleri farklıysa, "common_view" geri döndürmektedir. Buradan hareketle geri dönüş değeri ya gönderdiğimiz "view" in kendisi, ya "common_view" ya da "ref_view" olmaktadır. * Örnek 1, #include #include #include int main() { std::list myList(10); // "list" türünde bir nesne oluşturulmuş. auto my_take = std::views::take(myList, 4); // "take" isimli adaptör kullanılarak bir nesne oluşturulmuş. auto my_iota = std::views::iota(35, 45); // "iota" isimli bir "factory" kullanılarak bir nesne oluşturulmuş. auto x = std::views::common(myList); // "x" is a "std::ranges::ref_view". Çünkü "begin" ve "end" ile elde edilenler // aynı tür fakat oluşturulan "range" bir "view" DEĞİL. auto y = std::views::common(my_take); // "y" is a "std::ranges::common_view". Çünkü "begin" ve "end" ile elde edilenler // aynı tür DEĞİL. auto z = std::views::common(my_iota); // "z" is a "std::ranges::iota_view". Çünkü argümanımız hem "view" hem de // "begin" ve "end" ile elde edilenler aynı tür. } * Örnek 2, #include #include #include #include #include "MyUtility.h" template struct Sentinel { bool operator==(auto pos) const { return *pos == ENDVAL; } }; int main() { /* # OUTPUT # 5 */ std::vector ivec(20); // 20 elemanlı bir dizimiz var ve her birinin değeri "0". ivec[5] = -1; // 5 indisli öğenin değerini "-1" yaptık. std::ranges::subrange sr(ivec.begin(), Sentinel<-1>()); // "range" nin "begin" ile "end" türleri farklı türlerdir. // Yani "Common Range" niteliğinde DEĞİL. // auto e = std::count(sr.begin(), sr.end(), 0); // "Common Range" OLMADIĞI İÇİN SENTAKS HATASI. auto cv = std::views::common(sr); // Artık "Common Range" niteliğinde. auto n = std::count(cv.begin(), cv.end(), 0); // Dolayısıyla "STL 1.0" içerisinde bulunan "count" fonksiyonuna // argüman olarak geçebiliriz. Yani sıfırıncı indisli öğe ile // beş indisli öğe arasındaki öğelerden değeri "0" olanları // sayacağız. std::cout << n << '\n'; } * Örnek 3, #include #include #include #include int main() { /* # OUTPUT # 61 */ std::vector ivec{ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37 }; auto rng = ivec | std::views::filter([](int x){ return x%10==7; }); // "ivec" içerisindeki baştan itibaren ilk // basamağı "7" olanlardan bir "range" oluşturacaktır. auto sum = std::accumulate(rng.begin(), rng.end(), 0); std::cout << sum << '\n'; } * Örnek 4, #include #include #include #include #include "MyUtility.h" using namespace MyUtility; int main() { std::vector ivec{ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37 }; auto rng = ivec | std::views::take_while([](int x){ return x<20; }); // "ivec" içerisindeki baştan itibaren değeri // 20'den küçük olanlar ile bir "range" oluşturacaktır. // Fakat "take_while" ile oluşturulan "range", bir // "common_range" DEĞİL. // Bir "range" nin "common_range" olup olmadığını aşağıdaki şekilde sınayabiliriz. static_assert(std::ranges::common_range); // error: static assertion failed } * Örnek 5, #include #include #include #include int main() { /* # OUTPUT # sum = 17 */ std::vector ivec{ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37 }; // auto rng = ivec | std::views::take_while([](int x){ return x<10; }) | std::views::common; auto rng = std::views::common( std::views::take_while( ivec, [](int x){ return x<10; } ) ); auto sum = std::accumulate(rng.begin(), rng.end(), 0); std::cout << "sum = " << sum << '\n'; static_assert(std::ranges::common_range); } >>> "std::ranges::subrange" : Bir "range" içerisindeki başka bir "range" demektir. Argüman olarak bir "range" gönderebildiğimiz gibi iki adet iteratör de gönderebiliriz. Dolayısıyla geri dönüş değerinin ne olduğu da gönderdiğimiz argümanlara göre değişkenlik gösterecektir. Üçüncü parametresine geçeceğimiz argümana göre, elde edilecek "range" nin "sized_range" olup olmadığını da belirleyebiliriz. * Örnek 1, #include #include #include #include #include int main() { /* # OUTPUT # sizeof sr1: 16 1 9 1 3 5 7 9 [1, 3, 5, 7, 9] [1, 3, 5, 7, 9] */ std::vector ivec{ 1, 3, 5, 7, 9 }; std::ranges::subrange sr1{ ivec }; // CTAD std::cout << "sizeof sr1: " << sizeof(sr1) << '\n'; static_assert(std::ranges::view); auto& r1 = sr1.front(); auto& r2 = sr1.back(); std::cout << r1 << ' ' << r2 << '\n'; std::ranges::subrange sr2{ ivec.begin(), ivec.end() }; for(const auto i : sr2) std::cout << i << ' '; std::cout << std::format("\n{}\n{}\n", sr1, sr2); // C++23 } * Örnek 2, #include #include #include #include template class Sentinel { public: bool operator==(auto x) const { return *x==ENDVAL; } }; int main() { /* # OUTPUT # [1, 3, 5, 7, 9] */ std::vector ivec{ 1, 3, 5, 7, 9, 8, 6, 4, 2, 0 }; std::ranges::subrange sb(ivec.begin(), Sentinel<8>{}); std::cout << std::format("{}\n", sb); // C++23 } * Örnek 3, #include #include #include #include template struct Sentinel { bool operator==(const auto x) const { return *x==ENDVAL; } }; // Constraint Template Parameter: "range" konseptinin "satisfied" olması gerekmektedir. // Yani ilgili konsepti sağlayan hem "R-Value" hem "L-Value" ile çağrılabilir. void print(std::ranges::range auto&& r) { for(const auto& val: r) std::cout << val << ' '; std::cout << '\n'; } int main() { /* # OUTPUT # 0 1 2 3 4 0 1 2 3 4 */ // auto vw = std::ranges::iota_view{ 0, 10 }; // std::vector ivec(vw.begin(), vw.end() + 10); auto ivec = std::ranges::iota_view{ 0, 10 } | std::ranges::to(); // Since C++23 std::ranges::subrange s1{ ivec.begin(), ivec.begin() + 5}; print(s1); std::ranges::subrange s2{ ivec.begin(), Sentinel<5>{}}; print(s2); } * Örnek 4.0, #include #include #include #include int main() { /* # OUTPUT # b1: 1 b2: 0 */ std::vector ivec{ 1, 3, 5, 7, 9, 8, 6, 4, 2, 0 }; std::ranges::subrange sbvec{ next(ivec.begin()), prev(ivec.end()) }; constexpr bool b1 = std::ranges::sized_range; std::cout << "b1: " << b1 << '\n'; std::list ilist{ 0, 2, 4, 6, 8, 9, 7, 5, 3, 1 }; std::ranges::subrange sblist{ next(ilist.begin()), prev(ilist.end()) }; constexpr bool b2 = std::ranges::sized_range; std::cout << "b2: " << b2 << '\n'; } * Örnek 4.1, #include #include #include #include int main() { /* # OUTPUT # b1: 1 b2: 1 */ std::vector ivec{ 1, 3, 5, 7, 9, 8, 6, 4, 2, 0 }; std::ranges::subrange sbvec{ next(ivec.begin()), prev(ivec.end()) }; constexpr bool b1 = std::ranges::sized_range; std::cout << "b1: " << b1 << '\n'; std::list ilist{ 0, 2, 4, 6, 8, 9, 7, 5, 3, 1 }; std::ranges::subrange sblist{ ilist.begin(), ilist.end(), ilist.size() }; constexpr bool b2 = std::ranges::sized_range; std::cout << "b2: " << b2 << '\n'; } * Örnek 5, #include #include #include int main() { /* # OUTPUT # b1: 1 b2: 1 */ int ar1[10]{}; int ar2[20]{}; static_assert(std::same_as); // Would Hold False using sbr_type1 = decltype(std::ranges::subrange{ar1}); using sbr_type2 = decltype(std::ranges::subrange{ar2}); static_assert(std::same_as); // Holds True /* * Buradan da şu sonucu çıkartmaktayız; * türleri farklı olan iki diziden, "subrange" oluşturarak, * aynı türden iki "range" oluşturabiliriz. */ } * Örnek 6, C++20, C++23 ile bir "range" in baştan veya sondan yarısını çekebileceğimiz bir "Range Adaptosr/Factories" bulunmamaktadır. Bu iş için aşağıdaki kodları kullanabiliriz. #include #include #include #include template auto left_half(Range r) { return std::ranges::subrange( std::begin(r), std::begin(r) + std::ranges::size(r) / 2 ); } template auto right_half(Range r) { return std::ranges::subrange( std::begin(r) + std::ranges::size(r) / 2, std::end(r) ); } int main() { /* # OUTPUT # [1, 3, 5, 7, 9] + [8, 6, 4, 2, 0] = [1, 3, 5, 7, 9, 8, 6, 4, 2, 0] */ std::vector ivec{ 1, 3, 5, 7, 9, 8, 6, 4, 2, 0 }; std::cout << std::format("{} + {} = {}\n", left_half(ivec), right_half(ivec), ivec); } Pekala bizler bir "range" içerisindeki öğeleri bir "container" içerisine yazabilir miyiz? C++23 ile bunun şöyle çok kısa bir yolu vardır: * Örnek 1, #include #include #include #include #include "MyUtility.h" using namespace MyUtility; int main() { /* # INPUT # iki sayi girin: 1 100 */ /* # OUTPUT # Size: 25 => 97 89 83 79 73 71 67 61 59 53 47 43 41 37 31 29 23 19 17 13 11 7 5 3 2 */ namespace rng = std::ranges; namespace vw = std::views; int start_point, end_point; std::cout << "iki sayi girin: "; std::cin >> start_point >> end_point; auto theFilteredContainer = vw::iota(start_point, end_point) | vw::reverse | vw::filter(Utility::isprime) | rng::to(); std::cout << "Size: " << theFilteredContainer.size() << "=> "; for (auto i : theFilteredContainer) std::cout << i << ' '; std::cout << '\n'; } > Hatırlatıcı Notlar: >> "sized_range" : Bir "range" in "sized_range" olması için öyle bir ".size()" fonksiyonuna sahip olmalıdır ki "constant time" içerisinde bize "size" bilgisini döndürecek. Yani ".size()" fonksiyonu "constant time" içerisinde o "range" de bulunan öğe sayısını döndürmelidir. Bu da beraberinde şunu getirmektedir; bir "range" in "sized_range" olup olmaması, bir takım işlemlerin yapılabilecek ve yapılamayacak olduğunu belirtmektedir. * Örnek 1, #include #include #include int main() { namespace rng = std::ranges; static_assert(rng::sized_range>); // Holds True } * Örnek 2, #include #include #include #include int main() { namespace rng = std::ranges; static_assert(rng::sized_range>); // Holds Failed } >> Aşağıdaki örneği inceleyelim: * Örnek 1, #include #include #include #include // Şablon, "constraint" edilmiştir. Şablon parametresine geçilen argüman "input_range" tip // "constraint" sağlamıyorsa, sentaks hatası oluşacaktır. Öte yandan fonksiyonun parametresi // bir "Universal Reference" şeklindedir. Fonksiyonun ismi "get_min" şeklinde olup, geri // dönüş değeri "std::ranges::range_value_t" nin "Range" açılımı biçimindedir. Tabii geri // dönüş değeri yerine "auto" da yazabilirdik. template std::ranges::range_value_t get_min(Range&& rng) { // Buradaki "empty" ismindeki "Function Object", fonksiyona // geçtiğimiz "range" nin boş olup olmadığını sınamaktadır. if(std::ranges::empty(rng)) return std::ranges::range_value_t{}; // Burada ise "begin" ismindeki "Function Object" ile // fonksiyona geçilen "range" in ilk öğesini gösteren // iteratörü elde etmiş oluyoruz. Yani burada ilgili // "range" nin ilk öğesini en küçük kabul ettik. auto pos = std::ranges::begin(rng); auto min = *pos; // Bir sonraki konum ilgili "range" nin son konumu // olmadığı müddetçe "range" içerisinde ilerliyoruz. // Burada son konum, ilk konum ile aynı türden olmak // zorunda değildir. Yani "Common Range" olmayan bir // "range" i de fonksiyona geçebiliriz. Döngünün her // turunda da o konumdaki değeri kullanarak ilgili // "range" içerisindeki en küçük öğeyi bulmuş oluyoruz. while(++pos != std::ranges::end(rng)) if(*pos < min) min = *pos; return min; } int main() { /* # OUTPUT # 2 */ std::vector ivec{ 9, 7, 2, 6, 5, 4 }; std::cout << get_min(ivec); } >> "range" LER BİR TÜR BELİRTMEZLER, BİR "concept" TİRLER. Yani "range concept" ini "satisfy" eden varlıklara "range" denmektedir. >> Mülakat Sorusu: 3 adet dizi var ve her birisinde sayılar tutuluyor. Bir tane toplam değeri giriliyor. Her diziden bir eleman almak suretiyle, girilen toplam değeri elde edilmeye çalışılıyor. En az bir tane toplam değerine erişilmesi yeterlidir. Eğer hiç bir şekilde erişilemiyorsa, çıktı olarak da bildirilmesi gerekmektedir. * Çözüm 1, "O(n^3)" karmaşıklığında: #include #include #include #include "MyUtility.h" using namespace MyUtility; using ivec = std::vector; auto get_vector(std::size_t size) { ivec vec(size); std::generate(vec.begin(), vec.end(), Random::Irand{-10'000, 10'000}); // Way - I // std::generate_n(vec.begin(), size, Random::Irand{-10'000, 10'000}); // Way - II // std::ranges::generate(vec, Random::Irand{-10'000, 10'000}); // Way - III return vec; } int main() { auto vec1 = get_vector(10'000u); auto vec2 = get_vector(10'000u); auto vec3 = get_vector(10'000u); std::cout << "Total Value: "; int sum{}; std::cin >> sum; O(n^3) for (auto i : vec1) for (auto j : vec2) for (auto k : vec3) if (i + j + k == sum) std::cout << std::format("{} + {} + {} = {}\n", i, j, k, sum); } * Çözüm 2, "o(n^2logn)" karmaşıklığında: #include #include #include #include "MyUtility.h" using namespace MyUtility; using ivec = std::vector; auto get_vector(std::size_t size) { ivec vec(size); std::generate(vec.begin(), vec.end(), Random::Irand{-10'000, 10'000}); // Way - I // std::generate_n(vec.begin(), size, Random::Irand{-10'000, 10'000}); // Way - II // std::ranges::generate(vec, Random::Irand{-10'000, 10'000}); // Way - III return vec; } int main() { auto vec1 = get_vector(10'000u); auto vec2 = get_vector(10'000u); auto vec3 = get_vector(10'000u); std::ranges::sort(vec3); // O(nlogn) std::cout << "Total Value: "; int sum{}; std::cin >> sum; // O(n^2) for (auto i : vec1) for (auto j : vec2) if (auto iter = std::ranges::lower_bound(vec3, sum - (i + j)); iter != vec3.end()) std::cout << std::format("{} + {} + {} = {}\n", i, j, *iter, sum); } /*================================================================================================================================*/ (23_17_09_2023) > "ranges" kütüphanesi (devam): Bu kütüphanedeki parametresi "Universal Referance Range" olanlar algoritmalar, "L-Value" ve/veya "R-Value" değerleri için de çağrılabilmektedir. "R-Value" ile çağrı yaptığımız zaman, ilgili "R-Value" ifadenin ömrü biteceği için, bahsi geçen algoritmaların geri dönüş değeri, "Dangling Iterator" oluşmaması için, "std::ranges::dangling" türünden olacaktır. Bu tür aslında boş bir "struct" şeklindedir. İş bu tür belirlemesi pekala derleme zamanında belli olmaktadır. * Örnek 1, #include #include #include std::vector get_vec() { return { 1, 3, 5, 7, 9 }; } int main() { auto iter = std::ranges::find(get_vec(), 5); // std::ranges::dangling iter = std::ranges::find(get_vec(), 5); } Fakat "std::ranges::dangling" türünden iteratörümüzü "dereference" ettiğimiz noktada sentaks hatası oluşacaktır çünkü bu türün "dereference" operatörü "overload" EDİLMEMİŞTİR. * Örnek 1, #include #include #include std::vector get_vec() { return { 1, 3, 5, 7, 9 }; } int main() { auto iter = std::ranges::find(get_vec(), 5); // std::ranges::dangling iter = std::ranges::find(get_vec(), 5); std::cout << *iter << '\n'; // error: no match for ‘operator*’ (operand type is ‘std::ranges::dangling’) } Özetle bu tip algoritmalara gönderilen ifadelerin değer kategorisine göre geri dönüş değerinin türü, derleme aşamasında, belirlenmektedir. Şimdide aşağıdaki örneği inceleyelim: * Örnek 1, #include #include #include #include int main() { std::string name{ "Ulya" }; auto iter1 = std::ranges::find(std::string_view{ name }, 'U'); std::cout << *iter1 << '\n'; // OK auto iter2 = std::ranges::find(std::vector{ 1, 3, 5, 7, 9 }, 5); std::cout << *iter2 << '\n'; // error: no match for ‘operator*’ (operand type is ‘std::ranges::dangling’) } İşte burada devreye "borrowed_range" kavramı girmektedir. >> "borrowed_range" : Öyle bir "range" ki "a function can take it by value and return iterators obtained from it without danger of dangling". Yani öyle "range" ler ki fonksiyonlara argüman olarak geçilir ve fonksiyonlardan geri dönüş değeri olarak da iş bu "range" in iteratörü geri döndürüldüğünde "dangling iterator" oluşmaz. "std::ranges::borrowed_range" ismindeki "concept" ile bir "range" in "borrowed_range" olup olmadığını sınayabiliriz. Bir "range" in "borrowed_range" olması için ya "L-Value" bir "range" olması ya da "std::ranges::enable_borrowed_range" in ilgili sınıf türünden "specialization" ının "true" değerine çekilmiş olması gerekmektedir. Buraya kadarkileri özetlersek; iteratör döndüren fonksiyonların geri dönüş değeri, -> "R-Value" olan ve "borrowed_range" olmayan "range" ler söz konusu olduğunda "std::ranges::dangling" türü olacak. -> "R-Value" ve "borrowed_range" olan bir "range" gönderirsek, geri dönüş değeri "std::ranges::dangling" olmayacak. Yani doğrudan iteratör türü olacak. Ancak geri dönüş değerinin doğru olması için de ilgili "range" in hayatta olması gerekmektedir. -> "L-Value" olması durumunda, doğrudan iteratör türü olmaktadır. -> "std::ranges::enable_borrowed_range" in ilgili sınıf türünden "specialization" ının "true" olması halinde, doğrudan iteratör türü olmaktadır. * Örnek 1, #include #include #include std::string foo() { return "Ulya"; }; int main() { auto iter3 = std::ranges::find(std::string_view{ foo() }, 'U'); std::cout << *iter3 << '\n'; // Burada bir sentaks hatasının olmaması, "Run Time" hatasının olmayacağı ANLAMINA GELMESİN. // "borrowed_range" olduğu için geri dönüş değerinin türü "std::ranges::dangling" DEĞİLDİR. // Fakat bu "range" in hayatta olup olmadığına ilişkin sorumluluk da bize aittir. } Pekiyi bizler hangi yol ve yöntemler ile bir "range" nesnesinden bir "view" nesnesi elde edebiliriz? Bu yöntemlerden ilki, dün de gördüğümüz "subrange" algoritmasını kullanmaktır. Diğer yöntemler ise şunlardır; "std::ranges::views::all" ve "std::ranges::counted" isimli algoritmaları kullanmaktır. >> "std::ranges::views::all": * Örnek 1, #include #include #include int main() { std::vector ivec{ 1, 4, 6, 7, 9, 12 }; auto v1 = std::views::all(ivec); // "v1" is of type "std::ranges::ref_view" auto v2 = std::views::all(std::vector{ 1, 4, 6, 7, 9, 12 }); // "v2" is of type "std::ranges::owning_view" auto v3 = std::views::all(v1); // "v3" is of type "std::ranges::ref_view" } >> "std::ranges::views::counted": * Örnek 1, #include #include #include #include int main() { std::vector ivec{ 1, 4, 6, 7, 9, 12 }; auto v1 = std::ranges::views::counted(ivec.begin(), 3); // "v1" is of type "std::span" std::list ilist{ 1, 4, 6, 7, 9, 12 }; auto v2 = std::ranges::views::counted(ilist.begin(), 3); // "v2" is of type "std::ranges::subrange" } Şimdi de evvelki derste değindiğimiz "Range Adaptors" / "Range Factories" lerin detaylarına değinelim; Bu algoritmaların iki kullanım yöntemi vardır; "Pipe Linening" sentaks kuralını işleyeceksek "|" atomunun sol tarafına kullanılacak "range", sağ tarafına ise ilgili algoritmayı geçiyoruz. Tabii eğer ilgili algoritmamız bir "predicate" vb. argümanlar alıyorsa, "()" içerisinde onları da belirtmemiz gerekmektedir. Eğer "Pipe Linening" sentaksını kullanmayacaksak, ilgili algoritmanın birinci argümanı kullanılacak "range", diğer argümanları ise varsa "predicate" vb. argümanlardır. >> "std::ranges::views::reverse" : İlgili "range" içerisindeki öğeleri ters-düz eder. * Örnek 1, #include #include #include int main() { std::vector ivec{ 3, 1, -1, 4, 7, 9, -1, 2, 6, 5, 8, 0, -4 }; // Usage as an adaptor (1): for (auto i: std::views::reverse(ivec)) std::cout << i << ' '; // -4 0 8 5 6 2 -1 9 7 4 -1 1 3 std::cout << '\n'; // Usage as an adaptor (2): auto rvs = std::views::reverse(ivec); for (auto i: rvs) std::cout << i << ' '; // -4 0 8 5 6 2 -1 9 7 4 -1 1 3 std::cout << '\n'; // Usage of Pipeline for (auto i: ivec | std::views::reverse) std::cout << i << ' '; // -4 0 8 5 6 2 -1 9 7 4 -1 1 3 std::cout << '\n' // Usage of explicit "reverse_view" object: std::ranges::reverse_view rv = std::views::reverse(ivec); for (auto i: rv) std::cout << i << ' '; // -4 0 8 5 6 2 -1 9 7 4 -1 1 3 std::cout << '\n'; // Work-Around: std::ranges::subrange sr{ ivec.rbegin(), ivec.rend() }; for (auto i: sr) std::cout << i << ' '; // -4 0 8 5 6 2 -1 9 7 4 -1 1 3 std::cout << '\n'; } >> "std::ranges::views::filter" : Argüman olarak bir adet de "predicate" alır ve "true" olan öğeler ile bir "range" oluşturur. * Örnek 1, #include #include #include int main() { std::vector ivec{ 1, 3, 5, 7, 9, 8, 6, 4, 2, 0 }; auto const is_even = [](int x){ return !(x & 1); }; // Usage of the adaptor: auto v1 = std::ranges::views::filter(ivec, is_even); // [8, 6, 4, 2, 0] for (auto i : v1) std::cout << i << ' '; std::cout << '\n'; // Usage of Pipeline Rotation: auto v2 = ivec | std::ranges::views::filter(is_even); // [8, 6, 4, 2, 0] for (auto i : v2) std::cout << i << ' '; std::cout << '\n'; // Usage of explicit filter_view object: std::ranges::filter_view v3{ ivec, is_even }; // [8, 6, 4, 2, 0] for (auto i : v3) std::cout << i << ' '; std::cout << '\n'; } * Örnek 2, #include #include #include #include int main() { std::vector svec{ "ali", "mert", "can", "zeynep", "melike", "necati" }; char c = 'a'; for (const auto& s : std::ranges::views::filter(svec, [c](const auto& s){ return s.contains(c); })) std::cout << s << ' '; // ali can necati c = 'e'; for (const auto& s : svec | std::ranges::views::reverse | std::ranges::views::filter([c](const auto& s){ return s.contains(c); })) std::cout << s << ' '; // necati melike zeynep mert } * Örnek 3, #include #include #include #include int main() { std::vector ivec{ 1, 2, 3, 4, 5, 15, 24, 33 }; const auto f = [](int x){ return x%5==0; }; std::ranges::filter_view my_filter_1{ ivec, f }; for(auto i: my_filter_1) std::cout << i << ' '; // 5 15 std::cout << '\n'; auto my_filter_2 = std::ranges::views::filter( ivec, f ); for(auto i: my_filter_2) std::cout << i << ' '; // 5 15 std::cout << '\n'; auto my_filter_3 = ivec | std::ranges::views::filter(f); for(auto i: my_filter_3) std::cout << i << ' '; // 5 15 std::cout << '\n'; } * Örnek 4.0, Aşağıdaki örnekte "vf" isimli "range" oluşturulurken bir "Sentinel" kullanılması durumunda, "vf.begin()" ve "vf.end()" fonksiyonları farklı türlerden olacağından, "dest" isimli vektörü hayata getiremezdik. #include #include #include #include int main() { std::vector source{ 1, 2, 3, 4, 5, 15, 24, 33 }; auto vf = source | std::ranges::views::filter([](int v){ return v%2==0; }); std::vector dest(vf.begin(), vf.end()); for (auto i : dest) std::cout << i << ' '; // 2 4 24 std::cout << '\n'; } * Örnek 4.1, #include #include #include #include int main() { std::vector source{ 1, 2, 3, 4, 5, 15, 24, 33 }; auto vf = source | std::ranges::views::filter([](int v){ return v%2==0; }) | std::ranges::to(); for (auto i : vf) std::cout << i << ' '; // 2 4 24 std::cout << '\n'; } >> "std::ranges::views::stride" : Argüman olarak "atlama kademesi" alır. Böylelikle üzerinde çalıştığı "range" deki öğelerden, atlama kademesi kadar atlayarak, yeni bir "range" oluşturur. C++23 ile dile eklenmiştir. * Örnek 1, #include #include #include int main() { std::vector ivec{ 1, 3, 5, 7, 9, 8, 6, 4, 2, 0 }; // Usage of the adaptor: auto v1 = std::ranges::views::stride(ivec, 3); // [1, 7, 6, 0] for (auto i : v1) std::cout << i << ' '; std::cout << '\n'; // Usage of Pipeline Rotation: auto v2 = ivec | std::ranges::views::stride(3); // [1, 7, 6, 0] for (auto i : v2) std::cout << i << ' '; std::cout << '\n'; // Usage of explicit filter_view object: std::ranges::stride_view v3{ ivec, 3 }; // [1, 7, 6, 0] for (auto i : v3) std::cout << i << ' '; std::cout << '\n'; } >> "std::ranges::views::counted" : Bizden bir adet iteratör ve "how_many" argümanı alıyor. İteratörden başlayarak, "how_many" adedince ilerliyor. Bu iki nokta arasındaki öğelerden bir "range" oluşturur. Yalnız bunu "Pipeline" mekanizması ile kullanamayız. Ayrıca "std::ranges::counted_view" gibi bir şey de MEVCUT DEĞİLDİR. Dolayısıyla sadece ilgili adaptörü kullanabiliriz. * Örnek 1, #include #include #include int main() { std::vector ivec{ 1, 3, 5, 7, 9, 8, 6, 4, 2, 0 }; // Usage of the adaptor: auto v1 = std::ranges::views::counted(ivec.begin(), 5); // [1, 3, 5, 7, 9] for (auto i : v1) std::cout << i << ' '; std::cout << '\n'; } >> "std::ranges::views::take" : Bir "range" içerisindeki ilk "n" tane öğeden "std::ranges::view" oluşturur. * Örnek 1, #include #include #include int main() { std::vector ivec{ 1, 3, 5, 7, 9, 8, 6, 4, 2, 0 }; // Usage of the adaptor: auto v1 = std::ranges::views::take(ivec, 5); for (auto i : v1) std::cout << i << ' '; std::cout << '\n'; // 1 3 5 7 9 // Usage of Pipeline Rotation: auto v2 = ivec | std::ranges::views::take(5); for (auto i : v2) std::cout << i << ' '; std::cout << '\n'; // 1 3 5 7 9 // Usage of explicit filter_view object: std::ranges::take_view v3{ ivec, 5 }; for (auto i : v3) std::cout << i << ' '; std::cout << '\n'; // 1 3 5 7 9 } >> "std::ranges::views::drop" : Bir "range" içerisindeki ilk "n" tane öğeyi atlamak suretiyle, geri kalan öğelerden "std::ranges::view" oluşturur. * Örnek 1, #include #include #include int main() { std::vector ivec{ 1, 3, 5, 7, 9, 8, 6, 4, 2, 0 }; // Usage of the adaptor: auto v1 = std::ranges::views::drop(ivec, 5); for (auto i : v1) std::cout << i << ' '; std::cout << '\n'; // 8 6 4 2 0 // Usage of Pipeline Rotation: auto v2 = ivec | std::ranges::views::drop(5); for (auto i : v2) std::cout << i << ' '; std::cout << '\n'; // 8 6 4 2 0 // Usage of explicit filter_view object: std::ranges::drop_view v3{ ivec, 5 }; for (auto i : v3) std::cout << i << ' '; std::cout << '\n'; // 8 6 4 2 0 } >> "std::ranges::views::take_while" : Argüman olarak bir "predicate" alır ve o "predicate" "true" değer döndürdüğü sürece, ilgili "range" in başındaki öğelerden, "std::ranges::view" oluşturur. * Örnek 1, #include #include #include int main() { std::vector ivec{ 1, 3, 5, 7, 9, 8, 6, 4, 2, 0 }; auto is_even = [](int x){ return 0 < x; }; // Usage of the adaptor: auto v1 = std::ranges::views::take_while(ivec, is_even); for (auto i : v1) std::cout << i << ' '; std::cout << '\n'; // 1 3 5 7 9 8 6 4 2 // Usage of Pipeline Rotation: auto v2 = ivec | std::ranges::views::take_while(is_even); for (auto i : v2) std::cout << i << ' '; std::cout << '\n'; // 1 3 5 7 9 8 6 4 2 // Usage of explicit filter_view object: std::ranges::take_while_view v3{ ivec, is_even }; for (auto i : v3) std::cout << i << ' '; std::cout << '\n'; // 1 3 5 7 9 8 6 4 2 } >> "std::ranges::views::drop_while" : Argüman olarak bir "predicate" alır ve o "predicate" "true" değer döndürdüğü sürece, ilgili "range" in başındaki öğeleri atlamak suretiyle, "std::ranges::view" oluşturur. * Örnek 1, #include #include #include int main() { std::vector ivec{ 1, 3, 5, 7, 9, 8, 6, 4, 2, 0 }; auto is_even = [](int x){ return 0 < x; }; // Usage of the adaptor: auto v1 = std::ranges::views::drop_while(ivec, is_even); for (auto i : v1) std::cout << i << ' '; std::cout << '\n'; // 0 // Usage of Pipeline Rotation: auto v2 = ivec | std::ranges::views::drop_while(is_even); for (auto i : v2) std::cout << i << ' '; std::cout << '\n'; // 0 // Usage of explicit filter_view object: std::ranges::drop_while_view v3{ ivec, is_even }; for (auto i : v3) std::cout << i << ' '; std::cout << '\n'; // 0 } * Örnek 2.0, #include #include #include int main() { std::vector pvec{ 2, 3, 5, 7, 9, 11, 13, 17, 19, 23, 29 }; auto filter = pvec | // Bu vektördeki... std::views::drop_while([](int x) { return x < 10; }) | //...10'dan küçük değerdeki öğeleri düşür => 11, 13, 17, 19, 23, 29 std::views::reverse | //...ve ters sırala => 29, 23, 19, 17, 13, 11 std::views::drop(3); //...ve baştan üç tanesi hariç, geri kalanlarını çek => 17, 13, 11 for (auto i: filter) std::cout << i << ' '; // 17 13 11 } * Örnek 2.1, #include #include #include #include int main() { std::vector pvec{ 2, 3, 5, 7, 9, 11, 13, 17, 19, 23, 29 }; auto my_list = pvec | // Bu vektördeki... std::views::drop_while([](int x) { return x < 10; }) | //...10'dan küçük değerdeki öğeleri düşür => 11, 13, 17, 19, 23, 29 std::views::reverse | //...ve ters sırala => 29, 23, 19, 17, 13, 11 std::views::drop(3) | //...ve baştan üç tanesi hariç, geri kalanlarını çek => 17, 13, 11 std::ranges::to(); // Elde ettiklerinden de "list" türünde bağlı liste oluştur. for (auto i: my_list) std::cout << i << ' '; // 17 13 11 } >> "std::ranges::views::slide" : Argüman olarak aldığı "range" i parçalara böler ve her bir parça, ikinci parametresindeki öğe kadar öğeye sahiptir. C++23 ile dile eklenmiştir. * Örnek 1, #include #include #include int main() { /* # OUTPUT # 1 3 3 5 5 7 7 9 9 8 8 6 6 4 4 2 2 0 1 3 5 3 5 7 5 7 9 7 9 8 9 8 6 8 6 4 6 4 2 4 2 0 1 3 5 7 3 5 7 9 5 7 9 8 7 9 8 6 9 8 6 4 8 6 4 2 6 4 2 0 */ std::vector ivec{ 1, 3, 5, 7, 9, 8, 6, 4, 2, 0 }; // Usage of the adaptor: auto v1 = std::ranges::views::slide(ivec, 2); for (auto i : v1){ for (auto j: i) std::cout << j << ' '; std::cout << '\n'; } // Usage of Pipeline Rotation: auto v2 = ivec | std::ranges::views::slide(3); for (auto i : v2){ for (auto j: i) std::cout << j << ' '; std::cout << '\n'; } // Usage of explicit filter_view object: std::ranges::slide_view v3{ ivec, 4 }; for (auto i : v3){ for (auto j: i) std::cout << j << ' '; std::cout << '\n'; } } * Örnek 2, #include #include #include #include #include #include int main() { /* # OUTPUT # murat mert gul nihat cevahir jale seyhan -------------------------------------------- murat|mert|gul| mert|gul|nihat| gul|nihat|cevahir| nihat|cevahir|jale| cevahir|jale|seyhan| */ std::vector svec{ "murat", "mert", "gul", "nihat", "cevahir", "jale", "seyhan" }; std::ranges::copy(svec, std::ostream_iterator(std::cout, " ")); std::cout << "\n--------------------------------------------\n"; for (auto rn: std::views::slide(svec, 3)) { for (const auto& s: rn) std::cout << s << '|'; std::cout << '\n'; } std::cout << '\n'; } >> "std::ranges::views::adjacent" : Argüman olarak aldığı "range" i parçalara böler ve her bir parça, şablon parametresindeki öğe kadar öğeye sahiptir. C++23 ile dile eklenmiştir. "std::ranges::views::slide" bize "view" döndürürken, bu ise "tuple-like" obje döndürmektedir. * Örnek 1, #include #include #include int main() { std::vector ivec{ 1, 3, 5, 7, 9, 8, 6, 4, 2, 0 }; // Usage of the adaptor: auto v1 = std::ranges::views::adjacent<2>(ivec); for (auto i : v1) std::cout << i << ' '; std::cout << '\n'; // [1,3] [5,7] [9,8] [6,4] [2,0] // Usage of Pipeline Rotation: auto v2 = ivec | std::ranges::views::adjacent<3>; for (auto i : v2) std::cout << i << ' '; std::cout << '\n'; // [1,3,5] [7,9,8] [6,4,2] [0] } >> "std::ranges::views::pairwise" : "std::ranges::views::slide" a parametre olarak "2" değerinin geçilmiş halidir. Geri döndürdüğü nesne bir "view" değil, "tuple-like" nesnedir. C++23 ile dile eklenmiştir. * Örnek 1, #include #include #include int main() { std::vector ivec{ 1, 3, 5, 7, 9, 8, 6, 4, 2, 0 }; // Usage of Pipeline Rotation: auto v2 = ivec | std::ranges::views::pairwise; for (auto i : v2) std::cout << i << ' '; std::cout << '\n'; // [1,3] [5,7] [9,8] [6,4] [2,0] } >> "std::ranges::views::transform" : Aldığı "range" içerisindeki öğeleri bir "callable" nesneye gönderir ve elde ettiği geri dönüş değerlerinden bir "view" oluşturur. * Örnek 1, #include #include #include int main() { std::string name{ "abcd" }; const auto to_upper = [](char c){ return static_cast(std::toupper(static_cast(c))); }; // Usage of the adaptor: auto v1 = std::ranges::views::transform(name, to_upper); for (auto i : v1) std::cout << i << ' '; std::cout << '\n'; // U L Y A Y U R U K // Usage of Pipeline Rotation: auto v2 = name | std::ranges::views::transform(to_upper); for (auto i : v2) std::cout << i << ' '; std::cout << '\n'; // U L Y A Y U R U K // Usage of explicit filter_view object: std::ranges::transform_view v3{ name, to_upper }; for (auto i : v3) std::cout << i << ' '; std::cout << '\n'; // U L Y A Y U R U K } >> "std::ranges::views::split" : Adeta "tokenization" işlevi görmektedir. Argüman olarak aldığı değeri bir nevi ayraç olarak kullanmak suretiyle, ilgili "range" i bölmektedir. * Örnek 1, #include #include #include int main() { std::vector ivec{ 1, 3, 5, 7, 3, 1, 9, 8, 6, 4, 3, 1, 2, 0 }; // Usage of the adaptor: auto v1 = std::ranges::views::split(ivec, 3); // [1] [5,7] [1,9,8,6,4] [1,2,0] // Usage of Pipeline Rotation: auto v2 = ivec | std::ranges::views::split(1); // [3,5,7,3] [9,8,6,4,3] [2,0] // Usage of explicit filter_view object: std::array arr{ 3,1 }; std::ranges::split_view v3{ ivec, arr }; // [1,3,5,7] [9,8,6,4] [2,0] } * Örnek 2, #include #include #include #include #include #include int main() { /* # OUTPUT # 2 5 4 2 9 8 7 5 6 */ std::vector ivec{ 2, 5, 1, 4, 1, 2, 9, 8, 7, 1, 5, 6 }; auto rng = ivec | std::views::split(1); for (auto i: rng){ for (auto j: i) std::cout << j << ' '; std::cout << '\n'; } } >> "std::ranges::views::chunk_by" : Bir adet "predicate" vardır ve ilgili "range" deki öğeler sıralı biçimde "true" olduğu müddetçe bu öğelerden bir "view" oluşturur. C++23 ile gelmiştir. * Örnek 1, #include #include #include int main() { std::vector ivec{ 1,2,0,2,4,5,8,4,6,3,5,2,4 }; // Usage of the adaptor: auto v1 = std::ranges::views::chunk_by(ivec, std::ranges::less{}); // [1,2] [0,2,4,5,8] [4,6] [3,5] [2,4] const auto diff_less3 = [](int x, int y){ return std::abs(x-y) < 3; }; auto v2 = std::ranges::views::chunk_by(ivec, diff_less3); // [1,2,0,2,4,5] [8] [4,6] [3,5] [2,4] // Usage of Pipeline Rotation: auto v3 = ivec | std::ranges::views::chunk_by(std::ranges::less{}); // [1,2] [0,2,4,5,8] [4,6] [3,5] [2,4] // Usage of explicit filter_view object: std::ranges::chunk_by_view v4{ ivec, std::ranges::less{} }; // [1,2] [0,2,4,5,8] [4,6] [3,5] [2,4] } >> "std::ranges::views::join" : İlgili "range" içerisindeki öğeleri tek bir "range" haline getirir. * Örnek 1, #include #include #include #include int main() { std::vector svec{ "Ulya", "Yuruk", "Istanbul" }; // Usage of the adaptor: auto v1 = std::ranges::views::join(svec); for (auto i : v1) std::cout << i << ' '; std::cout << '\n'; // U l y a Y u r u k I s t a n b u l // Usage of Pipeline Rotation: auto v2 = svec | std::ranges::views::join; // U l y a Y u r u k I s t a n b u l for (auto i : v2) std::cout << i << ' '; std::cout << '\n'; // Usage of explicit filter_view object: std::ranges::join_view v3{ svec }; // U l y a Y u r u k I s t a n b u l for (auto i : v3) std::cout << i << ' '; std::cout << '\n'; } * Örnek 2, #include #include #include #include int main() { std::vector svec{ "Ulya", "Yuruk", "Istanbul", "Uskudar" }; auto v1 = std::ranges::views::join(svec); for (auto i : v1) std::cout << i << ' '; std::cout << '\n'; // U l y a Y u r u k I s t a n b u l U s k u d a r auto v2 = svec | std::ranges::views::reverse | std::ranges::views::join; for (auto i : v2) std::cout << i << ' '; std::cout << '\n'; // U s k u d a r I s t a n b u l Y u r u k U l y a } >> "std::ranges::views::join_with" : Yine "std::ranges::views::join" gibi birleştirme işlemi yapar. Ek olarak araya istediğimiz "delimeter" karakterini de ekler. C++23 ile dile eklenmiştir. * Örnek 1, #include #include #include #include int main() { std::vector svec{ "Ulya", "Yuruk", "Istanbul", "Uskudar" }; auto v1 = std::ranges::views::join(svec); for (auto i : v1) std::cout << i << ' '; std::cout << '\n'; // U l y a Y u r u k I s t a n b u l U s k u d a r auto v2 = svec | std::ranges::views::reverse | std::ranges::views::join_with(','); for (auto i : v2) std::cout << i << ' '; std::cout << '\n'; // U s k u d a r , I s t a n b u l , Y u r u k , U l y a } >> "std::ranges::views::zip" : Argüman olarak iki ya da daha fazla "range" i argüman olarak alır. Geriye de "tuple-like" nesnelerden oluşan bir "view" DÖNDÜRÜR. Bir öğe bir "range", bir öğe diğer "range" den temin edilir. C++23 ile dile eklenmiştir. * Örnek 1, #include #include #include #include int main() { /* # OUTPUT # [1 A][2 h][3 m][4 e][5 t] */ std::string svec1{ "Ahmet" }; std::vector ivec{ 1, 2, 3, 4, 5 }; for (auto t : std::ranges::views::zip(ivec, svec1)) { auto [i, c] = t; std::cout << "[" << i << ' ' << c << "]"; } } * Örnek 2, #include #include #include #include #include int main() { std::vector ivec{ 2, 5, 8, 1, 3, 9 }; std::string name{ "Ulya Yuruk" }; std::vector dvec{ 2.2, 5.5, 8.8, 1.1, 3.3, 9.9 }; std::cout << std::format("{}\n", std::ranges::views::zip(ivec, name, dvec)); // [(2, 'U', 2.2), (5, 'l', 5.5), (8, 'y', 8.8), (1, 'a', 1.1), (3, ' ', 3.3), (9, 'Y', 9.9)] } >> "std::ranges::views::repeat" : C++23 ile dile eklenmiştir. Birinci parametresine öğeyi, ikinci parametresine kaç tane istediğimizi geçiyoruz. Eğer ikinci parametresine bir şey geçilmezse, sonsuz öğeden oluşan bir "view" elde edilecektir. Fakat bu şekilde yalın haliyle kullanmanın da bir manası kalmayacağından, "std::ranges::views::take" veya "std::ranges::views::drop" dan birisini kullanmalıyız. * Örnek 1, #include #include int main() { for (auto i: std::ranges::views::repeat(5, 7)) std::cout << i << ' '; // 5 5 5 5 5 5 5 std::cout << '\n'; for (auto i: std::ranges::views::repeat('A') | std::ranges::views::take(4)) std::cout << i << ' '; // A A A A std::cout << '\n'; } >> "std::ranges::views::iota" : Argüman olarak aldığı rakamdan başlamak suretiyle, bir artarak, "infinite range" oluşturmak için kullanılır. * Örnek 1, #include #include int main() { auto v = std::ranges::views::iota(10); for (auto i: v | std::ranges::views::take(15)) std::cout << i << ' '; // 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 } * Örnek 2, #include #include #include int main() { auto source = std::views::iota(10) //10'dan başlayarak birer artan... | std::views::take(20) //...ilk 20 rakamdan... | std::views::filter([](int i){ return i%2==0; }); //...bir "range" oluşturuldu. std::vector dest; std::ranges::copy(source, std::back_inserter(dest)); for (auto i : dest) std::cout << i << ' '; // 10 12 14 16 18 20 22 24 26 28 } >> "std::ranges::views::elements" : İlgili "tuple" nesnesinin şablon parametresi olarak geçilen indisindeki öğelerden bir "view" oluşturmaya yarar. * Örnek 1, #include #include #include #include #include #include int main() { // Vektörün her bir elemanı bir "tuple" nesnesi. std::vector>> my_vec{ { 12, "ulya", 123u }, { 24, "yuruk", 456u }, { 36, "uskudar", 789u } }; // "index" is a type of "std::tuple<>" for (const auto& index: my_vec) { // "i" is of type "int" // "s" is of type "std::string" // "b" is of type "std::bitset" auto[i, s, b] = index; std::cout << "[" << i << ", " << s << ", " << b << "]"; } std::cout << "\n--------------------------\n"; // "ivec" is a vector containing "int" types, obtained from the "tuple". auto ivec = my_vec | std::ranges::views::elements<0> | std::ranges::to(); for (auto i: ivec) std::cout << i << ' '; std::cout << '\n'; // "svec" is a vector containing "std::string" types, obtained from the "tuple". auto svec = my_vec | std::ranges::views::elements<1> | std::ranges::to(); for (auto i: svec) std::cout << i << ' '; std::cout << '\n'; // "bvec" is a vector containing "std::bitset" types, obtained from the "tuple". auto bvec = my_vec | std::ranges::views::elements<2> | std::ranges::to(); for (auto i: bvec) std::cout << i << ' '; std::cout << '\n'; } >> "std::ranges::views::keys" : "std::pair" çifti tutan "range" içerisindeki "key" değerlerinden bir "view" oluşturmaya yarar. * Örnek 1, #include #include #include #include #include #include int main() { std::vector> my_vector{ { 28, "ulya yuruk" }, { 34, "Istanbul" }, { 52, "Ordu" } }; for (const auto& s : std::views::keys(my_vector)) std::cout << s << ' '; // 28 34 52 std::cout << '\n'; } >> "std::ranges::views::values" : "std::pair" çifti tutan "range" içerisindeki "value" değerlerinden bir "view" oluşturmaya yarar. * Örnek 1, #include #include #include #include #include #include int main() { std::vector> my_vector{ { 28, "ulya yuruk" }, { 34, "Istanbul" }, { 52, "Ordu" } }; for (const auto& s : std::views::keys(my_vector)) std::cout << s << ' '; // 28 34 52 std::cout << '\n'; for (const auto& s : std::views::values(my_vector)) std::cout << s << ' '; // ulya yuruk Istanbul Ordu std::cout << '\n'; } * Örnek 2, #include #include #include #include #include #include int main() { std::vector> my_vector{ { 28, "ulya yuruk" }, { 34, "Istanbul" }, { 52, "Ordu" } }; for (const auto& s : std::views::keys(my_vector)) std::cout << s << ' '; // 28 34 52 std::cout << '\n'; for (const auto& s : std::views::values(my_vector)) std::cout << s << ' '; // ulya yuruk Istanbul Ordu std::cout << '\n'; for (const auto& [key, value] : std::views::zip(std::views::keys(my_vector), std::views::values(my_vector))) std::cout << key << " - " << value << ' '; // 28 - ulya yuruk 34 - Istanbul 52 - Ordu std::cout << '\n'; } Şimdi de C++23 ile dile eklenen, "auto" anahtar sözcüğünün yeni bir "overload" edilmiş halini inceleyelim; Anımsanacağı üzere fonksiyon şablonlarında fonksiyon çağrısı sırasında "std::initializer_list" kullanılması sentaks hatasıyken, "auto" kullanılarak değişken bildirimlerinde sentaks hatası meydana gelmemektedir. Hatta "auto" ile şablon parametresi "T" nin farklı olduğu tek nokta da burasıydı. * Örnek 1.0, #include #include template void func(T x) { std::cout << x.size() << ':'; for(auto i: x) std::cout << i << ' '; std::cout << '\n'; } int main() { func({ 3, 5, 7, 9 }); // ERROR } * Örnek 1.1, #include #include template void func(T x) { std::cout << x.size() << ':'; for(auto i: x) std::cout << i << ' '; std::cout << '\n'; } int main() { func(std::initializer_list{ 3, 5, 7, 9 }); // 4:3 5 7 9 } Artık C++23 ile birlikte "auto" anahtar sözcüğünü de "func" fonksiyonu çağrısında kullanabiliriz. Fakat burada unutmamamız gereken şey, "auto()" ile oluşturulan ifadenin "R-Value" OLDUĞUDUR. * Örnek 1, #include #include template void func(T x) { std::cout << x.size() << ':'; for(auto i: x) std::cout << i << ' '; std::cout << '\n'; } int main() { func(auto({ 3, 5, 7, 9 })); // 4:3 5 7 9 } * Örnek 2, #include int main() { int y = 10; int& z = auto(y); // cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int' } > Hatırlatıcı Notlar: >> Bir "view" oluşturabilmek için ilgili sınıfın "Default Ctor.", "Destructor", "Move Ctor." ve varsa "Copy Ctor." fonksiyonlarının "Constant Time" karmaşıklığında olması gerekmektedir. Fakat bu karmaşıklık garantisini derleyicinin koda bakarak anlaması mümkün olmadığından, sınıfı yazanın deklare etmesi gerekmektedir. template concept view = ranges::range && std::movable && ranges::enable_view; Görüldüğü üzere "std::ranges::enable_view" in "T" açılımının "true" olması gerekmektedir. * Örnek 1, #include #include int main() { static_assert(std::ranges::enable_view>); // error: static assertion failed std::vector ivec; auto vw = ivec | std::ranges::views::take(5); static_assert(std::ranges::enable_view); // OK } >> "std::ranges::views" kavramı için detaylı kaynak olarak "https://hackingcpp.com/" internet sitesini de kullanabiliriz. /*================================================================================================================================*/ (24_23_09_2023) > "ranges" kütüphanesi (devam): Aşağıdaki örneği inceleyelim: * Örnek 1.0, "view" nesnelerinin arka planda "cache" mekanizmasının kullanmasının getirdikleri: #include #include #include int main() { /* # OUTPUT # # First Tour # The Value: 2 The Value: 7 The Value: 9 The Value: 5 The Value: 4 The Value: 10 The Value: 6 The Value: 7 # Second Tour # The Value: 4 The Value: 10 The Value: 6 The Value: 7 */ std::vector ivec{ 2, 7, 9, 5, 4, 10, 6, 7 }; auto v = ivec | std::ranges::views::filter( [](int x){ std::cout << "The Value: " << x << '\n'; return x % 5 == 0; } ); std::cout << "# First Tour #\n"; for (auto i: v) { //... } std::cout << '\n'; std::cout << "# Second Tour #\n"; for (auto i: v) { //... } std::cout << '\n'; /* * Görüleceği üzere "Second Tour" a üç indisli öğeden başladı. Bu da demektir ki bir şekilde * ilgili indisli öğe "cache" edilmiş. Böylelikle sıfırıncı, birinci ve ikinci indisli * öğeler BOŞ YERE TEKRARDAN GEZİLMEMİŞTİR. Bu durum performans için çok önemli bir şey * olsa bile arka plandaki mekanizmaya hakim olmadığımız zaman başımıza problem olabilir. * Peki hangi senaryolarda başımıza dert olabilir? * I. İlk defa dolaştıktan sonra iş bu "view" a ilişkin "range" modifiye edersek, ikinci * dolaşmamızda "cache" edilmiş lokasyondan başlayacağı için, "Tanımsız Davranış" gibi sorunlar * gibi problemler ile karşılaşabiliriz. Yani beklemediğimiz sonuçlar oluşabilir. */ } * Örnek 1.1, iş bu "cache" mekanizmasının doğurduğu lojik hataya ilişkin örnek. #include #include #include void print_range(std::ranges::input_range auto&& rng) { for (const auto& i: rng) std::cout << i << " "; std::cout << '\n'; } int main() { std::list ilist{ 2, 3, 5, 7, 11, 13 }; auto v = ilist | std::views::drop(3); // The range: 7, 11, 13 print_range(v); // OUTPUT: 7 11 13 ilist.push_front(-1); // The list: -1, 2, 3, 5, 7, 11, 13 print_range(v); // OUTPUT: 7 11 13 /* * İlk "print_range" çıktısı ile biz aslında "7, 11, 13" rakamlarını görmekteyiz ki aslında bu * beklenen bir şeydir. Fakat "ilist.push_front(-1);" çağrısı ile ilgili "range" üzerinde bir * modifikasyonda bulunduk. Fakat ilk "print_range" çağrısı ile "7" rakamının bulunduğu indis * "cache" edildiğinden, ikinci tura o indisten başladığını gördük. Her ne kadar burada bir * "Tanımsız Davranış" olmasa bile bir mantık hatası, yani lojik hata, vardır. */ } * Örnek 1.2, iş bu "cache" mekanizmasının doğurduğu lojik hataya ilişkin bir diğer örnek. #include #include #include void print_range(std::ranges::input_range auto&& rng) { for (const auto& i: rng) std::cout << i << " "; std::cout << '\n'; } int main() { /* # OUTPUT # */ std::vector ivec{ 2, 3, 5, 1, 2, 8, 7 }; // The Vector: 2, 3, 5, 1, 2, 8, 7 auto IsBigger = [](int i){ return i > 3; }; auto v = ivec | std::ranges::views::filter(IsBigger); // The Range: 5, 8, 7 print_range(v); // OUTPUT: 5 8 7 ++ivec[1]; ivec[2] = 0; // The Vector: 2, 4, 0, 1, 2, 8, 7 print_range(v); // OUTPUT: 0 8 7 /* * Yine çıktılardan da görüleceği üzere, ikinci turda elde ettiğimiz değerler üçten büyük * değerler değil. Bunun sebebi yine ilk turda "5" rakamının bulunduğu indisin "cache" * edilmesi ve ikinci tura bu indisten başlanmasıdır. */ } * Örnek 1.3, iş bu "cache" mekanizmasının doğurduğu sentaks hatasına ilişkin bir örnek. #include #include #include #include void print(const auto& rg) { for (const auto& elem: rg) std::cout << elem << ' '; std::cout << '\n'; } int main() { std::vector ivec{ 1, 2, 3, 4, 5, 6, 7, 8, 9 }; std::list ilist{ 1, 2, 3, 4, 5, 6, 7, 8, 9 }; print(ivec | std::ranges::views::take(3)); // I: 1 2 3 print(ivec | std::ranges::views::drop(3)); // II: 4 5 6 7 8 9 print(ilist | std::ranges::views::take(3)); // III: 1 2 3 //print(ilist | std::ranges::views::drop(3)); // IV: Error // V: 4 5 6 7 8 9 for (const auto& elem: ilist | std::ranges::views::drop(3)) std::cout << elem << ' '; std::cout << '\n'; auto IsEven = [](const auto& val){ return val%2==0; }; //print(ivec | std::ranges::views::filter(IsEven)); // VI: Error /* * Errors: Buradaki sentaks hatalarının muhtemel nedeni, "print" fonksiyonundaki "rg" * parametresinin "const" olmasıdır. Dolayısıyla "print" fonksiyonu içerisinde ilgili * "range", "const" olarak ele alınmaktadır. Yani "const" bir nesne için arka plandaki * "cache" mekanizmasında kullanılan "non-const" bir fonksiyonun çağrılması olayıdır. */ } * Örnek 1.4.0, iş bu "cache" mekanizmasının doğurduğu tanımsız davranışa ilişkin bir örnek #include #include #include auto get_elems() { std::vector ivec{ 3, 6, 7, 9, 2, 1, 5, 8 }; //... return ivec | std::ranges::views::take(4); } int main() { auto v = get_elems(); for (auto i : v) std::cout << i << ' '; // 4 21957 32 0 std::cout << '\n'; /* * Çıktıdan da görüleceği üzere, "ivec" nesnesinin ömrü bitecektir. * İlgili "v" isimli "range" de aslında "for" döngüsündeki çağrı ile * oluşturulacağı için, ömrü biten bir nesne kullanılarak "range" * oluşturulmuş olacaktır. Otomatik ömürlü bir nesneyi adres veya * referans yolu ile döndürmekten bir farkı yoktur bu durumun. */ } * Örnek 1.4.1, Aşağıdaki örnekte taşıma semantiği devreye girdiğinden, tanımsız davranış meydana gelmeyecektir. #include #include #include auto get_elems() { return std::vector{3, 6, 7, 9, 2, 1, 5, 8} | std::ranges::views::take(4); } int main() { auto v = get_elems(); for (auto i : v) std::cout << i << ' '; // 3 6 7 9 std::cout << '\n'; } * Örnek 1.4.2, Aşağıdaki örnekte taşıma semantiği devreye girdiğinden, tanımsız davranış meydana gelmeyecektir. #include #include #include auto get_elems_I() { return std::vector{3, 6, 7, 9, 2, 1, 5, 8} | std::ranges::views::take(4); } auto get_elems_II() { std::vector ivec{3, 6, 7, 9, 2, 1, 5, 8}; return std::move(ivec) | std::ranges::views::take(4); } int main() { auto v = get_elems_I(); for (auto i : v) std::cout << i << ' '; // 3 6 7 9 std::cout << '\n'; auto vv = get_elems_II(); for (auto i : vv) std::cout << i << ' '; // 3 6 7 9 std::cout << '\n'; } Buradaki örnekleri özetlersek; "Care must be taken when modifying ranges used by views". > "std::span" : "std::span", adet "std::string_view" ın genelleştirilmiş halidir. Nasılki "std::string_view" nesneleri bir yazının gözlemcisiyse, "std::span" da "contiguous" olan herhangi bir "range" in öğelerinin gözlemcisidir. Yine bunlar da tıpkı "std::string_view" gibi "light-weight", yani kopyalama maliyeti olmayan nesnelerdir. C++20 ile dile eklenen önemli araçlardan bir tanesidir. Başlık dosyası "span" biçimindedir. Diğer yandan "std::span" ve "std::string_view" sınıfları, "view" konseptini sağlayan sınıflardır. Yine "std::string_view" gibi, "std::span" da "non-owning" dir. Yani gözlemlediği nesnenin hayatı biterse, bünyelerindeki gösterici(ler) "dangling" hale gelecektir. Bu sınıf her ne kadar bir "container" olmasa da "container-like" arayüze sahiptirler. Ek olarak "std::span", "extent" kavramına sahiptir. Bu kavramın değeri "static_extent" veya "dynamic_extent" değerlerinden birisi olabilir. "std::span" sınıfının ikinci şablon parametresi olan "size" değerine bir rakam geçmezsek, yani varsayılan "size" değerini kullanırsak, iş bu "extent" kavramının değeri "dynamic_extent" olacaktır. Bir rakam geçmemiz durumunda "static_extent" olacaktır. Burada kullanılan varsayılan "size" değeri ise "std::size_t" türünden "-1" dir. Yani "std::size_t" türünün en büyük değeri. Bu Şimdi de aşağıdaki örnekler ile sınıfı daha iyi tanıyalım: * Örnek 1, #include #include #include #include #include #include #include int main() { auto print = [](std::string_view const name, std::size_t ex) { std::cout << name << ", "; if (std::dynamic_extent == ex) std::cout << "dynamic extent\n"; else std::cout << "static extent = " << ex << '\n'; }; int a[]{1, 2, 3, 4, 5}; std::span span1{a}; print("span1", span1.extent); // span1, static extent = 5 std::span span2{a}; print("span2", span2.extent); // span2, dynamic extent std::array ar{1, 2, 3, 4, 5}; std::span span3{ar}; print("span3", span3.extent); // span3, static extent = 5 std::vector v{1, 2, 3, 4, 5}; std::span span4{v}; print("span4", span4.extent); // span4, dynamic extent } * Örnek 2, #include #include #include int main() { // std::span sp1; // Error: "extent == 0 || extent == std::dynamic_extent" ise "Default Ctor." çağrılabilir. std::span sp2; if constexpr (sp2.extent != std::dynamic_extent) { std::cout << "sp2 is a static_extent\n"; // OUTPUT: sp2 is a static_extent std::cout << "sp2.extent: " << sp2.extent << '\n'; // OUTPUT: sp2.extent: 0 std::cout << "sp2.size : " << sp2.size() << '\n'; // OUTPUT: sp2.size : 0 } std::span sp3; if constexpr (sp3.extent == std::dynamic_extent) { std::cout << "sp3 is a dynamic_extent\n"; // OUTPUT: sp3 is a dynamic_extent std::cout << "sp3.extent: " << sp3.extent << '\n'; // OUTPUT: sp3.extent: 18446744073709551615 std::cout << "sp3.size : " << sp3.size() << '\n'; // OUTPUT: sp3.size : 0 } int a[]{ 1, 2, 3, 4, 5, 6 }; std::span sp4{ a }; if constexpr (sp4.extent == std::dynamic_extent) { std::cout << "sp4 is a dynamic_extent\n"; // OUTPUT: sp4 is a dynamic_extent std::cout << "sp4.extent: " << sp4.extent << '\n'; // OUTPUT: sp4.extent: 18446744073709551615 std::cout << "sp4.size : " << sp4.size() << '\n'; // OUTPUT: sp4.size : 6 } std::array ar{ 7, 8, 9 }; std::span sp5{ ar }; if constexpr (sp5.extent == std::dynamic_extent) { std::cout << "sp5 is a dynamic_extent\n"; // OUTPUT: sp5 is a dynamic_extent std::cout << "sp5.extent: " << sp5.extent << '\n'; // OUTPUT: sp5.extent: 18446744073709551615 std::cout << "sp5.size : " << sp5.size() << '\n'; // OUTPUT: sp5.size : 3 } // "CTAT" kullanıldığı için "size" değerine ilişkin şablon parametresi // için de tür çıkarımı yapıldı. std::span sp6{ ar }; if constexpr (sp6.extent != std::dynamic_extent) { std::cout << "sp6 is a dynamic_extent\n"; // OUTPUT: sp6 is a dynamic_extent std::cout << "sp6.extent: " << sp6.extent << '\n'; // OUTPUT: sp6.extent: 3 std::cout << "sp6.size : " << sp6.size() << '\n'; // OUTPUT: sp6.size : 3 } } * Örnek 3, #include #include #include int main() { std::vector ivec{ 1, 2, 3, 4, 5, 6, 7, 8, 9 }; std::span sp9{ ivec }; // OK // "std::span" için "size" değeri olarak // "9" rakamı geçtiğimizden, "static_extent" // oluyor. Bu durumda ilgili "span" nesnesi // "9" öğelik bir "range" in gözlemcisi // olacaktır. assert(sp9.size() == ivec.size()); // OK std::span sp4{ ivec.begin(), 4 }; // OK // Tanımsız Davranış oluşturacak durumlar: std::span sp10{ ivec }; // Hem "static_extent" olsun hem de dinamik olarak... std::span sp5{ ivec }; // ..."size" değeri değişebilecek bir öğe kullanmak. std::span sp3{ ivec.begin(), 8 }; // Verdiğimiz aralık değeri uygun değil. std::span sp8{ ivec.begin(), 3 }; // Verdiğimiz aralık değeri uygun değil. // Sentaks Hatası oluşturacak durumlar: No such Ctor. Func. // std::span sp7{ ivec, 7 }; // std::span sp6{ ivec.begin() }; } * Örnek 4, #include #include int main() { int c_arr[10]; std::span sp1{ c_arr }; // std::span sp3{ c_arr }; // Invalid: No such Ctor. Func. std::array arr{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; std::span sp2{ arr }; // std::span sp4{ arr }; // Invalid: No such Ctor. Func. std::span sp5{ arr.data(), 5 }; } * Örnek 5, #include #include int main() { int c_arr[10]; // "sp1" is a pointer to "int" contiguous values of "c_arr". std::span sp1{ c_arr }; // "sp2" is a const pointer to "int" contiguous values of "c_arr". // From now on, "sp2" can only observe "c_arr". // Just like "const-pointer to int". const std::span sp2{ c_arr }; // "sp3" is a pointer to "const-int" contiguous values of "c_arr". // From now on, "sp2" cannot change the observed contiguous values of "c_arr". // Just like "pointer to const-int". std::span sp3{ c_arr }; } * Örnek 6, #include #include int main() { std::vector ivec{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; std::span sp1{ ivec }; // Syntax Errors: // std::span sp2{ ivec }; // std::span sp3{ sp1 }; // std::span sp4{ sp1 }; /* * Her ne kadar "long long" türünden "int" türüne * veya tam tersi yönde örtülü dönüşüm mümkün olsa da, * "std::span" söz konusu olduğunda sentaks hatası * oluşmaktadır. */ } * Örnek 7, #include #include #include int main() { std::vector ivec{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; std::array arr{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; std::span vec_span{ ivec }; std::span arr_span{ arr }; // Burada "std::span" nesnelerinin türü birbiri ile // AYNIDIR. static_assert(std::same_as); // True } * Örnek 8, #include #include #include int main() { std::array arr1{ 1, 2, 3, 4, 5 }; std::array arr2{ 6, 7, 8, 9, 10 }; const std::span c_span{ arr1 }; // "std::span" itself is "const" c_span[0] = 42; // Valid. ++c_span.back(); // Valid c_span = arr2; // ERROR: "c_span" cannot observe any range other than "arr1". std::span span{ arr1 }; // The observed values are "const", not "std::span" itself. span[0] = 42; // ERROR: The observed values cannot be changed any more. span = arr2; // Valid. } * Örnek 9, #include #include #include template void print_span(std::span sp) { std::cout << "Size = " << sp.size() << '\n'; if constexpr (Sz == std::dynamic_extent) std::cout << "Dynamic Extent\n"; else std::cout << "Fixed/Static Extent\n"; for (const auto& i: sp) std::cout << i << ' '; std::cout << '\n'; } int main() { int a[]{ 1, 2, 3 }; // print_span(a); // ERROR: Cannot deduce! std::cout << "\n================================\n"; std::span s1{ a }; print_span(s1); /* # OUTPUT # Size = 3 Fixed/Static Extent 1 2 3 */ std::cout << "\n================================\n"; print_span(std::span{ a }); /* # OUTPUT # Size = 3 Fixed/Static Extent 1 2 3 */ std::vector vec{ 4, 5, 6 }; // print_span(vec); // ERROR: Cannot deduce! std::cout << "\n================================\n"; std::span s2{ vec }; print_span(s2); /* # OUTPUT # Size = 3 Dynamic Extent 4 5 6 */ std::cout << "\n================================\n"; print_span(std::span{ vec }); /* # OUTPUT # Size = 3 Dynamic Extent 4 5 6 */ std::cout << "\n================================\n"; std::span s3{ vec }; print_span(s3); /* # OUTPUT # Size = 3 Dynamic Extent 4 5 6 */ std::cout << "\n================================\n"; std::span s4{ vec }; print_span(s4); /* # OUTPUT # Size = 3 Fixed/Static Extent 4 5 6 */ } * Örnek 10.0, #include #include #include std::vector get_vec() { return { 1, 2, 3, 4, 5, 6, 7 }; } auto get_span() { std::vector ivec{ 1, 2, 3, 4, 5, 6, 7 }; return std::span(ivec); } int main() { std::span x{ get_vec().begin(), 5 }; // Tanımsız Davranış for (auto i: x) std::cout << i << ' '; // OUTPUT: 1652225899 5 1338709025 1000630154 5 std::cout << '\n'; auto sp = get_span(); // Tanımsız Davranış for (auto i: x) std::cout << i << ' '; // OUTPUT: 1652225899 5 1338709025 1000630154 5 /* * Tıpkı "std::string_view" da olduğu gibi * hayatı bitmiş nesnelerin gözlemcisi olursak, * kullandığımız göstericiler "dangling" hale * gelir. */ } * Örnek 10.1, #include #include #include void print_span(std::span sp) { for (auto i: sp) std::cout << i << ' '; std::cout << '\n'; } int main() { std::vector ivec{ 3, 6, 9, 2, 8 }; std::cout << "ivec.capacity() : " << ivec.capacity() << '\n'; // ivec.capacity() : 5 std::span sp{ ivec }; print_span(sp); // 3 6 9 2 8 for (auto i = 0; i < 5; ++i) ivec.push_back(i); std::cout << "ivec.capacity() : " << ivec.capacity() << '\n'; // ivec.capacity() : 10 print_span(sp); // 1508200307 5 -1362903946 -149923851 8 /* * Çıktıdan da görüleceği üzere "reallocation", * Tanımsız Davranışa neden olacaktır. */ } * Örnek 10.2, #include #include #include void print_span(std::span sp) { for (auto i: sp) std::cout << i << ' '; std::cout << '\n'; } int main() { std::vector ivec{ 3, 6, 9, 2, 8 }; std::cout << "ivec.capacity() : " << ivec.capacity() << '\n'; // ivec.capacity() : 5 std::span sp{ ivec }; print_span(sp); // 3 6 9 2 8 for (auto i = 0; i < 5; ++i) ivec.push_back(i); std::cout << "ivec.capacity() : " << ivec.capacity() << '\n'; // ivec.capacity() : 10 sp = ivec; print_span(sp); // 3 6 9 2 8 0 1 2 3 4 /* * Çıktıdan da görüleceği üzere "reallocation" sonrası * ilgili "span" nesnesine yeniden atama yaptığımızda, * Tanımsız Davranış ortadan kalkacaktır. */ } * Örnek 10.3, #include #include #include int main() { /* # OUTPUT # vec.size() = 9 vec.capacity() = 9 vec.size() = 12 vec.capacity() = 18 1697459321 */ std::vector vec{ 1, 2, 3, 4, 5, 6, 7, 8, 9 }; std::cout << "vec.size() = " << vec.size() << '\n'; std::cout << "vec.capacity() = " << vec.capacity() << '\n'; std::span sp{ vec }; vec.insert( vec.end(), { 10, 11, 12 } ); std::cout << "vec.size() = " << vec.size() << '\n'; std::cout << "vec.capacity() = " << vec.capacity() << '\n'; // Yine "reallocation" kaynaklı bir Tanımsız Davranış // söz konusu. std::cout << sp[0] << '\n'; } * Örnek 11, #include #include #include int main() { std::vector v{ 1, 2, 3, 4, 5 }; std::span sp{ v }; // sp.size(): 5 std::cout << "sp.size(): " << sp.size() << '\n'; // sp.size_bytes(): 20 std::cout << "sp.size_bytes(): " << sp.size_bytes() << '\n'; // sp.extent(): 18446744073709551615 std::cout << "sp.extent(): " << sp.extent << '\n'; // 1 // 100 std::cout << sp[0] << '\n'; sp[0] *= 100; std::cout << sp[0] << '\n'; // 100 // 101 std::cout << sp.front() << '\n'; ++sp.front(); std::cout << sp.front() << '\n'; // 5 // 500 std::cout << sp.back() << '\n'; sp.back() *= 100; std::cout << sp.back() << '\n'; // Is Empty (false) = 101 2 3 4 500 std::cout << std::boolalpha << "Is Empty (" << sp.empty() << ") = "; for (auto i{0}; i < sp.size(); ++i) std::cout << *(sp.data() + i) << ' '; std::cout << '\n'; // Görüleceği üzere "std::span" nesnesi, "non-owning" olması hasebiyle, // gözlemcisi olduğu "range" in son durumu hakkında bilgi sahibi değildir. v.clear(); // Is Empty (false) = 101 2 3 4 500 std::cout << std::boolalpha << "Is Empty (" << sp.empty() << ") = "; for (auto i{0}; i < sp.size(); ++i) std::cout << *(sp.data() + i) << ' '; std::cout << '\n'; // İlgili "std::span" nesnesine yeniden atama yaparak, onu güncelleyebiliriz. // Tıpkı "reallocation" sonrası oluşan Tanımsız Davranış senaryolarında // yaptığımız gibi. sp = v; // Is Empty (true) = std::cout << std::boolalpha << "Is Empty (" << sp.empty() << ") = "; for (auto i{0}; i < sp.size(); ++i) std::cout << *(sp.data() + i) << ' '; std::cout << '\n'; } * Örnek 12.0, #include #include #include #include int main() { std::vector v{ 1, 2, 3, 4, 5 }; std::span sp{ v }; // Holds: "std::span" da "view" konseptini // karşılamaktadır. static_assert(std::ranges::view); } * Örnek 12.1, #include #include #include #include int main() { std::span sp{ "Ulya Yuruk" }; // 'T' is "const char", "size" is "13"; static_extent static_assert(std::ranges::view); // Holds // Gözlemlenen dizinin son karakteri olan '\0' karakteri de işlemlere dahildir. for (auto c: sp | std::ranges::views::drop(5) | std::ranges::views::take(3)) std::cout.put(c); // Yur } * Örnek 13, #include #include #include #include template requires requires (T x) { std::cout << x; } void print_span(std::span sp) { for (size_t i{}; i < sp.size(); ++i) std::cout << sp[i] << ' '; std::cout << '\n'; } int main() { /* # OUTPUT # [10] => 0 1 2 3 4 5 6 7 8 9 [4] => 3 4 5 6 [3] => 7 8 9 */ int a[10]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; std::span sp1{ a }; // "T" is "int", "n" is "10"; "static_extent" std::cout << "[" << sp1.size() << "] => "; print_span(sp1); // Starting from the third index, the next four element will be used. auto sp2 = sp1.subspan(3, 4); std::cout << "[" << sp2.size() << "] => "; print_span(sp2); // Starting from the seventh index, until the end of the range. auto sp3 = sp1.subspan(7); std::cout << "[" << sp3.size() << "] => "; print_span(sp3); } * Örnek 14, #include #include #include int main() { int a[10]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; std::span sp{ a }; for (auto i: sp) std::cout << i << ' '; // 0 1 2 3 4 5 6 7 8 9 std::cout << '\n'; for (auto i: sp | std::ranges::views::reverse) std::cout << i << ' '; // 9 8 7 6 5 4 3 2 1 0 std::cout << '\n'; for (auto iter = sp.begin(); iter != sp.end(); ++iter) std::cout << *iter << ' '; // 0 1 2 3 4 5 6 7 8 9 std::cout << '\n'; for (auto riter = sp.rbegin(); riter != sp.rend(); ++riter) std::cout << *riter << ' '; // 9 8 7 6 5 4 3 2 1 0 std::cout << '\n'; } * Örnek 15, #include #include #include template void print_span(std::span sp) { for (size_t i{}; i < sp.size(); ++i) std::cout << sp[i] << ' '; std::cout << '\n'; } int main() { int a[10]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; std::span sp1{ a }; std::cout << "[" << sp1.size() << "] => "; // [10] => 0 1 2 3 4 5 6 7 8 9 print_span(sp1); auto sp2 = sp1.first<5>(); // "std::span" is "static_extent" std::cout << "[" << sp2.size() << "] => "; // [5] => 0 1 2 3 4 print_span(sp2); auto sp3 = sp1.first(5); // "std::span" is "dynamic_extent" std::cout << "[" << sp3.size() << "] => "; // [5] => 0 1 2 3 4 print_span(sp3); auto sp4 = sp1.last<5>(); // "std::span" is "static_extent" std::cout << "[" << sp4.size() << "] => "; // [5] => 5 6 7 8 9 print_span(sp4); auto sp5 = sp1.last(5); // "std::span" is "dynamic_extent" std::cout << "[" << sp5.size() << "] => "; // [5] => 5 6 7 8 9 print_span(sp5); } * Örnek 16, #include #include #include int main() { std::vector ivec{ 1, 2, 3, 4, 5, 5, 4, 3, 2, 1 }; auto sp = std::ranges::views::counted(ivec.begin(), 5); // "counted" returns a "std::span" static_assert( std::same_as< decltype(sp), std::span > ); // Holds True } > "2. Cpp Idioms/Patterns > Exception Tracker" : Hangi sınıfın "exception" gönderdiğinin takibini yapmak için kullanılan bir deyimdir. Buradaki kilit nokta "," operatörünün bir "seperator" olarak değil, bir operatör olarak kullanılmasıdır. Böylelikle ilgili operatörün sol tarafındaki kod işletildikten sonra "discard" edilecek, operatörün ürettiği değer de sağ tarafındaki değer olacaktır. * Örnek 1, #include #include struct X{ X(const char* name = nullptr) { //... // Uncomment the code below so that "struct X" will throw // an exception before "struct Y". //throw std::runtime_error{"An exception from X::X()"}; } }; struct Y{ Y(const char* name = nullptr) { //... // Uncomment the code below so that "struct Y" will throw // an exception before "struct Z". //throw std::runtime_error{"An exception from Y::Y()"}; } }; struct Z{ Z(const char* name = nullptr) { //... throw std::runtime_error{"An exception from Z::Z()"}; } }; class Neco{ private: X mx; Y my; Z mz; enum TrackerType{ NONE, ONE, // Will track struct X TWO, // Will track struct Y THREE // Will track struct Z }; public: Neco(TrackerType tracker = NONE) try : // İçerideki parantezler ile "," operatörü, ayıraç yerine, operatör işlevini görmektedir. mx((tracker = ONE, "Ulya")), my((tracker = TWO, "Yuruk")), mz((tracker = THREE, "Uskudar")) { std::cout << "Neco ctor. body\n"; } catch (const std::exception& ex){ std::size_t flag_X{}, flag_Y{}, flag_Z{}; if (tracker == ONE) flag_X = 1; if (tracker == TWO) flag_Y = 1; if (tracker == THREE) flag_Z = 1; if (flag_X) std::cout << "struct X threw an exception\n"; if (flag_Y) std::cout << "struct Y threw an exception\n"; if (flag_Z) std::cout << "struct Z threw an exception\n"; std::cout << "[" << ex.what() << "]\n"; throw; // Bizler elle "rethrow" etmiş olduk. } }; int main() { /* # OUTPUT # struct Z threw an exception [An exception from Z::Z()] An exception was caught: [An exception from Z::Z()] */ try { Neco n; } catch (const std::exception& ex){ std::cout << "An exception was caught: [" << ex.what() << "]\n"; } } > "3. Cpp Idioms/Patterns > Exception Dispatcher" : Yakalanan hata nesnesinin işleme mekanizması, ortak bir noktada toplanarak ele alınmıştır. * Örnek 1, #include class a_exception{ /*...*/ }; class b_exception{ /*...*/ }; class c_exception{ /*...*/ }; void handle_exception() { try { throw; // rethrow statement } catch (const a_exception&) { std::cout << "a_exception is handled.\n"; } catch (const b_exception&) { std::cout << "b_exception is handled.\n"; } catch (const c_exception&) { std::cout << "c_exception is handled.\n"; } } void a() { throw a_exception{}; } void b() { throw b_exception{}; } void c() { throw c_exception{}; } int main() { /* # OUTPUT # [0]: main started a_exception is handled. [1]: main is running b_exception is handled. [2]: main is running c_exception is handled. [3]: main is running */ std::cout << "[0]: main started\n"; try { a(); } catch(...){ handle_exception(); } std::cout << "[1]: main is running\n"; try { b(); } catch(...){ handle_exception(); } std::cout << "[2]: main is running\n"; try { c(); } catch(...){ handle_exception(); } std::cout << "[3]: main is running\n"; } > Hatırlatıcı Notlar: >> "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. >> "Function-Try-Block": Aşağıdaki örnekleri inceleyelim: * Örnek 1, Kurucu işlevler tarafından gönderilen hata nesnesinin yakalanmasına/işlenmesi: #include #include class Member{ public: Member() = default; Member(int x) { if (x > 5) { throw std::runtime_error{"Exception from Member Ctor.(int)\n"}; } } Member(float x) { if (x > 5.0f) { throw std::runtime_error{"Exception from Member Ctor.(float)\n"}; } } }; class Neco{ public: Neco(int x) : mx{x} { // Aşağıdaki "try-catch" bloğunu kullanarak, // "Member" sınıfının ilgili kurucu işlevinden // gönderilen hatayı YAKALAYAMAYIZ. Onun için // "Function-Try-Block" mekanizmasını kullanmalıyız. try { //... } catch(...){ std::cout << "An exception was caught!(int)\n"; } } Neco(float x) try: mx{x}{ //... } catch(...){ std::cout << "An exception was caught!(float)\n"; // Derleyici "implicit" olarak aşağıdaki kodu // yazmaktadır: /* throw; */ // (*) } private: Member mx; }; int main() { // "Member" sınıfının kurucu işlevinden gönderilen hata // nesnesi yakalanamadığı için sırasıyla önce "std::terminate", // sonra "std::abort" fonksiyonları çağrılmıştır. // "terminate called after throwing an instance of 'std::runtime_error'" // " what(): Exception from Member Ctor.(int)"" // Neco n(34); // "n" nesnesinin oluşturduğu problemi gidermek adına bizler // "Function-Try-Block" mekanizmasını kullandık. Fakat derleyici // "catch" deyiminin sonuna "implicit" olarak "throw;" deyimini // eklediği için, yakalamış olduğumuz hata nesnesi "rethrow" // edildi. İş bu "rethrow" edilen hata nesnesi yakalanamadığı için // önce "std::terminate", sonra "std::abort" fonksiyonları çağrılmıştır. // "An exception was caught!(float)" // "terminate called after throwing an instance of 'std::runtime_error'" // " what(): Exception from Member Ctor.(float)" // Neco nn(34.f); try { // Artık "mx" nesnesinin kurucu işlevinden gönderilen hata nesnesini // ilk olarak "nnn" nesnesinin kurucu işlevinde yakaladık. Fakat orada // "rethrow" edildi. İşte bunu da "main" fonksiyonu içerisinde yakaladık. // "An exception was caught!(float)" // "An exception was caught!(MAIN FUNC)" Neco nnn(454.f); } catch(...){ std::cout << "An exception was caught!(MAIN FUNC)\n"; } // (*): // Burada bizler yakaladığımız hataya ilişkin bir aksiyon almadığımız // için derleyici "implicit" olarak "rethrow" gerçekleştirdi. Bunun // yerine bizler yakaladığımız hatayı başka bir hata nesnesine dönüştürmek // suretiyle işlemek, kendimiz elle "std::terminate" çağrısı yapmak vb. // yöntemler de kullanabiliriz. } * Örnek 2, Düz fonksiyonlardaki kullanım biçimi: //... int foo(int x) try { // "try-block" together with the // "function-block" if (x > 10) throw x; return x; } catch(...) { // the "catch" block. std::cout << "An error has been caught!\n"; return -1; } int main() { foo(100); // An error has been caught! } /*================================================================================================================================*/ (25_24_09_2023) && (26_30_09_2023) && (27_01_10_2023) && (28_14_10_2023) && (29_15_10_2023) && (30_21_10_2023) && (31_22_10_2023) > "4. Cpp Idioms/Patterns > ADL Fallback" : * Örnek 1, #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); // [1] // Dolayısıyla bizlerin "std" isim alanı veya başka isim // alanı içerisindeki aynı isimli fonksiyonu çağırabilmemiz // için, ilgili fonksiyonu çağırırken, o isim alanını nitelemeliyiz. std::swap(x, y); // [2] // İşte fonksiyonu çağırırken hedef isim alanını niteleyerek çağırmaktansa, // "using-directive" kullanarak, hedef isim alanını kendimizinkine enjekte // ediyoruz. Eğer "T" türüne ilişkin isim alanında "swap" fonksiyonu // bulunamazsa, "std" isim alanındaki "swap" fonksiyonu çağrılacaktır. using std::swap; swap(x, y); // ADL, my_ns::swap(Myclass) was called. } int main() { 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. } * Örnek 2, #include #include template void func(T x) { T y; using std::swap; swap(x, y); } namespace nec { class Foo{}; class Bar{}; void swap(Foo&, Foo&) { std::cout << "nec::swap(Foo&, Foo&) was called.\n"; } } int main() { /* # OUTPUT # */ nec::Foo x; nec::Bar y; // "nec" isim alanı içerisindeki "swap" fonksiyonunun // parametresi "Foo" olduğundan, o çağrıldı. func(x); // nec::swap(Foo&, Foo&) was called. // "nec" isim alanı içerisindeki "swap" fonksiyonunun // parametresi "Foo" türünden olduğu için, "std" isim // alanı içerisindeki "swap" fonksiyonu çağrıldı. func(y); } > "5. Cpp Idioms/Patterns > Hidden Friend" : * Örnek 0, "ADL" ile bulunma özelliğine sahip, istersek de "ADL Fallback" den fayda sağladığımıza ilişkin bir örnek. #include #include class Myclass{ public: friend void foo(int) {}; friend void bar(Myclass) {}; }; int main() { // [0] // İlgili "foo" fonksiyonu bir // "member" fonksiyon DEĞİLDİR. // "Myclass" hangi isim alanındaysa, // "foo" da o isim alanındadır. Ancak // "foo", o isim alanı içerisinde // görülür DEĞİLDİR. Sadece "ADL" ile // o ismi bulabiliriz. // foo(12); // ERROR: ‘foo’ was not declared in this scope // [1] Myclass mx; bar(mx); // OK } * Örnek 1, class Myclass{ public: friend void bar(Myclass, Myclass) {}; }; int main() { Myclass mx, my; bar(mx, my); // OK } * Örnek 2.0, struct A{}; struct B{}; struct C{}; struct D{}; struct E{}; struct F{}; A operator+(const A&, const A&); B operator+(const B&, const B&); C operator+(const C&, const C&); D operator+(const D&, const D&); E operator+(const E&, const E&); F operator+(const F&, const F&); class Nec{}; int main() { /* # OUTPUT # main.cpp: In function ‘int main()’: main.cpp:34:17: error: no match for ‘operator+’ (operand types are ‘Nec’ and ‘Nec’) 34 | auto n3 = n1+n2; | ~~^~~ | | | | | Nec | Nec main.cpp:11:3: note: candidate: ‘A operator+(const A&, const A&)’ 11 | A operator+(const A&, const A&); | ^~~~~~~~ main.cpp:11:13: note: no known conversion for argument 1 from ‘Nec’ to ‘const A&’ 11 | A operator+(const A&, const A&); | ^~~~~~~~ main.cpp:12:3: note: candidate: ‘B operator+(const B&, const B&)’ 12 | B operator+(const B&, const B&); | ^~~~~~~~ main.cpp:12:13: note: no known conversion for argument 1 from ‘Nec’ to ‘const B&’ 12 | B operator+(const B&, const B&); | ^~~~~~~~ main.cpp:13:3: note: candidate: ‘C operator+(const C&, const C&)’ 13 | C operator+(const C&, const C&); | ^~~~~~~~ main.cpp:13:13: note: no known conversion for argument 1 from ‘Nec’ to ‘const C&’ 13 | C operator+(const C&, const C&); | ^~~~~~~~ main.cpp:14:3: note: candidate: ‘D operator+(const D&, const D&)’ 14 | D operator+(const D&, const D&); | ^~~~~~~~ main.cpp:14:13: note: no known conversion for argument 1 from ‘Nec’ to ‘const D&’ 14 | D operator+(const D&, const D&); | ^~~~~~~~ main.cpp:15:3: note: candidate: ‘E operator+(const E&, const E&)’ 15 | E operator+(const E&, const E&); | ^~~~~~~~ main.cpp:15:13: note: no known conversion for argument 1 from ‘Nec’ to ‘const E&’ 15 | E operator+(const E&, const E&); | ^~~~~~~~ main.cpp:16:3: note: candidate: ‘F operator+(const F&, const F&)’ 16 | F operator+(const F&, const F&); | ^~~~~~~~ main.cpp:16:13: note: no known conversion for argument 1 from ‘Nec’ to ‘const F&’ 16 | F operator+(const F&, const F&); | ^~~~~~~~ */ Nec n1, n2; /* * Şimdi burada derleyici hem "Nec" sınıfı * içerisinde hem de "global" isim alanı * içerisinde "operator+" fonksiyonu için * uygun bir "overload" arayacaktır. Bundan * dolayıdır ki derleyici yukarıdaki A...F * sınıflarının hepsinde uygun "overload" * olup olmadığına bakacaktır. Bu da beraberinde * hem karmaşık hata mesajlarını getirecek hem de * derleme zamanının uzun sürmesine neden olacaktır. * Tabii diğer yandan uygun bir "Conversion Opt." * fonksiyonunun olup olmadığına da bakacaktır. */ auto n3 = n1+n2; } * Örnek 2.1, Derleme zamanını kısaltması ve hata mesajlarının daha net olmasına ilişkin bir örnek. #include #include // Artık aşağıdaki "operator+" fonksiyonları // sadece "ADL" ile bulunabilirler. struct A{ friend A operator+(const A&, const A&); //... }; struct B{ friend B operator+(const B&, const B&); //... }; struct C{ friend C operator+(const C&, const C&); //... }; struct D{ friend D operator+(const D&, const D&); //... }; struct E{ friend E operator+(const E&, const E&); //... }; struct F{ friend F operator+(const F&, const F&); //... }; class Nec{}; int main() { /* # OUTPUT # main.cpp: In function ‘int main()’: main.cpp:47:17: error: no match for ‘operator+’ (operand types are ‘Nec’ and ‘Nec’) 47 | auto n3 = n1+n2; | ~~^~~ | | | | | Nec | Nec */ Nec n1, n2; /* * İşte yukarıdaki "operator+" fonksiyonları sayesinde * üretilen hata mesajının daha açık, derleme zamanının * daha kısa olmasına neden oldu. */ auto n3 = n1+n2; } * Örnek 3, Örtülü dönüşümleri engellemesine ilişkin bir örnek. #include #include namespace nec{ struct A{ friend void bar(A) { std::cout << "bar(A) was called.\n"; } }; struct B{ operator A() { std::cout << "Cast from B to A\n"; return A{}; } // "A" türüne dönüştürme opt. fonksiyonu. }; void foo(A) { std::cout << "foo(A) was called.\n"; } } int main() { nec::B b; // Burada "B" sınıfı "operator A" fonksiyonuna sahip olduğundan, // "A" sınıfına dönüştürülüyor. Sonrasında da "foo" fonksiyonuna // çağrı yapılıyor. Yani aslında ilgili operatör fonksiyonunun // varlığı, örtülü dönüşüme neden oluyor. foo(b); // Ancak "A" sınıfındaki "hidden-friend" fonksiyonumuz, // iş bu örtülü dönüşümün meydana gelmesini engellemekte. bar(b); } > "6. Cpp Idioms/Patterns > Scope Guard" : Anımsanacağı üzere otomatik ömürlü nesneler, "scope" larının sonunda "destroy" ediliyorlar. Eğer böylesi bir nesne "exception" gönderirse, ancak bu hata nesnesi yakalanırsa çalıştırılacak "Stack Unwinding" süreci içinde, o nesnelerin "Dtor."fonksiyonları çağrılıyor. Yani otomatik ömürlü nesnelerin "Dtor." fonksiyon çağrıları için ya "scope" sonuna gelmesi ya da "Stack Unwinding" mekanizmasının çalıştırılması gerekmektedir. Bu mekanizmanın ilk akla gelen kullanımı, akıllı göstericilerdir. İşte akıllı göstericilerin daha genelleştirilmiş haline de "Scope Guard" denmektedir. Birden fazla uygulanış biçimi vardır. Şöyleki; * Örnek 1.0, "scope" sonuna gelinmesinden dolayı "Dtor". çağrıldı, o da "clean_up" fonksiyonunu. #include template class scope_guard { public: scope_guard(Func f) noexcept : m_f{f} { } ~scope_guard() { if (m_call) m_f(); } scope_guard(const scope_guard&) = delete; scope_guard& operator=(const scope_guard&) = delete; //... private: Func m_f; bool m_call{true}; }; void clean_up() { std::cout << "clean_up called\n"; } int main() { /* # OUTPUT # main basladi clean_up called main devam ediyor */ std::cout << "main basladi\n"; if (1) { scope_guard sg{ clean_up }; } std::cout << "main devam ediyor\n"; } * Örnek 1.1, "Stack Unwinding" den dolayı "Dtor". çağrıldı, o da "clean_up" fonksiyonunu. #include #include template class scope_guard { public: scope_guard(Func f) noexcept : m_f{f} { } ~scope_guard() { if (m_call) m_f(); } scope_guard(const scope_guard&) = delete; scope_guard& operator=(const scope_guard&) = delete; //... private: Func m_f; bool m_call{true}; }; void bar() { throw std::runtime_error{ "An exception from bar" }; } void clean_up() { std::cout << "clean_up called\n"; } void foo() { if (1) { scope_guard sg{ clean_up }; bar(); } } int main() { /* # OUTPUT # main basladi clean_up called Exception Caught: [An exception from bar] main devam ediyor */ std::cout << "main basladi\n"; try { foo(); } catch(const std::exception& ex) { std::cout << "Exception Caught: [" << ex.what() << "]\n"; } std::cout << "main devam ediyor\n"; } * Örnek 1.2, Tabii böylesi sınıflara "dismiss" amacı taşıyan fonksiyonlar da ekleyerek, "clean_up" fonksiyonlarının "Dtor." tarafından çağrılmasını da iptal edebiliriz. #include #include template class scope_guard { public: scope_guard(Func f) noexcept : m_f{f} { } ~scope_guard() { if (m_call) m_f(); } void dismiss() { m_call = false; } scope_guard(const scope_guard&) = delete; scope_guard& operator=(const scope_guard&) = delete; //... private: Func m_f; bool m_call{true}; }; void bar() { throw std::runtime_error{ "An exception from bar" }; } void clean_up() { std::cout << "clean_up called\n"; } void foo() { if (1) { scope_guard sg{ clean_up }; //... sg.dismiss(); bar(); } } int main() { /* # OUTPUT # main basladi clean_up called Exception Caught: [An exception from bar] main devam ediyor */ std::cout << "main basladi\n"; try { foo(); } catch(const std::exception& ex) { std::cout << "Exception Caught: [" << ex.what() << "]\n"; } std::cout << "main devam ediyor\n"; } > "7. Cpp Idioms/Patterns > Return Type Resolver" : * Örnek 1, #include #include #include "MyUtility.h" class MyStringClass { public: MyStringClass(const char* p) : m{ p } {} operator int() const { return std::stoi(m); } operator double() const { return std::stod(m); } operator long long() const { return std::stoll(m); } private: std::string m; }; int main() { /* # OUTPUT # 123 123.456 123 */ MyStringClass s{"123.456"}; int i = s; std::cout << i << '\n'; double d = s; std::cout << d << '\n'; long long ll = s; std::cout << ll << '\n'; } * Örnek 2, #include #include #include #include #include #include "MyUtility.h" class MyRangeClass { public: MyRangeClass(int from, int to) : m_from{ from }, m_to{ to } { if (to < from) throw std::runtime_error{ "Invalid range values!" }; } template operator C() const { C the_container; for (auto i{ m_from }; i < m_to; ++i) the_container.insert(the_container.end(), i); return the_container; } private: const int m_from; const int m_to; }; int main() { /* # OUTPUT # [10] => 0 1 2 3 4 5 6 7 8 9 [9] => 11 12 13 14 15 16 17 18 19 [9] => 21 22 23 24 25 26 27 28 29 */ std::vector ivec = MyRangeClass(0, 10); std::cout << "[" << ivec.size() << "] => "; for (auto i : ivec) std::cout << i << ' '; std::cout << '\n'; std::list ilist = MyRangeClass(11, 20); std::cout << "[" << ilist.size() << "] => "; for (auto i : ilist) std::cout << i << ' '; std::cout << '\n'; std::set iset = MyRangeClass(21, 30); std::cout << "[" << iset.size() << "] => "; for (auto i : iset) std::cout << i << ' '; std::cout << '\n'; } > "8. Cpp Idioms/Patterns > NVI(non-virtual interface)" : Taban sınıfın sanal fonksiyonlarını sınıfın "public" bölümüne değil, "protected" ya da "private" bölümüne koyun. Taban sınıfın sanal olmayan fonksiyonlarını da "public" bölümüne koyun ve "protected" ya da "private" bölümüne koyduğumuz sanal fonksiyonları çağırsın. Bu şekilde bizler "interface" ile "implementation" kısmını birbirinden ayırmış olduk. Aksi halde "public" kısımdaki "virtual" fonksiyonlarımız hem "interface" hem de "implementation" olacaktı. Fakat esas önemli avantajı ise taban sınıfın, sanal fonksiyon çağrısından evvel, bir takım kontrolleri yapabilme imkanına sahip olmasıdır. Aksi halde direkt olarak türemiş sınıfa ilişkin "overload" çağrılacağından, taban sınıf tarafından ön kontrol yapabilme imkanı oluşmayacaktır. * Örnek 1, #include #include "MyUtility.h" class Base { public: void foo() { // Interface /* * İşte bu noktada bizler bir takım ön işlemleri yapabilme imkanına * sahibiz. Bu işlemlerden sonra türemiş sınıfa ilişkin "overload" * çağrılacaktır. */ foo_imp(); } private: virtual void foo_imp() { // Implementation } }; int main() { //... } * Örnek 2.0, Aşağıdaki örneği ele alırsak, taban içerisinde "speak" fonksiyonuna ilişkin ön işlemler yapacak DURUMDA DEĞİLİZ. Dolayısıyla ön işlemleri türemiş sınıf içerisinde yapmak durumunudayız ki bu da aslında bir kod tekrarına neden olmaktadır. #include #include "MyUtility.h" class Animal { public: virtual void speak()const = 0; }; class Cat : public Animal { public: virtual void speak() const override { std::cout << "Cat spoke\n"; } }; class Dog : public Animal { public: virtual void speak() const override { std::cout << "Dog spoke\n"; } }; int main() { /* # OUTPUT # Cat spoke Dog spoke */ auto* Animal_Cat = new Cat; auto* Animal_Dog = new Dog; Animal_Cat->speak(); Animal_Dog->speak(); delete Animal_Cat; delete Animal_Dog; } * Örnek 2.1, Artık bir takım ön işlemleri yapabilecek durumdayız. #include #include "MyUtility.h" class Animal { public: void speak() { std::cout << "Bir takim on islemler...\n"; speak_imp(); } private: virtual void speak_imp()const = 0; }; class Cat : public Animal { public: virtual void speak_imp() const override { std::cout << "Cat spoke\n"; } }; class Dog : public Animal { public: virtual void speak_imp() const override { std::cout << "Dog spoke\n"; } }; int main() { /* # OUTPUT # Bir takim on islemler... Cat spoke Bir takim on islemler... Dog spoke */ auto* Animal_Cat = new Cat; auto* Animal_Dog = new Dog; Animal_Cat->speak(); Animal_Dog->speak(); delete Animal_Cat; delete Animal_Dog; } > "Fragile Base Class Problem" : Taban sınıfta yapmış olduğumuz bazı değişikliklerin, türemiş sınıflardaki kodları hata durumuna düşürmesidir. * Örnek 1.0, "CountingSet" sınıfındaki "m_count" öğesinin değeri, ilgili "range" içerisindeki öğe sayısı kadar, olacaktır. #include #include #include "MyUtility.h" class MySet { public: void add(int i) { ms.insert(i); add_impl(i); // Note that it is a virtual call. } void add_range(const int* begin, const int* end) { ms.insert(begin, end); // --------(1) add_range_impl(begin, end); // Note that it is a virtual call. } private: virtual void add_impl(int i) = 0; virtual void add_range_impl(const int* begin, const int* end) = 0; private: std::set ms; }; class CountingSet : public MySet { public: int get_count() const { return m_count; } private: int m_count; virtual void add_impl(int i) override { m_count++; } virtual void add_range_impl(const int* begin, const int* end) override { m_count += std::distance(begin, end); } }; int main() { CountingSet cs; int a[]{ 3, 5, 6, 7, 8 }; cs.add_range(a, a + 5); std::cout << cs.get_count() << '\n'; // 5 } * Örnek 1.1, "CountingSet" sınıfındaki "m_count" öğesinin değeri, ilgili "range" içerisindeki öğe sayısının iki katı kadar, olacaktır. Çünkü hem "add_range" içerisindeki "add_range_impl" hem de "add" içerisindeki "add_impl" çağrılarından dolayı iki defa arttırım olmuştur. #include #include #include "MyUtility.h" class MySet { public: void add(int i) { ms.insert(i); add_impl(i); // Note that it is a virtual call. } void add_range(const int* begin, const int* end) { auto b = begin; auto e = end; while (b != e) add(*b++); // --------(1) add_range_impl(begin, end); // Note that it is a virtual call. } private: virtual void add_impl(int i) = 0; virtual void add_range_impl(const int* begin, const int* end) = 0; private: std::set ms; }; class CountingSet : public MySet { public: int get_count() const { return m_count; } private: int m_count; virtual void add_impl(int i) override { m_count++; } virtual void add_range_impl(const int* begin, const int* end) override { m_count += std::distance(begin, end); } }; int main() { CountingSet cs; int a[]{ 3, 5, 6, 7, 8 }; cs.add_range(a, a + 5); std::cout << cs.get_count() << '\n'; // 10 } > C++ dilindeki şablonlar ve genel hatırlatmalar: C++ dilindeki şablonlar şunlardır; "Function Template", "Class Template", "Variable Template", "Alias Template" ve "concept" Ve bunları açtığımız zaman, yani bunların "specialization" halleri ise sırasıyla, "Function", "Class", "Variable", "Tür Eş İsmi" ve "Concept" oluşur. Hatırlarsanız "concept" kelimesi hem şablonun kendisi hem de o şablon açıldığında ortaya çıkan ürüne denmektedir. Diğer yandan şablonların parametreleri de şunlar olabilir; "Type Parameter", "Non-Type Parameter" ve "Template Parameter". Şimdi de şablonlarla alakalı şu alt başlıkları irdeleyelim: >> Şablonlardan bahsederken şu konular da akla gelmektedir; Deduction Specialization Overloading CTAD Full(Explicit) Specialization Partial Overloading Rules Deduction Guides Partial Specialization Friend Declarations Meta Functions (type_traits) SFINAE Tag Dispatch Default Template Arguments Abbreviated Template Syntax Constrained Templates CRTP Member Templates Expression Templates declval type_identity Perfect Forwarding static if static assert Fold Expressions >> Şablonlara ilişkin terminoloji ise şu şekildedir; =================================================== template< typename T > class Heap; ---------- ---- | a: | b: a: "Template-Parameter-List" b: "Template-Name" =================================================== template< typename T > class Heap; template< typename T > class Heap; -------- | a: template<> // "<>" indicates that it is an excplicit specialization, or diamon specialization. class Heap ; ---- ----- | b: | c: a: "Template-ID" b: "Template-Name" c: "Template-Argument-List" =================================================== Heap< int > aHeap; --- | a: Heap aHeap2; ----------- | b: a: "Template-Argument-List" b: "Template-ID" =================================================== template< typename T > void print (const T& x) {} ---------- ----- | a: | b: a: "Template-Parameter-List" b: "Template-Name" =================================================== Bir şablonun belli bir türe göre açıldığında ortaya çıkan ürüne ise "Specialization" denmektedir. Her ne kadar "Specialization" kelimesi "Full(Explicit) Specialization" / "Partial Specialization" kavramlarını akla getirsede, buradaki kullanım daha geneldir. >> Şablonlarda, "T" için yapılacak tür çıkarımı ise şu kurallara bağlıdır; template void foo(T p); /* * > Fonksiyon isimleri ve dizi isimleri söz konusu olduğunda "decay" gerçekleşir. * > "const" özelliği düşer. * > "reference" özelliği düşer. * > Çıkarım sonucunda "T" ile "p" nin türleri aynıdır. */ template void foo(T& p); /* * > Fonksiyon isimleri ve dizi isimleri söz konusu olduğunda "decay" GERÇEKLEŞMEZ. * > "const" özelliği DÜŞMEZ. * > "reference" özelliği DÜŞMEZ. * > Çıkarım sonucunda "T" ile "p" nin türleri FARKLI OLABİLİR. */ template void foo(T&&); * Örnek 1, #include #include #include /* * "func" ın üçüncü parametresi aslında bir "Template Parameter". İki tane * "type" parametresine sahip bir "template" i argüman olarak geçmeliyiz. */ templatetypename Con> void func(const Con& con) { std::cout << "T is " << typeid(T).name() << '\n'; std::cout << "A is " << typeid(A).name() << '\n'; std::cout << "Con is " << typeid(Con).name() << '\n'; std::cout << "con is " << typeid(con).name() << '\n'; } int main() { /* # OUTPUT # T is int A is class std::allocator Con is class std::vector con is class std::vector > ======================== T is double A is class std::allocator Con is class std::list con is class std::list > */ std::vector ivec; std::list ilist; func(ivec); std::cout << "\n========================\n\n"; func(ilist); } * Örnek 2, #include #include #include template void func(T) { std::cout << "void func \n<" << typeid(T).name() << "> (\n" << typeid(T).name() << "\n)"; } int main() { /* # OUTPUT # void func ( void (__cdecl*)(double) ) */ void (*func_ptr)(double) = &func; func(func_ptr); } * Örnek 3, #include #include template void foo( std::array arr1, std::array arr2 ) { std::cout << "arr1<" << typeid(T).name() << ',' << sizeof(U) << ">\n"; std::cout << "arr2<" << typeid(U).name() << ',' << sizeof(T) << ">\n"; } int main() { /* # OUTPUT # arr1 arr2 */ std::array a{}; std::array b{}; foo(a, b); std::cout << "\n==========================\n\n"; std::array c; // 'void foo(std::array,std::array)' // : cannot convert argument 2 from 'std::array' to 'std::array' // "a" için yapılan tür çıkarımı sonucunda "T" için "int", "U" için "double" // geleceğinden "int<8>" türünde bir dizi olacak. // Fakat "c" için yapıldığında "U" için "double", "T" için "int" geleceğinden // "double<4>" olacak ama argüman olan dizimiz "" olduğundan sentaks // hatası alacağız. // foo(a, c); } * Örnek 4, template void f1(T*); template void f2(E(&)[N]); template void f3(T1(T2::*)(T3*)); class s { public: void f(double*); }; void g(int*** ppp) { bool b[42]; f2(b); // "E" is "bool" // "N" is "42" f1(ppp); // "T" is "int**" f3(&s::f); // "T1" is "void" // "T2" is "s" // "T3" is "double" } int main() { } * Örnek 5, template void func(T(*)(U)); int foo(double); int main() { func(foo); // "T" is "int" // "U" is "double" // func([](int i) { return i + 5; }); // Error: No Implicit Convertion // Positive Lambda Expression func(+[](int i) { return i + 5; }); // "T" is "int" // "U" is "int" } * Örnek 6, #include template void func(T&& x, const std::vector& ivec); int main() { std::vector ivec(10); // func(ivec[0], ivec); // "ivec[0]" ifadesinin türü "int" ve değer // kategorisi "L-Value". Dolayısıyla "T" için // "int&" türü olacak. // "ivec" ifadesi yine aynı şekilde fakat tür // çıkarımı "T" için "int" olacak. "ambiguous" // oluşacağından sentaks hatası. func((int)ivec[0], ivec); // OK // Birinci parametrenin türü "int", değer // kategorisi ise "R-Value". Dolayısıyla "T" // için "int" olacak. // İkinci parametre için "T" yine "int" olacak. } * Örnek 7, template void func(T = 0); // Burada fonksiyon parametresi ile bir işimiz olmadığından, "T=0" şeklinde yazdık. Aksi halde "T x=0" biçiminde yazmalıyız. class Myclass {}; int main() { int x{ 20 }; func(&x); // "T" is "int*" func(20); // "T" is "int" func(2.0); // "T" is "double" func(Myclass{}); // "T" is "Myclass" //func(); // Error } >> Anımsanacağı üzere şablon parametreleri üç farklı şekildedir. Bunlar, "Type Template Parameter", "Non-Type Template Parameter" ve "Template Template Parameter". >>> "Type Template Parameter" : template template template template void func(T x); void func(T& x); void func(const T& x); void func(T&& x); void foo(auto x); void foo(auto& x); void foo(const auto& x); void foo(auto&& x); Eğer "concept" kullanmak istersek de; template void func(T x); void func(std::integral auto x); >>> "Non-Type Template Parameter" : "NTTP" diye de kısaltılır. Buradaki tür bilgisi tam sayı olabilir, gösterici ve referans türlerinden olabilir ki bunlara sınıfların üye fonksiyon ve global fonksiyon göstericileri ve referansları da dahildir, gerçek sayı türleri olabilir. * Örnek 1, class Myclass { public: double foo(double); }; int g{}; int foo(int); // template template class A {}; template class B {}; template // Function Pointer class C {}; template // Member Function Pointer class D {}; template class E {}; int main() { int ival{}; // OK A<5> ax; // -----(1): "5" nasılki bir tür belirtmiyorsa, sabit olarak işlev görüyorsa // OK B<&g> bx; // -----(2): "&g" ifadesi de aynı işlevi görecektir. // error C2971: 'B': template parameter 'x': 'l': // a variable with non-static storage duration cannot be used as a non-type argument // int l{}; // B<&l> by; // OK static int q{}; B<&q> bz; // -----(3): "&q" ifadesi de aynı işlevi görecektir. // OK C cx; // -----(4): "foo" ifadesi de aynı işlevi görecektir. // OK D<&Myclass::foo> dx; // -----(5): "&Myclass::foo" ifadesi de aynı işlevi görecektir. // OK E ex; // -----(6): "&g" ifadesi de aynı işlevi görecektir. } * Örnek 2, #include template class Neco {}; char g_s[] = "ULYA"; const char g_cs[] = "YURUK"; int main() { Neco n1; // OK Neco n2; // OK static char s2[] = "yuruk"; Neco n3; // OK // Error: "s1" has an auto life time. char s1[] = "ulya"; Neco n4; // Error: "String Literals" belongs to the "Internal Linkage". // So, we cannot use them with this way. Neco<"UlyA"> n5; } * Örnek 3, #include template // "non-type" parametreleri kullanmadığımız için isim vermedik. class Nec {}; int main() { Nec<10, true> n1; // OK Nec n2; // OK Nec<5, sizeof(int) > 2> n3; // ">" aslında açısal parantezin kapananı olarak ele alınıyor. Nec<5, (sizeof(int) > 2)> n4; // OK } * Örnek 4, #include template constexpr bool less(const T(&a)[N], const T(&b)[M]) { for (int i = 0; i < N && i < M; ++i) { if (a[i] < b[i]) return true; if (b[i] < a[i]) return false; } return N < M; } int main() { int a[] = { 3, 7, 9 }; constexpr int b[] = { 3, 7, 9, 2, 6 }; std::cout << std::boolalpha << less(a, b) << '\n'; constexpr int c[] = { 3, 7, 9, 2, 6 }; constexpr auto f = less(b, c); std::cout << std::boolalpha << f << '\n'; } * Örnek 5, #include // C++20 template T foo(); int main() { auto x = foo<5>(); // "Val" is "5", "T" is "int". auto y = foo<5.5>(); // "Val" is "5.5", "T" is "double". auto z = foo<3, double>(); // "Val" is "3", "T" is "double". } * Örnek 6, struct Neco { int foo(int); int bar(int); int mx; int my; }; template class C {}; int main() { C<&Neco::mx, &Neco::foo> c1; C<&Neco::my, &Neco::foo> c2; C<&Neco::mx, &Neco::bar> c3; C<&Neco::my, &Neco::bar> c4; } * Örnek 7, #include #include template T add_value(T x) { return x + val; } int main() { std::vector source(100); std::vector dest(100); transform(begin(source), end(source), begin(dest), add_value); transform(begin(source), end(source), begin(dest), [](auto i) { return i + 10; }); } * Örnek 8, #include template // non-type parameter class Myclass { public: Myclass() { ++x; } }; int g{30}; int main() { std::cout << g << '\n'; // 30 Myclass<(g)> m1; std::cout << g << '\n'; // 31 } * Örnek 9, template void bar() { // ++N; // error C2105: '++' needs l-value ++r; // auto p1 = &N; // error C2101: '&' on constant auto p2 = &r; // int& r1 = N; // error C2440: 'initializing': cannot convert from 'int' to 'int &' int& r2 = r; } int g{31}; int main() { bar<10, g>(); } * Örnek 10, #include template struct Sum { static auto constexpr value = x + y; }; int main() { std::cout << Sum<3.5, 35L>::value << '\n'; // 38.5 } * Örnek 11.0, C++20 ile birlikte "non-type" parametreler gerçek sayılardan olabilir. #include template struct Neco { Neco() { std::cout << d << '\n'; } }; constexpr auto foo(double d) { return d + d; } int main() { constexpr auto dval{ 19.93 }; Neco d1; // 19.93 } * Örnek 11.1, #include #include template struct Neco; int main() { static_assert(std::is_same_v< Neco<0.3>, Neco<0.7 - 0.4> >); // FAILS static_assert(std::is_same_v< Neco<0.2>, Neco<0.1 + 0.1> >); // HOLDS static_assert(std::is_same_v< Neco<+0.>, Neco<-0.> >); // FAILS } * Örnek 11.2, #include #include template struct StepOne {}; template // All must be type "double". struct StepTwo {}; template // All can be different types. struct StepThree {}; int main() { StepOne<1.2f> so1; StepOne<1.2> so2; StepOne<1.2L> so3; StepTwo<0.1, 1.2, 3.4> st1; StepTwo<5.6, 7.8, 9.1> st2; StepThree<0.1f, 0.2, 0.3L, 'A', 10, 20LL> sth1; } * Örnek 12.0, C++20 ile birlikte sınıflar da belli şartları sağladıkları müddetçe "non-type" olabilirler. Bu şartlardan birisi de ilgili sınıfın kurucu işlevinin "constexpr" olmasıdır. Bu şartlardan bir diğeri de "private" elemana SAHİP OLMAMASIDIR. #include #include template struct MyStringLiteral { constexpr MyStringLiteral(const char(&arr)[N]) { std::copy(arr, arr + N, s); } char s[N]; }; template struct Neco { Neco() { std::cout << str.s << '\n'; } }; int main() { // OK : UlyaYuruk // Arka planda "MyStringLiteral" sınıfının kurucu işlevine // çağrı yapılmıştır. Neco<"UlyaYuruk"> mynec; } * Örnek 12.1, #include struct TNullOpt { }; /* "OptionalInt" is a Literal Class Type */ struct OptionalInt { constexpr OptionalInt(TNullOpt) {} constexpr OptionalInt(int value) : has_value{ true }, value(value) {} const bool has_value{ false }; const uint32_t value{}; }; template void Print() { if constexpr (maybe.has_value) std::cout << "Value: " << maybe.value << '\n'; else std::cout << "No Value\n"; } int main() { // Way - I constexpr OptionalInt x(123); Print(); // Value: 123 // Way - II Print(); // Value: 312 // Way - III Print<231>(); // Value: 231 // Way - I TNullOpt NullOpt; constexpr OptionalInt y(NullOpt); // No Value Print(); // Way - II Print(); // No Value // Way - III Print(); // No Value } * Örnek 12.2, #include template struct Myclass { Myclass() { std::cout << typeid(decltype(x)).name() << '\n'; } }; int main() { // class `int __cdecl main(void)'::`2':: Myclass < []() {} > mx; // class `int __cdecl main(void)'::`2':: Myclass < [](int x) { std::cout << "x: " << x; } > my; // class `int __cdecl main(void)'::`2':: Myclass < [](double x) { return x * x; } > mz; } * Örnek 12.3, #include template struct Myclass { Myclass() { std::cout << typeid(decltype(x)).name() << '\n'; } auto Print(auto value) const { std::cout << "Value: [" << x(value) << "]\n"; } }; int main() { // class `int __cdecl main(void)'::`2':: // Value: [100] Myclass < [](int x) { return x * x; } > my; my.Print(10); // class `int __cdecl main(void)'::`2':: // Value: [149.084] Myclass < [](double x) { return x * x; } > mz; mz.Print(12.21); } * Örnek 12.4, #include #include #include template struct Neco { }; int main() { // Way - I constexpr std::pair sp1{ 13, 1.3 }; Neco nec1; // Way - II constexpr std::pair sp2{ 26, 2.6 }; Neco nec2; // Way - III Neco < std::pair{ 52, 5.2 } > nec3; // Way - I constexpr std::array primes1{ 2, 3, 5, 7 }; Neco nec4; // Way - II constexpr std::array primes2{ 11, 13, 17, 19 }; Neco nec5; // Way - III Neco < std::array{ 2, 3, 5, 7, 11, 13, 17, 19 } > nec6; } * Örnek 12.5, #include #include constexpr int foo() { return 42; } struct Lit { int x = foo(); int y; constexpr Lit(int i) : y(i) {} }; struct Data { int i; std::array vals; Lit lit; }; template void func() { std::cout << typeid(Obj).name() << '\n'; } int main() { func < Data{ 42, { 1, 2, 3}, 42} > (); // struct Data constexpr Data d2{ 1, {2}, 3 }; func(); // struct Data } * Örnek 12.6, #include #include struct Vat { double Val; constexpr Vat(double v) : Val(v) {} friend std::ostream& operator<<(std::ostream& os, const Vat& other) { return os << other.Val; } }; template int add_vat(int value) { return static_cast(std::round(value * (1 + vat.Val))); } int main() { constexpr Vat v{ .2 }; std::cout << "v: " << v << '\n'; // v: 0.2 std::cout << add_vat(45) << '\n'; // 54 std::cout << add_vat(317) << '\n'; // 380 std::cout << add_vat < Vat{ 48.17 } > (4817) << '\n'; // 236852 } >>> "Template Template Parameter" : * Örnek 1, Aşağıdaki örnekte "Template Template Parameter", iki tane "Type Template Parameter" içermelidir. #include template struct NE{}; template struct Neco{}; template struct Erg{}; template< typename T, // Type Parameter typename U, // Type Parameter templatetypename Con // Template Parameter > class NecoErg{}; int main() { NecoErg m1; NecoErg m2; // Error: type/value mismatch at argument 1 in template parameter list for // ‘template class std::vector’ // NecoErg> m3; // Error: type/value mismatch at argument 3 in template parameter list for // ‘template class Con> class NecoErg’ // NecoErg m4; } * Örnek 2.0, #include #include templatetypename C> void func(const C& c) { std::cout << "Size: " << c.size() << '\n'; } int main() { /* # OUTPUT # Size: 50 */ std::vector ivec(50); // "T" is "int". // "C" is "std::vector" // "c" is "std::vector" func(ivec); } * Örnek 2.1, #include #include templatetypename C> void func(const C& c) { std::cout << "Size: " << c.size() << '\n'; } int main() { /* # OUTPUT # Size: 500 */ std::vector ivec(500); func(ivec); } * Örnek 3, #include #include #include templatetypename Con> void func(const Con& con) { std::cout << "T is a " << typeid(T).name() << '\n'; std::cout << "A is a " << typeid(A).name() << '\n'; std::cout << "Con is a " << typeid(Con).name() << '\n'; } int main() { std::vector ivec; std::list dlist; // "T" is "int" // "A" is "class std::allocator" // "Con" is "class std::vector" func(ivec); // "T" is "double" // "A" is "class std::allocator" // "Con" is "class std::list" func(dlist); } * Örnek 4.0, #include #include template class Nec {}; templatetypename Temp> class Myclass { public: Myclass() { Temp x; Temp y; std::cout << typeid(Myclass).name() << '\n'; std::cout << typeid(Temp).name() << '\n'; std::cout << typeid(x).name() << '\n'; std::cout << typeid(y).name() << '\n'; } }; int main() { /* # OUTPUT # class Myclass class Nec class Nec class Nec */ Myclass x; static_assert(std::is_same_v>); } * Örnek 4.1, #include #include template class Nec {}; templatetypename Temp> class Myclass { public: Myclass() { Temp<5> x; Temp<10> y; std::cout << typeid(Myclass).name() << '\n'; std::cout << typeid(Temp).name() << '\n'; std::cout << typeid(x).name() << '\n'; std::cout << typeid(y).name() << '\n'; } }; int main() { Myclass x; static_assert(std::is_same_v>); } * Örnek 4.2, #include #include template class Nec {}; templatetypename Temp> class Myclass { public: Myclass() { Temp<5> x; Temp<10> y; Temp<105u> z; Temp<10.5f> q; std::cout << typeid(Myclass).name() << '\n'; std::cout << typeid(Temp).name() << '\n'; std::cout << typeid(x).name() << '\n'; std::cout << typeid(y).name() << '\n'; } }; int main() { Myclass x; static_assert(std::is_same_v>); } * Örnek 5, #include #include template class A{}; template class B{}; template class C{}; // "Template Template Parameter Pack" // template< templatetypename T, templatetypename U, templatetypename V> template< templatetypename ...Ts> class Myclass { public: Myclass() { // "class Myclass< class A, class B, class C>" std::cout << typeid(Myclass).name() << '\n'; // "class std::tuple< class A, class B, class C>" std::cout << typeid(std::tuple...>).name() << '\n'; // "class std::tuple< class A, class B, class C>" std::cout << typeid(std::tuple...>).name() << '\n'; } }; int main() { Myclass x; } * Örnek 6, #include #include #include templatetypename C> std::ostream& operator<<(std::ostream& os, const C& c) { for (const auto& e : c) os << e << ' '; return os; } int main() { std::vector ivec{10, 20, 30, 40, 50, 60}; std::cout << ivec << '\n'; // 10 20 30 40 50 60 std::list ilist{1, 2, 3, 4, 5, 6}; std::cout << ilist << '\n'; // 1 2 3 4 5 6 } * Örnek 7.0, #include #include #include template< templatetypename Con > void f1() {} template< templatetypename Con > void f2() {} template< templatetypename Con > void f3() {} template< templatetypename Con > void f4() {} template class T1{}; template class T2{}; template class T3{}; template class T4{}; int main() { f1(); f2(); f3(); f4(); f4(); f4(); f4(); } * Örnek 7.1, #include #include #include #include template std::ostream& operator<<(std::ostream& os, const std::pair& p) { return os << "<" << p.first << "," << p.second << ">"; } template< templatetypename Con, typename ...Ts> std::ostream& operator<<(std::ostream& os, const Con& c) { for (const auto& i : c) os << i << ' '; return os; } int main() { /* # OUTPUT # */ std::map mymap{ {2,4}, {4,7}, {3,5} }; std::cout << mymap << '\n'; // <2,4> <3,5> <4,7> std::vector ivec{10, 20, 30, 40, 50, 60}; std::cout << ivec << '\n'; // 10 20 30 40 50 60 std::list ilist{1, 2, 3, 4, 5, 6}; std::cout << ilist << '\n'; // 1 2 3 4 5 6 } >> Şimdi de "Variable Templates" konusunu ele alalım. Nasılki sınıf şablonlarının açılımları sonucunda sınıf, fonksiyon şablonlarının açılımında bir fonksiyon elde ediyoruz, değişken şablonlarının açılımından da bir değişken elde ederiz. C++14 ile dile eklenmiştir. * Örnek 1, #include template constexpr bool is_four = sizeof(T) == 4; template bool is_eight = sizeof(T) == 8; int main() { // OK since "is_four" is "constexpr". if constexpr (is_four) { // sizeof int has 4 bytes std::cout << "sizeof int has " << sizeof(int) << " bytes\n"; } // OK. if (is_four) { // sizeof double has 8 bytes std::cout << "sizeof double has " << sizeof(double) << " bytes\n"; } // OK since "is_eight" is NOT "constexpr". if (is_eight) { // sizeof double has 8 bytes std::cout << "sizeof double has " << sizeof(double) << " bytes\n"; } } * Örnek 2, #include #include // "T" bir "constexpr" olarak nitelendiği // için "Compile-Time" sabiti olarak da // kullanabiliriz. template constexpr T pi = T(3.1415926535897932385L); template T circular_area(T r) { return pi *r * r; } int main() { std::cout << pi << '\n'; // 3 std::cout << pi << '\n'; // 3.14159 std::cout << pi << '\n'; // 3.14159 std::cout << pi << '\n'; // 3.14159 std::cout << circular_area(123456789) << '\n'; // 45724736250571563 } * Örnek 3, #include template constexpr std::size_t fact = n * fact; // Base Case: (Explicit Specialization) template<> constexpr std::size_t fact<0> = 1u; int main() { // "val" is "120U". // It is calculated in the compile time. constexpr auto val = fact<5>; static_assert(val == 120U); // HOLDS } * Örnek 4, #include template constexpr std::size_t power = base * power; // Partial Specialization template constexpr std::size_t power = base; // Partial Specialization template constexpr std::size_t power = 1; int main() { static_assert(power<3, 4> == 81u); // HOLDS static_assert(power<2, 10> == 1024u); // HOLDS } * Örnek 5, #include template constexpr int ar[] = { Vals... }; int main() { // 1 2 3 4 5 for (auto i : ar< 1, 2, 3, 4, 5>) std::cout << i << ' '; } * Örnek 6, #include #include template constexpr T sz = static_cast(sizeof(U)); class Myclass {}; int main() { // "var" is of type "int" and // has value of "1". constexpr auto var = sz; } * Örnek 7, #include template constexpr bool IsPositive{ N > 0 }; class BigInt { public: constexpr BigInt(int x) : mx(x) {} constexpr bool operator>(const BigInt& other) const{ return mx > other.mx; } int mx; }; int main() { // Positive if constexpr (IsPositive < BigInt{ 98 } > ) { std::cout << "Positive\n"; } else { std::cout << "Negative\n"; } } * Örnek 8, #include template constexpr int sum = (... + Vals); // Fold Expression template constexpr int sum_square = (... + (Vals * Vals)); int main() { constexpr auto result_sum = sum<1, 2, 3, 4, 5, 6, 7, 8, 9>; static_assert(result_sum == 45); // HOLDS constexpr auto result_sum_square = sum_square< 1, 2, 3, 4, 5, 6, 7, 8, 9>; static_assert(result_sum_square == 285); // HOLDS } * Örnek 9, #include template constexpr int div_left = (... / Vals); template constexpr int div_right = (Vals / ...); int main() { { // Result: 5 constexpr auto result = (((500 / 5) / 10) / 2); constexpr auto result_1 = div_left<500, 5, 10, 2>; static_assert(result == result_1); } { // Result: 500 constexpr auto result = (500 / (5 / (10 / 2))); constexpr auto result_1 = div_right<500, 5, 10, 2>; static_assert(result == result_1); } } >> Şimdi de şablon parametrelerinin varsayılan argüman almasına değinelim; Burada fonksiyonların parametre değişkenlerinin varsayılan argüman almasıyla şablon parametrelerinin varsayılan argüman alması birbiriyle karıştırılabilmektedir. Diğer yandan sınıf şablonları ile fonksiyon şablonları arasında da varsayılan argüman alma konusunda bir kural farklılığı vardır. Şöyleki; sınıf şablonlarının sadece en sondaki şablon parametresi varsayılan argüman alabilirken, fonksiyon şablonlarında böyle bir şey kısıtlama yoktur. Fakat bunun da şöyle bir "work-around" yöntemi vardır; anımsanacağı üzere fonksiyon bildirimlerini derleyiciler kümülatif olarak ele alırlar. İşte bunu sınıf şablonlarında da kullanabiliriz. * Örnek 1, // Fonksiyon Şablonları için OK template void func() { //... } // Sınıf Şablonları için Sentaks Hatası template class Myclass{ }; * Örnek 2, #include template struct DefaultDeleter{ void operator()(T* p) { std::cout << "DefaultDeleter::operator was called\n"; delete p; } }; template> class UniquePtr{ public: UniquePtr() : mp{ new T } {} ~UniquePtr() { if (mp) D{}(mp); } private: T* mp; }; int main() { /* # OUTPUT # main basladi DefaultDeleter::operator was called main bitecek */ std::cout << "main basladi\n"; { UniquePtr up1; } std::cout << "main bitecek\n"; } * Örnek 3, #include #include template class A{ }; template> class B{ }; int main() { A a1; // A A a2; // A B b1; // B> B> b2; // B> } * Örnek 4, Yukarıda bahsedilen "Work-Around". Tabii sınıf şablonlarında, şablon parametresini kullanmadığımız için, "T, U, V" harflerini silsek de olurdu. #include // Derleyici aşağıdaki bildirimleri kümülatif olarak ele alacağından, // günün sonunda her üç argüman da aslında varsayılan argüman // alacaktır. İşte bunu sınıf şablonlarında da uygulayabiliriz ----(1) void f1(int, int, int = 0); void f1(int, int = 1, int); void f1(int = 2, int, int) {} // ----(1) template class Myclass; template class Myclass; template class Myclass{}; int main() { /* # OUTPUT # */ Myclass<> m; static_assert( std::same_as< decltype(m), Myclass > ); // HOLDS } * Örnek 5, #include template struct NE{}; template struct Neco{}; template struct Erg{}; template< typename T, // Type Parameter typename U, // Type Parameter templatetypename Con = Neco // Template Parameter > class NecoErg{}; int main() { NecoErg m1; // NecoErg m1; NecoErg m2; // Error: type/value mismatch at argument 1 in template parameter list for // ‘template class std::vector’ // NecoErg> m3; // Error: type/value mismatch at argument 3 in template parameter list for // ‘template class Con> class NecoErg’ // NecoErg m4; } * Örnek 6, #include template T func(T x = {}) { std::cout << "x : " << x << '\n'; return x; } int main() { /* # OUTPUT # */ func(); // func(int{}); // x : 0 func(31); // x : 31 func("Ulya Yuruk"); // x : Ulya Yuruk } * Örnek 7, #include #include template> bool func(T x, F f = {}){ return f(x, x+1); } int main() { std::cout << std::boolalpha << func(5) << '\n'; // true std::cout << std::boolalpha << func(10, std::greater{}) << '\n'; // false } * Örnek 8, #include #include #include template> C func(T x = {}, U y= {}, C c = {}){ c.first = x; c.second = y; return c; } int main() { auto my_pair = func(19, 93); std::cout << "<" << my_pair.first << "," << my_pair.second << ">\n"; // <19,93> my_pair = func(); std::cout << "<" << my_pair.first << "," << my_pair.second << ">\n"; // <0,0> } >> Şimdi de "Full(Explicit) Specialization" ve "Partial Specialization" konularını ele alalım. Her iki biçimde de bizim amacımız, şablonun belli türleri için, bizim yazacağımız kodların kullanılmasını sağlatmaktır. Artık derleyici o türler için kod açılım yapmayacak ve bizim yazdığımız kodu kullanacaktır. "Partial Specialization" sadece sınıf şablonları içindir. Öte yandan sadece özelleştirilmiş versiyonları kullanacaksak, "Base Template" sınıfı/fonksiyonu sadece bildirmek yeterli gelecektir. >>> "Full(Explicit) Specialization": Bu özelleştirme türüne aynı zamanda "Diamond Specialization" da denir. Orjinal şablona "Base Template" denir. Bu tip "Specialization" yönteminde "Interface" ler aynı olmak zorunda DEĞİLDİR. Sınıf ve fonksiyon şablonları için kullanılabilir. Diğer yandan "non-type template parameter" için de özelleştirme yapabiliriz. Fakat bunun için "Base Template" in de yine "non-type template parameter" olması gerekmektedir. Diğer yandan sınıfların üye fonksiyonları için de "Full(Explicit) Specialization" yapabiliriz. Hakeza sınıfların "static" veri elemanları için de özelleştirme yapabiliriz. Yine fonksiyon şablonlarında bu özelleştirmeyi kullanırken şu noktaya dikkat etmemiz gerekmektedir; "Function Overload Resolution" a "Base Template" in kendisi girmektedir, bizim yazdığımız DEĞİL. Eğer yapılan eleme sonucunda kazanan "Base Template" ise ve çağrı sırasında da özelleştirilen tür kullanılmışsa, bizim yazdığımız kodlar çağrılacaktır. Yani ilk aşama "Base Template" in seçilmesi, devamında da uygun türün çağrı sırasında kullanılması gerekmektedir. Ek olarak, "Variable Template" lere de özelleştirme yapılabilmektedir. * Örnek 1.0, #include template struct Myclass { Myclass () { std::cout << "Primary Template for Myclass\n"; } void f1() {} void f2() {} }; template<> struct Myclass { Myclass () { std::cout << "Explicit Specialization for Myclass\n"; } void f3() {} void f4() {} }; int main() { /* # OUTPUT # Primary Template for Myclass Primary Template for Myclass Primary Template for Myclass Primary Template for Myclass Explicit Specialization for Myclass */ Myclass uc; uc.f1(); // OK Myclass sc; sc.f2(); // OK Myclass c; c.f3(); // error: ‘struct Myclass’ has no member named ‘f3’; Myclass f; f.f4(); // error: ‘struct Myclass’ has no member named ‘f4’; Myclass i; i.f1(); // error: ‘struct Myclass’ has no member named ‘f1’; i.f2(); // error: ‘struct Myclass’ has no member named ‘f2’; i.f3(); i.f4(); } * Örnek 1.1, #include template struct Myclass { Myclass () { std::cout << "Primary Template for Myclass\n"; } }; template<> struct Myclass { Myclass () { std::cout << "Explicit Specialization for Myclass\n"; } }; int main() { Myclass m1; // Primary Template for Myclass Myclass m2; // Explicit Specialization for Myclass Myclass m3; // Primary Template for Myclass } * Örnek 2, #include template struct Myclass { Myclass () { std::cout << "Primary Template for Myclass\n"; } }; template<> struct Myclass { Myclass () { std::cout << "Explicit Specialization for Myclass\n"; } }; template<> struct Myclass { Myclass () { std::cout << "Explicit Specialization for Myclass\n"; } }; int main() { /* # OUTPUT # */ Myclass m1; // Primary Template for Myclass Myclass m2; // Explicit Specialization for Myclass Myclass m3; // Primary Template for Myclass Myclass m4; // Explicit Specialization for Myclass } * Örnek 3, #include template struct Myclass { Myclass () { std::cout << "Primary Template\n"; } }; template<> struct Myclass<3> { Myclass () { std::cout << "Explicit Specialization for Myclass<3>\n"; } }; template<> struct Myclass<5> { Myclass () { std::cout << "Explicit Specialization for Myclass<5>\n"; } }; int main() { /* # OUTPUT # Explicit Specialization for Myclass<3> Explicit Specialization for Myclass<5> Primary Template */ Myclass<3> m1; Myclass<5> m2; Myclass<7> m3; } * Örnek 4, #include template struct Myclass; template<> struct Myclass<1> { Myclass () { std::cout << "Explicit Specialization for Myclass<1>\n"; } }; template<> struct Myclass<3> { Myclass () { std::cout << "Explicit Specialization for Myclass<3>\n"; } }; template<> struct Myclass<5> { Myclass () { std::cout << "Explicit Specialization for Myclass<5>\n"; } }; template<> struct Myclass<7> { Myclass () { std::cout << "Explicit Specialization for Myclass<7>\n"; } }; int main() { /* # OUTPUT # Explicit Specialization for Myclass<1> Explicit Specialization for Myclass<3> Explicit Specialization for Myclass<5> Explicit Specialization for Myclass<7> */ Myclass<1> m1; Myclass<3> m3; Myclass<5> m5; Myclass<7> m7; // Myclass<9> m9; // error: aggregate ‘Myclass<9> m9’ has incomplete type and cannot be defined } * Örnek 5, #include // C++98 template struct Factorial { static const int value = n * Factorial::value; }; template<> struct Factorial<0> { static const int value = 1; }; int main() { /* # OUTPUT # */ std::cout << "5! : " << Factorial<5>::value << '\n'; // 5! : 120 } * Örnek 6, Döngü kullanmadan, 0-100 arasındakileri ekrana yazma. #include template struct Counter : Counter { Counter() { std::cout << n << (n % 20 == 0 ? '\n' : ' '); } }; template<> struct Counter<0> {}; int main() { /* # OUTPUT # 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 */ Counter<100> m; } * Örnek 7, #include template struct Nec { void func(T) { std::cout << "Primary Template\n"; } }; template<> void Nec::func(int) { std::cout << "Explicit Template\n"; } int main() { /* # OUTPUT # Primary Template Primary Template Primary Template Explicit Template */ Nec m1; Nec m2; Nec m3; Nec m4; m1.func(1.2f); m2.func(1.2f); m3.func(1.2f); m4.func(1.2f); } * Örnek 8.0, #include template void foo(T) { std::cout << "Primary Template\n"; } template<> void foo/**/(char) { std::cout << "Specialization: foo\n"; } template<> void foo(signed char){ std::cout << "Specialization: foo\n"; } template<> void foo(unsigned char){ std::cout << "Specialization: foo\n"; } void foo(int) { std::cout << "foo(int)\n"; } void foo(float) { std::cout << "foo(float)\n"; } int main() { // Specialization: foo foo('a'); // foo(int) foo(4); // foo(float) foo(4.f); // Primary Template foo(8.); } * Örnek 8.1, #include template void func(T) { std::cout << 1; } template<> void func(int*) { std::cout << 2; } template void func(T*) { std::cout << 3; } int main() { int* p = nullptr; func(p); // 3: "1" ve "3" girer "Function Overload Resolution" a. Eğer "1" kazansaydı ve // argümanın türü "int*" olsaydı, "2" seçilecekti. } * Örnek 8.2, #include template void func(T) { std::cout << 1; } template void func(T*) { std::cout << 2; } template<> void func(int*) { std::cout << 3; } int main() { int* p = nullptr; func(p); // 3 // Şimdi yarışmaya "1" ve "2" girer. Kazanan ise "2" olur, çünkü o daha spesifik. // Fakat argümanın türü "int*" olduğundan "3" seçilir. } * Örnek 8.3, #include template void func(T) { std::cout << 1; } template<> void func(int*) { std::cout << 2; } template void func(T*) { std::cout << 3; } template<> void func(int*) { std::cout << 4; } int main() { int* p = nullptr; func(p); // 4 // "1" ve "3" yarışmaya girer ve "3" daha spesifik olduğundan kazanır. Fakat // "int*" özelleştirme yapıldığından, "4" seçilir. } * Örnek 9, #include template struct Neco{ static int x; }; // The Definition: template int Neco::x = 67; // The Specialization: template<> int Neco::x = -1; int main() { std::cout << Neco::x << '\n'; // 67 std::cout << Neco::x << '\n'; // -1 std::cout << Neco::x << '\n'; // 67 std::cout << Neco::x << '\n'; // 67 } * Örnek 10.0, "sizeof(void)" sentaks hatası oluşturur çünkü "void" türü "incomplete" türdür. #include template constexpr std::size_t SZ = sizeof(T); int main() { auto result = SZ + SZ + SZ + SZ; } * Örnek 10.1, Artık "SZ" nin "void" açılımı için özelleştirme yazdığımız için "sizeof(void)" ifadesi sentaks hatası oluşturmayacaktır. #include template constexpr std::size_t SZ = sizeof(T); template<> constexpr std::size_t SZ = 0; int main() { auto result = SZ + SZ + SZ + SZ; std::cout << result << '\n'; // 3 } >>> "Partial Specialization": "Full(Explicit) Specialization" a göre daha kapsayıcıdır. Yine "non-type template parameter" için de kullanılabilir. Yine bu tip özelleştirme "constraint" edilebilir. * Örnek 1, #include template struct Myclass { Myclass() { std::cout << "Primary Template\n"; } }; // Partial Specialization: Bütün gösterici türleri için // aşağıdaki seçilecek. template struct Myclass { Myclass() { std::cout << "Partial Template\n"; } }; int main() { Myclass m1; // Primary Template Myclass m2; // Partial Template Myclass m3; // Primary Template Myclass m4; // Partial Template } * Örnek 2.0, #include template struct Myclass { Myclass() { std::cout << "Primary\n"; } }; /* * Görüldüğü üzere "Base" olanın şablon bildiriminden * farklı biçimde bir özelleştirme yapabiliriz. Artık * aynı türler için bu, farklı türler için "Base" olan * kullanılacak. */ template struct Myclass { Myclass() { std::cout << "Partial\n"; } }; int main() { Myclass m1; // Partial Myclass m2; // Primary } * Örnek 2.1, #include template struct Myclass { Myclass() { std::cout << "Primary\n"; } }; template struct Myclass { Myclass() { std::cout << "Partial\n"; } }; int main() { Myclass m0; // Primary Myclass m1; // Partial Myclass m2; // Partial Myclass m3; // Primary // Myclass m4; // ERROR: cannot declare pointer to 'int&' } * Örnek 2.2, #include #include template struct Myclass { Myclass() { std::cout << "Base\n"; } }; template struct Myclass> { Myclass() { std::cout << "Primary\n"; } }; int main() { Myclass m1; // Base Myclass> m2; // Base Myclass> m3; // Base Myclass> m4; // Base Myclass> m5; // Primary } * Örnek 3.0, #include #include template struct IsPointer : std::false_type{}; template struct IsPointer : std::true_type{}; int main() { constexpr bool b = IsPointer::value; // true } * Örnek 3.1, #include #include template struct IsPointer : std::false_type{}; template struct IsPointer : std::true_type{}; template constexpr bool IsPointer_v = IsPointer::value; int main() { constexpr bool a = IsPointer::value; // true constexpr auto b = IsPointer_v; // true } * Örnek 4, #include #include template struct Neco{ Neco() { std::cout << "Primary\n"; } }; template struct Neco{ Neco() { std::cout << "T[]\n"; } }; template struct Neco{ Neco() { std::cout << "T[4]\n"; } }; template struct Neco{ Neco() { std::cout << "T[12]\n"; } }; int main() { Neco m; // T[] Neco m0; // Primary Neco m2; // Primary Neco m4; // T[4] Neco m6; // Primary Neco m8; // Primary Neco m10; // Primary Neco m12; // T[12] } * Örnek 5, #include #include template struct Neco{ Neco() { std::cout << "Primary\n"; } }; template struct Neco { Neco() { std::cout << "Neco\n"; } }; template struct Neco { Neco() { std::cout << "Neco\n"; } }; int main() { Neco m0; // Neco Neco m1; // Neco Neco m2; // Primary Neco m3; // Primary Neco m4; // Primary } * Örnek 6.0, #include #include #include template struct X { X() { std::cout << "Primary\n"; } }; template struct X { X() { std::cout << "integral\n"; } }; template struct X { X() { std::cout << "floating point\n"; } }; int main() { X x1; // integral X x2; // integral X x3; // floating point X x4; // Primary X x5; // Primary } * Örnek 6.1, #include #include #include #include #include #include #include template struct X { X() { std::cout << "Primary\n"; } }; template struct X { X() { std::cout << "range\n"; } }; int main() { X x1; // Primary X x2; // Primary X x3; // Primary X x5; // Primary X x4; // range X> x6; // range X> x7; // range X> x8; // Primary } >> Şimdi de "Variadic Template" konusuna değinelim. Bu tip şablonlar, parametresi parametre paketi olan şablonlardır. "Template Type Parameter", "Template Non-Type Parameter" ve "Template Template Parameter" türleri ile kendi içinde paketler oluşturabiliriz. * Örnek 1, // Burada "n" adet "type template parameter" // içeren bir paket oluşturduk. template class X {}; // Burada "n" farklı "non-type template parameter" // içeren bir paket oluşturduk. template class Y_1 {}; // Burada "n" adet "int" türlerinden bir paket oluşturduk. template class Y_2 {}; // Burada öyle bir paket oluşturduk ki her bir öğe, // bir adet "type template parameter" e sahip // bir "Template Template Parameter". template< templatetypename ...> class Z_1{}; // Burada öyle bir paket oluşturduk ki her bir öğe, // iki adet "type template parameter" e sahip // bir "Template Template Parameter". template< templatetypename ...> class Z_2 {}; // Burada öyle bir paket oluşturduk ki her bir öğe, // üç adet "type template parameter" e sahip // bir "Template Template Parameter". template< templatetypename ...> class Z_3 {}; // Burada öyle bir paket oluşturduk ki her bir öğe, // bir adet "int" e sahip // bir "Template Template Parameter". template< templatetypename ...> class Z_4 {}; // Burada öyle bir paket oluşturduk ki her bir öğe, // "n" adet "int" e sahip // bir "Template Template Parameter". template< templatetypename ...> class Z_5 {}; // Burada öyle bir paket oluşturduk ki her bir öğe, // "n" farklı "non-type template parameter" e sahip // bir "Template Template Parameter". template< templatetypename ...> class Z_6 {}; int main() {} * Örnek 2, Bir paket içerisinde kaç öğe olduğunu da derleme zamanı sabiti olarak öğrenebiliriz. #include template void print_elements(TypeArgs ... elements) { constexpr auto amount_TypeArgs = sizeof...(TypeArgs); constexpr auto amount_elements = sizeof...(elements); std::cout << "Amount of TypeArgs: " << amount_TypeArgs << '\n'; std::cout << "Amount of Elements: " << amount_elements<< '\n'; } int main() { // 'void print_elements(int,int,int)' // Amount of TypeArgs : 3 // Amount of Elements : 3 print_elements(3, 4, 5); // 'void print_elements(char,int,float,double,long long,long double)' // Amount of TypeArgs : 6 // Amount of Elements : 6 print_elements('a', 2, 3.f, 4., 5LL, 6.L); } * Örnek 3.0, Yine paket parametresi ile birlikte "Type Template Parameter", "Non-Type Template Parameter" ve "Template Template Parameter" de kullanılabilir. #include template void print_elements(T x, TypeArgs ... elements) { std::cout << "x: " << typeid(T).name() << '\n'; constexpr auto amount_TypeArgs = sizeof...(TypeArgs); constexpr auto amount_elements = sizeof...(elements); std::cout << "Amount of TypeArgs: " << amount_TypeArgs << '\n'; std::cout << "Amount of Elements: " << amount_elements<< '\n'; } int main() { // 'void print_elements(T,int,float,double,long long,long double)' // x: char // Amount of TypeArgs : 5 // Amount of Elements : 5 print_elements('a', 2, 3.f, 4., 5LL, 6.L); } * Örnek 3.1, #include template void print_elements(std::size_t x, TypeArgs ... elements) { std::cout << "n: " << typeid(n).name() << '\n'; std::cout << "x: " << typeid(x).name() << '\n'; constexpr auto amount_TypeArgs = sizeof...(TypeArgs); constexpr auto amount_elements = sizeof...(elements); std::cout << "Amount of TypeArgs: " << amount_TypeArgs << '\n'; std::cout << "Amount of Elements: " << amount_elements<< '\n'; } int main() { // 'void print_elements<5,float,double,long long,long double>(size_t,float,double,long long,long double)' // n : unsigned int // x : unsigned int // Amount of TypeArgs : 4 // Amount of Elements : 4 print_elements<5>('a', 3.f, 4., 5LL, 6.L); } Şimdi "Variadic Template" söz konusu olduğunda, birden fazla parametre kullanılacağı için, bu konuda karşımıza bir takım yöntemler çıkmaktadır. Bunlar sırasıyla "Compile Time Recursivity", "Compile Time Recursivity with 'if constexpr'", "initializer list" ve "fold expression". Fakat bu teknikleri irdelemeden evvel bizlerin paketin nasıl açıldığına da değinmemiz gerekmektedir. >>> Paket Açılımı: Eğer elimizde bir liste varsa, ister sınıf şablonunda ister fonksiyon, ve uygun şartlar da sağlanmışsa, virgüller ile ayrılmış listeye bu paketi dönüştürebiliriz. İşte buna "Pack Expansion" denir ve dilin de saptadığı bir takım arayüzler, yani "pattern" ler mevcuttur. Bu arayüzlere ise "Pack Expansion Pattern" denir. * Örnek 1.0, template void func(Ts ... args) { //... // Assume 'foo' is a function; foo(args...); // foo(p1, p2, p3); foo(++args...); // foo(++p1, ++p2, ++p3); foo(&args...); // foo(&p1, &p2, &p3); foo(sizeof(args)...); // foo(sizeof(p1), sizeof(p2), sizeof(p3)); // Görüldüğü üzere "..." ifadesi, kendisinden evvelki // ifadeler neyse, onları kullanmaktadır. } int main() {} * Örnek 1.1, template void foo(Ts ... args) { //... } template void func(Ts ... args) { foo(&args...); // foo(&p1, &p2, &p3); } int main() { func(12, 45, 67); } * Örnek 1.2.0, #include int bar(int x) { std::cout << "bar(" << x << ")\n"; return x; } template void foo(Ts ... args) { //... } template void func(Ts ... args) { foo(bar(args)...); // foo(bar(p1), bar(p2), bar(p3)); // ERROR: 'bar': function does not take 3 arguments // foo(bar(args...)); // foo(bar(p1, p2, p3)); } int main() { func(12, 45, 67); // bar(67) // bar(45) // bar(12) } * Örnek 1.2.1, #include int bar(int x) { std::cout << "bar(" << x << ")\n"; return x; } template void foo(Ts ... args) { //... } template void func(Ts ... args) { foo(bar(args*args)...); // foo(bar(p1), bar(p2), bar(p3)); } int main() { func(12, 45, 67); // bar(4489) // bar(2025) // bar(144) } * Örnek 1.3, template void func(Args... args) { // 'foo' nun bir fonksiyon olduğunu ve 'func' // fonksiyonuna da dört tane argüman gönderildiğini // varsayalım. Bu durumda 'foo' fonksiyonunu şu // şekilde görebiliriz; // foo(p1, p2, p3, p4); // Buradaki önemli nokta, parametre paketinden sonra // "..." atomunun gelmesidir. foo(args...); } * Örnek 1.4.0, #include #include template void print(const Ts&... args) { // error C3520: 'args': parameter pack must be expanded in this context // ((std::cout << args << ' '), 0); // -----> (1) /* -----> (1) * "((std::cout << args << ' '), 0)" ifadesiyle birlikte * "args" değeri ekrana yazdırılacak. İfadenin geri dönüş * değeri "0" olacak. Fakat paket açılımı yapmadığımız için * sentaks hatası alacağız. */ // {((std::cout << args << ' '), 0)...}; // -----> (2) /* -----> (2) * "{((std::cout << args << ' '), 0)...}" ifadesiyle bizler * hem ekrana "args" değerini yazıyor, hem "," operatörünün * geri dönüş değeri ile bir "initializer_list" oluşturuyor * hem de paket açılımı yapmaktayız. İşte ortaya çıkarılan * iş bu "initializer_list" i bir "container" için ilk değer * vermekte kullanabiliriz. */ // int a[] = { ((std::cout << args << ' '), 0)... }; // -----> (3) /* -----> (3) * Artık "a" dizimizin elemanları, parametre paketindeki * öğe sayısı kadar "0" elemanı içermektedir. Fakat buradaki * problem ise bu dizinin tanımlanmış fakat daha sonra * kullanılmamış olmasıdır. Böylesi bir dizi yerine * "std::initializer_list" in "int" açılımı türünden geçici * bir nesne oluşturabiliriz. */ // (void)std::initializer_list{ ((std::cout << args << ' '), 0)... }; // -----> (4) /* -----> (4) * Artık "std::initializer_list" in "int" açılımı türünden geçici * bir nesne oluşturduk. Statik kod analiz programları ve/veya * derleyici programlar herhangi bir uyarı vermesin diye de bu * nesneyi "void" türüne cast ettik. Artık "print" fonksiyonuna * gönderilen argümanları daha etkili bir biçimde ekrana yazdırabiliriz. */ using extender = int[]; (void)extender { ((std::cout << args << ' '), 0)... }; // -----> (5) /* -----> (5) * Fakat o yöntem de yazım kalabalığı oluşturduğu için * biraz zahmetli. İşte adına "Poor Man's Fold Expression" * da denilen yukarıdaki yöntemle, yazma zahmetinden de * kurtulmuş olmaktayız. */ } int main() { print('a', 2, 4.4, "ulya yuruk"); // a 2 4.4 ulya yuruk } * Örnek 1.4.1, "," operatöründen faydalanamak. #include // Old School: //template //void print(Args...args) { // C++20, Aggreviated Template Syntax: void print(auto... args) { ((std::cout << args << ' '), ...); // Unary Left Fold Expression std::cout << '\n'; } auto g(auto* x) { return *x * *x + 10; } // Old School: //template //void f(Args...args) { // C++20, Aggreviated Template Syntax: void f(auto... args) { // Assuma 3 arguments were used to call "f", // called "p1", "p2" and "p3". // Kural Şudur: // "..." atomu, kendisinden evvelki en uzun // ifadeye ele almaktadır. // # Pattern # # Function Call # # Result # print(args...); // args print(p1, p2, p3); 1 2 3 print(&args...);// &args print(&p1, &p2, &p3); 007DF7A0 007DF7A4 007DF7A8 print(10 * args...);// 10*args print(10*p1, 10*p2, 10*p3); 10 20 30 print(args * args...);// args*args print(p1*p1, p2*p2, p3*p3); 1 4 9 print(g(&args)...);// g(&args) print(g(&p1), g(&p2), g(&p3)); 11 14 19 print(++args...);// ++args print(++p1, ++p2, ++p3); 2 3 4 } int main() { int x{ 1 }, y{ 2 }, z{ 3 }; f(x, y, z); } * Örnek 1.4.2, #include #include template void bar(Ts&&... args) { int cnt{}; ( (std::cout << ++cnt << ". argument is : " << (std::is_lvalue_reference_v ? "L-Value" : "R-Value") << '\n') , ... ); } template void foo(Ts&&... args) { bar( std::forward(args)... ); // # Pattern # // std::forward(args) // # Function Call # // bar( // std::forward(p1), // std::forward(p2), // std::forward(p3), // std::forward(p4) // ); } int main() { int x{}; double y{}; // void bar // (int &,_Ty &&,_Ty &&,double &,double &&,const char (&)[11]) // void foo // (int &,int &&,int &&,double &,double &&,const char (&)[11]) foo(x, 34, std::move(x), y, +y, "Ulya Yuruk"); /* # OUTPUT # 1. argument is : L-Value 2. argument is : R-Value 3. argument is : R-Value 4. argument is : L-Value 5. argument is : R-Value 6. argument is : L-Value */ } * Örnek 1.5, #include // C++20: // auto square(auto x) { // Old School: template T square(T x) { return x * x; } // C++20 auto sum(auto...args) { return (... + args); } template void call_sum(T... args) { auto x1 = sum(args...); // sum(1, 2, 3, 4, 5); std::cout << "x1: " << x1 << '\n'; // 15 x1 = sum(85, args...); // sum(85, 1, 2, 3, 4, 5); std::cout << "x1: " << x1 << '\n'; // 100 x1 = sum(square(args)...); // sum(1, 4, 9, 16, 25); std::cout << "x1: " << x1 << '\n'; // 55 } int main() { call_sum(1, 2, 3, 4, 5); } * Örnek 1.6, template void f(Us... pargs) {} template void g(Ts... args) { // # Pattern # # Function Call # # Result # g(&args...); // &args f(&p1, &p2, &p3) } int main() { // "Ts..." will expand to "int", "double", "char" for // "p1", "p2" and "p3", respectivelly. // So, "&args" means that "&p1", "&p2" and "&p3". // So, "Us..." will expand to "int*", "double*", "char*" for // "p1", "p2" and "p3", respectivelly. g(1, .2, 'u'); } * Örnek 1.7, #include template constexpr auto sum(Ts... args) { return (0 + ... + args); // Binary Fold Expression // Bu fonksiyona geçilen argümanların türleri farklı // olabilir. Örneğin, "int", "float" ve "double". // Dolayısıyla "constraint" edilmediğinden, toplama // işlemi sırasında dikkatli olmalıyız. } template constexpr auto multiply(Ts... args) { return (1 * ... * args); } template constexpr auto foo(Ts... args) { return multiply(sum(args...) + args...); // Patterns: | | sum(args...) + args // | sum(args...) } int main() { constexpr auto result1 = foo(1, 2, 4); // 792 /* # Function Call # multiply( sum(1, 2, 4) + 1, // 8 sum(1, 2, 4) + 2, // 9 sum(1, 2, 4) + 4 // 11 ); // multiply(8, 9, 11); */ constexpr auto result2 = foo('a', 2., 16LL); // 3'249'324.0 /* # Function Call # multiply( sum(97, 2., 16) + 97, // 212 sum(97, 2., 16) + 2., // 117 sum(97, 2., 16) + 16 // 131 ); // multiply(212, 117, 131); */ } * Örnek 1.8, #include #include template class Myclass { //... std::tuple mx; }; int main() { Myclass a; // mx: std::tuple } * Örnek 1.9, #include #include template class Myclass { public: static constexpr size_t sz = sizeof...(Args); }; int main() { Myclass a; // sz = 3; } * Örnek 1.10, #include #include template auto make_an_empty_array(Args... args) { return std::array{}; // "sizeof...(args)" ve "sizeof...(Args) => "sizeof" operatörü; } template auto make_a_filled_array(Args... args) { return std::array{sizeof(args)...}; // "sizeof(Args)..." ve "sizeof(args)..." => "Pack Expansion" => "sizeof(T1), sizeof(T2)... } template auto make_a_real_array(Args... args) { return std::array{args...}; // "sizeof(Args)..." ve "sizeof(args)..." => "Pack Expansion" => "sizeof(T1), sizeof(T2)... } int main() { // "arr" is of type "std::array('a', 30, 19.31f); for (auto i : arr) { std::cout << i << ' '; // 0 0 0 } std::cout << '\n'; // "arr2" is of type "std::array('a', 30, 19.31f); for (auto i : arr2) { std::cout << i << ' '; // 1 4 4 } std::cout << '\n'; // ERROR: "int" to "std::size_t" && "float" to "std::size_t" is // narrowing conversion. // WARNING: "float" to "std::size_t" is possible loss of data. // auto arr3 = make_a_real_array('a', 30, 19.31f); auto arr3 = make_a_real_array(30, 45000, 1993); for (auto i : arr3) { std::cout << i << ' '; // 30 45000 1993 } } * Örnek 1.11, #include constexpr int square(int N) { return N * N; } template void print_array(int(&arr)[N]) { for (int i : arr) std::cout << i << ' '; std::cout << '\n'; } // Bu fonksiyona gönderilen argümanların // hepsinin "int" olması gerekmektedir. void foo(std::same_as auto... args) { int a1[] = { args... }; // "a1" contains elements "{1, 3, 5, 7}"; print_array(a1); // 1 3 5 7 int a2[] = { args..., 0 }; // "a2" contains elements "{1, 3, 5, 7, 0}"; print_array(a2); // 1 3 5 7 0 int a3[sizeof...(args) + 2] = { -1, args..., -1 }; // "a3" is "int[6]{-1, 1, 3, 5, 7, -1}"; print_array(a3); // -1 1 3 5 7 -1 int a4[] = { square(args)... }; // "a4" contains "{1, 9, 25, 49}"; print_array(a4); // 1 9 25 49 } int main() { foo(1, 3, 5, 7); } * Örnek 2.0, Usage in Inheritence #include template class Base { public: Base() { std::cout << typeid(Base).name() << '\n'; } }; template class Der : Base { public: Der() { std::cout << typeid(Der).name() << '\n'; } }; int main() { /* # OUTPUT # class Base class Der */ Der myder; } * Örnek 2.1, #include template class Base { public: Base() { std::cout << typeid(Base).name() << '\n'; } }; template class Der : Base { public: Der() { std::cout << typeid(Der).name() << '\n'; } }; int main() { /* # OUTPUT # class Base class Der */ Der myder; } * Örnek 2.2, #include template class Base { public: Base() { std::cout << typeid(Base).name() << '\n'; } }; template class Der : Base... { public: Der() { std::cout << typeid(Der).name() << '\n'; } }; int main() { /* # OUTPUT # class Base class Base class Base class Der */ Der myder; } * Örnek 2.3.0, Usage in Multiple Inheritence. More than one base class was used. #include #include struct A { A(int x = 0) { std::cout << "A(" << x << ")\n"; } }; struct B { B(int x = 0) { std::cout << "B(" << x << ")\n"; } }; struct C { C(int x = 0) { std::cout << "C(" << x << ")\n"; } }; template struct ABC : public Ts... { ABC(int x) { std::cout << "ABC(" << x << ")\n"; } }; int main() { /* # OUTPUT # A(0) B(0) C(0) ABC(1993) */ ABC abc(1993); } * Örnek 2.3.1, #include #include struct A { A(int x = 0) { std::cout << "A(" << x << ")\n"; } void fA() const { std::cout << "fA()\n"; } }; struct B { B(int x = 0) { std::cout << "B(" << x << ")\n"; } void fB() const { std::cout << "fB()\n"; } }; struct C { C(int x = 0) { std::cout << "C(" << x << ")\n"; } void fC() const { std::cout << "fC()\n"; } }; template struct ABC : public Ts... { ABC(int x) { std::cout << "ABC(" << x << ")\n"; } }; int main() { /* # OUTPUT # A(0) B(0) C(0) ABC(1993) fA() fB() fC() */ ABC abc(1993); abc.fA(); abc.fB(); abc.fC(); } * Örnek 2.3.2, #include #include struct A { A(int x) { std::cout << "A(" << x << ")\n"; } }; struct B { B(int x) { std::cout << "B(" << x << ")\n"; } }; struct C { C(int x) { std::cout << "C(" << x << ")\n"; } }; template struct ABC : public Ts... { // -----> (1), Uncomment the code below: -----> (2) ABC(int x) /* :Ts{x}... */ { std::cout << "ABC(" << x << ")\n"; } }; int main() { /* // -----> (2), The output'd be: # OUTPUT # A(1993) B(1993) C(1993) ABC(1993) */ // no appropriate default constructor available for "A", "B", "C" // -----> (1) ABC abc(1993); } * Örnek 2.3.3, #include #include struct X { X(int i) { std::cout << "X(int i) i = " << i << '\n'; } }; struct Y { Y(int i) { std::cout << "Y(int i) i = " << i << '\n'; } }; struct Z { Z(int i) { std::cout << "Z(int i) i = " << i << '\n'; } }; template struct XYZ : public Types... { // Yukarıdaki paket açılımı ile "XYZ" sınıfı birden fazla // taban sınıfa sahip olabilir. XYZ() : Types{ 31 }... { // Yukarıdaki paket açılımında, taban sınıfın "ctor." fonksiyonlarına // "31" değeri gönderilecektir. } }; int main() { XYZ ax; //X(int i) i = 31 //Y(int i) i = 31 //Z(int i) i = 31 using type = decltype(ax); static_assert( std::is_base_of_v && std::is_base_of_v && std::is_base_of_v ); // Holds } * Örnek 2.3.4.0, Görüldüğü üzere taban sınıflarda bulunan fonksiyonlar türemiş sınıfa da gelmiş oldu. #include #include struct X { void fX(int x) { std::cout << "X::fX(int x) x = " << x << '\n'; } }; struct Y { void fY(double x) { std::cout << "Y::fY(double x) x = " << x << '\n'; } }; struct Z { void fZ(char x) { std::cout << "Z::fZ(char x) x = " << x << '\n'; } }; template struct XYZ : public Types... { }; int main() { /* # OUTPUT # X::fX(int x) x = 31 Y::fY(double x) x = 3.1 Z::fZ(char x) x = u */ XYZ ma; ma.fX(31); ma.fY(3.1); ma.fZ('u'); } * Örnek 2.3.4.1, Pekiyi taban sınıflardaki fonksiyonların isimleri aynı fakat parametreleri farklı olsaydı ne olacaktı? Tabii ki "ambiguity". Çünkü üç isim de türemiş sınıfta görünür vaziyettedir. #include #include struct X { void foo(int x) { std::cout << "X::foo(int x) x = " << x << '\n'; } }; struct Y { void foo(double x) { std::cout << "Y::foo(double x) x = " << x << '\n'; } }; struct Z { void foo(char x) { std::cout << "Z::foo(char x) x = " << x << '\n'; } }; template struct XYZ : public Types... { // -----> (1) // using Types::foo...; // Uncomment this code to compile. }; int main() { /* # OUTPUT # X::foo(int x) x = 31 Y::foo(double x) x = 3.1 Z::foo(char x) x = u */ XYZ ma; // error C2385: ambiguous access of 'foo' ma.foo(31); // -----> (1) // error C2385: ambiguous access of 'foo' ma.foo(3.1); // -----> (1) // error C2385: ambiguous access of 'foo' ma.foo('u'); // -----> (1) } * Örnek 2.4, #include #include template class Var {}; template class Myclass : public Var { public: static constexpr size_t size = sizeof...(Types); }; int main() { constexpr auto n = Myclass::size; // 2 static_assert(std::is_base_of_v, Myclass>); // Holds } * Örnek 2.5, #include #include template class Var { public: Var() { std::cout << typeid(Var).name() << '\n'; } }; template class Myclass : public Var { public: }; int main() { Myclass m; // class Var } * Örnek 3.0, Printing type names: #include #include template void my_tuple(A p1, B p2, OtherTypes... pOthers) { std::tuple the_tuple{p1, p2, pOthers...}; std::cout << "<" << typeid(the_tuple).name() << '\n'; std::cout << "<" << typeid(std::get<0>(the_tuple)).name() << "," << typeid(std::get<1>(the_tuple)).name() << "," << typeid(std::get<2>(the_tuple)).name() << "," << typeid(std::get<3>(the_tuple)).name() << "," << typeid(std::get<4>(the_tuple)).name() << "," << ">"; std::cout << "\n"; std::cout << "<" << std::get<0>(the_tuple) << "," << std::get<1>(the_tuple) << "," << std::get<2>(the_tuple) << "," << std::get<3>(the_tuple) << "," << std::get<4>(the_tuple) << "," << ">"; } int main() { /* # OUTPUT # <2,3.4,Q,14,5.6,> */ my_tuple(2, 3.4, 'Q', 14, 5.6); } * Örnek 3.1, #include #include template void my_tuple(A p1, B p2, OtherTypes... pOthers) { std::tuple the_tuple{p1, p2, pOthers...}; std::cout << "<" << typeid(the_tuple).name() << '\n'; std::cout << "<" << typeid(std::get<0>(the_tuple)).name() << "," << typeid(std::get<1>(the_tuple)).name() << "," << typeid(std::get<2>(the_tuple)).name() << "," << typeid(std::get<3>(the_tuple)).name() << "," << typeid(std::get<4>(the_tuple)).name() << "," << ">"; std::cout << "\n"; std::cout << "<" << std::get<0>(the_tuple) << "," << std::get<1>(the_tuple) << "," << std::get<2>(the_tuple) << "," << std::get<3>(the_tuple) << "," << std::get<4>(the_tuple) << "," << ">"; } int main() { /* # OUTPUT # <2,3,14.56,14,5.6,> */ my_tuple(2, 3.4, 14.56f, 14, 5.6); } * Örnek 3.2, #include #include template void print_type(const T&) { std::cout << typeid(T).name() << '\n'; } template void func(A arg1, B arg2, Ts... args) { std::tuple t1; // std::tuple t1; print_type(t1); // class std::tuple std::tuple t2; // std::tuple t1; print_type(t2); // class std::tuple std::tuple t3; // std::tuple t1; print_type(t3); // class std::tuple } int main() { // void func // (A,B,double,long,char) func(1, 1.2f, 3.4, 5L, 'A'); } * Örnek 3.3, #include template void print_type(const T&) { std::cout << typeid(T).name() << '\n'; } template class Myclass { public: Myclass() { print_type(this); } Myclass(Args...) { print_type(this); } }; template void func(Ts... args) { Myclass m1; Myclass m2(args...); } template void foo(Ts&&... args) { Myclass m3(std::forward(args)...); } int main() { foo(17, 1.7, 1993LL, 19.93f, "UlyaYuruk"); // class Myclass std::cout << "\n=====================\n"; func(17, 1.7, 1993LL, 19.93f, "UlyaYuruk"); //class Myclass //class Myclass } * Örnek 3.4, #include template struct A { A() { std::cout << typeid(A).name() << '\n'; } }; template void func() { A ax; } int main() { func<1, 2, 3, 4>(); // struct A<1,2,3,4> } * Örnek 3.5, #include #include #include template struct Nec { // "mx" değişkeninin değeri, parametre paketindekilerin // toplamı kadar olacaktır. Nec(Ts... args) : mx{ (... + args) } {} // Parametre paketindeki türler "int", "long", "double" olması durumunda // "mx" değişkeninin türü, bu üç türün "common_type" türü olan "double" // olacaktır. std::common_type_t mx; }; template void foo(Ts...args) { // Yine "a" dizisinin türü, parametre paketinde "int", "long", "double" // olması halinde, "double" olacaktır. "sizeof..." ifadesiyle de bizler // parametre paketindeki eleman sayısını temin etmiş olacağız. Diğer // yandan "a" dizisine ilk değer veren "args..." ifadesiyle de bir // "initializer_list" oluşturmuş oluyoruz. std::array, sizeof...(Ts)> a{ args... }; std::cout << "Type: " << typeid(a).name() << '\n'; // ----->(1), Type: class std::array // ----->(2), Type: class std::array Nec nec1(args...); std::cout << "Type: " << typeid(nec1).name() << '\n'; // ----->(1), Type : struct Nec // ----->(2), Type : struct Nec std::cout << "nec1.mx: " << nec1.mx << '\n'; // ----->(1), nec1.mx : 60 // ----->(2), nec1.mx : 60 Nec nec2(--args...); std::cout << "Type: " << typeid(nec2).name() << '\n'; // ----->(1), Type : struct Nec // ----->(2), Type : struct Nec std::cout << "nec2.mx: " << nec2.mx << '\n'; // ----->(1), nec2.mx : 57 // ----->(2), nec2.mx : 57 } int main() { foo(10, 20L, 30.); // ----->(1) foo(10, 20, 30); // ----->(2) } * Örnek 3.6, #include template struct Neco { // This member template's parameter type is // "Non-Type Template Parameter". template struct Nested { }; }; using type_encloser = Neco; // Error: type name is not allowed. // using type_nested = type_encloser::Nested; using type_nested = type_encloser::Nested<10, 4.5, 56L, 'A'>; int main() { std::cout << typeid(type_encloser).name() << '\n'; // struct Neco std::cout << typeid(type_nested).name() << '\n'; // struct Neco::Nested<10, 4.500000, 56, 65> } * Örnek 4, Şablon parametresi birden fazla parametre paketi kullanılabilir. #include template void func(Types(&...args)[N]) { std::cout << __FUNCSIG__ << '\n'; } int main() { char a[1]{}; func(a); // void func(char(&)[1]) unsigned char b[2]{}; func(b); // void func(unsigned char(&)[2]) signed char c[3]{}; func(c); // void func(signed char(&)[3]) float d[4]{}; func(d); // void func(float(&)[4]) double e[5]{}; func(e); // void func(double(&)[5]) long double f[6]{}; func(f); // void func(long double(&)[6]) int g[7]{}; func(g); // void func(int(&)[7]) unsigned int h[8]{}; func(h); // void func(unsigned int(&)[8]) long int i[9]{}; func(i); // void func(long(&)[9]) long long j[10]{}; func(j); // void func<__int64, 0xa>(__int64(&)[10]) std::cout << "\n======================\n"; func(a, b, c, d, e, f, g, h, i, j); /* void func < char, unsigned char, signed char, float, double, long double, int, unsigned int, long, __int64, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa > ( char(&)[1], unsigned char(&)[2], signed char(&)[3], float(&)[4], double(&)[5], long double(&)[6], int(&)[7], unsigned int(&)[8], long(&)[9], __int64(&)[10] ) */ } >>> "Compile Time Recursivity" : Derleme zamanında "recursive" biçimde derleyiciye kod yazdırtmaktır. Fakat burada bizim "base" bir durumu da belirtmemiz gerekiyor ki derleyici sonsuza kadar kod yazmaya devam etmesin. * Örnek 1, Bu yönteme ilişkin en tipik örnek, "n" tane argümanın yazdırılmasıdır. #include template void print(const T& r) { std::cout << r << '\n'; } template void print(const T& r, const TemplateArgs&... elements) { print(r); print(elements...); } int main() { /* # OUTPUT # a b c -1 1 10 1 1 10 Ulya Yuruk */ signed char a = 'a'; char b = 'b'; unsigned char c = 'c'; int d = -1; unsigned int e = 1; long long f = 10LL; float g = 1.f; double h = 1.f; long double i = 10.L; const char* name = "Ulya Yuruk"; print(a, b, c, d, e, f, g, h, i, name); } >>> "initializer list": Burada ise "std::initializer_list" ve "," operatörünün yoğun kullanımı mevcuttur. Anımsanacağı üzere "," operatörünün ürettiği değer, sağındaki ifadedir. Fakat solundaki ifadeyi de öncesinde işletir. * Örnek 1, #include #include template void print(const Args& ...args) { (void)std::initializer_list{ (std::cout << args << '\n', 0)... }; // Burada "," operatörünün solundaki "std::cout << args << ' '" // ifadesi işletildikten sonra "discard" edilecek ve geri dönüş değeri // olarak "0" ifadesini döndürecek. Dolayısıyla paket açılırken öğeler // ekrana yazdırılacak, sonrasında "0" değeri "initializer_list" e eklenecek. // Dolayısıyla paketteki son öğe de açılıp değeri ekrana yazıldıktan sonra, // paketteki öğe sayısınca "0" değeri "initializer_list" içerisinde olacak. // Fakat bu geçici nesne olduğundan, hayatı da sona erecek. } int main() { /* # OUTPUT # a b c -1 1 10 1 1 10 Ulya Yuruk */ signed char a = 'a'; char b = 'b'; unsigned char c = 'c'; int d = -1; unsigned int e = 1; long long f = 10LL; float g = 1.f; double h = 1.f; long double i = 10.L; const char* name = "Ulya Yuruk"; print(a, b, c, d, e, f, g, h, i, name); } >>> "Fold Expression" : C++17 ile dile eklenmiştir. İfade parantez içerisinde olmak zorundadır. "Binary Operators" için kullanılan bir sentakstır. İki farklı kategoriye ayrılır; "Unary Fold" ve "Binary Fold". Her iki yöntemde de "Left" ve "Right" kavramı vardır. Dolayısıyla kullanılan operatörler sonucuna belirlenmesinde bir fark oluşturmazken, bazı operatörler oluşturabilir. Dolayısıyla dikkatli olmalıyız. Öte yandan parametre paketinin boş olması "Unary Fold" için sentaks hatası oluşturur eğer kullanılan operatör "&&", "||" ve "," operatörlerinden BİRİ DEĞİLSE. Bu üç operatörünün kullanıldığı durumlarda, parametre paketinin boş olması, "Unary Fold" için SENTAKS HATASI OLUŞTURMAZ. Yine paketin boş olması durumunda, bu operatörlerden "&&" ve "||" sırasıyla "true" ve "false" değer döndürürken, "," operatörü ise "void" türden bir ifade OLUŞTURUR. Hakeza buradaki "&&" ve "||" operatörleri yine kısa devreye sebep olacaktır. "Binary Fold" için operatör kullanımına ilişkin böyle bir kural yoktur, parametre paketinin boş olması bir sentaks hatasına yol açmaz. >>>> "Unary Fold" : Herhangi bir "Binary Operator" ü ele alalım. Bu operatörün bir operandını "...", diğer operandını da parametre paketi yapıyoruz. Bu durumda "...", operatörün solunda ise, ilgili "Fold Expression" için "Unary Left Fold", sağında ise "Unary Right Fold" denir. >>>>> "Unary Left Fold": * Örnek 1, "+" operatörünü ele alalım. Parametre paketinde ise "p1", "p2", "p3" ve "p4" elemanları olsun. "(... + args)" ifadesinin açılımı bir nevi şöyledir: "(((p1+p2) + p3) + p4)" >>>>> "Unary Right Fold": * Örnek 1, "+" operatörünü ele alalım. Parametre paketinde ise "p1", "p2", "p3" ve "p4" elemanları olsun. "(args + ...)" ifadesinin açılımı bir nevi şöyledir: "(p1 + (p2 + (p3+p4)))" >>>> "Binary Fold" : "Unary Fold" da olduğu gibi "Left" ve "Right" kavramı burada da vardır. Bu yöntemde ise ilgili operatör iki defa yazılır ve bu iki operatörün arasına "..." gelir. Operatörlerin ikisi de AYNI OLMAK ZORUNDADIR. >>>>> "Binary Left Fold": * Örnek 1, "+" operatörünü ele alalım. Parametre paketinde ise "p1", "p2", "p3" ve "p4" elemanları olsun. "(init + ... + args)" Buradaki "init" ifadesi olarak da "0" değerini kullanalım. Dolayısıyla ifadenin açılımı bir nevi şöyledir: "((((0+p1) + p2) + p3) + p4)" >>>>> "Binary Right Fold": * Örnek 1, "+" operatörünü ele alalım. Parametre paketinde ise "p1", "p2", "p3" ve "p4" elemanları olsun. "(args + ... + init)" Buradaki "init" ifadesi olarak da "0" değerini kullanalım. Dolayısıyla ifadenin açılımı bir nevi şöyledir: "(p1 + (p2 + (p3 + (p4+0))))" Açılımlardan da görüleceği üzere "init" ifadesi, "initializer" olarak işlev görmekte. Yani bir nevi başlangıç noktası gibi. Özetle "..." atomunun "args" ifadesinin sağında yer alıyorsa "Right", solunda yer alıyorsa "Left" versiyonu işletilecektir. Şimdi de örneklere geçelim; * Örnek 1.0, Summing variables: #include template auto sum(const Args& ...args) { return (args + ...); } int main() { /* # OUTPUT # a b c -1 1 10 1 1 10 Ulya Yuruk */ signed char a = 'a'; char b = 'b'; unsigned char c = 'c'; int d = -1; unsigned int e = 1; long long f = 10LL; float g = 1.f; double h = 1.f; long double i = 10.L; std::cout << sum(a, b, c, d, e, f, g, h, i); // 316 } * Örnek 1.1, #include #include template auto sum(Ts&&... args) { return (args + ...); // return (std::forward(args) + ...); // Sınıf türleri için önemli olabilir. } int main() { std::cout << sum(12, 4.5, 50L, 'a') << '\n'; // (12 + (4.5 + (50+97))) // 163.5 } * Örnek 1.2, #include #include template constexpr auto sum_left(Ts... args) { return (... + args); // Unary Left Fold } template constexpr auto sum_right(Ts... args) { return (args + ...); // Unary Right Fold } int main() { using namespace std::literals; { constexpr auto result_left= sum_left(1, 3, 5, 7, 9); std::cout << "Result of Left: " << result_left << '\n'; // Result of Left : 25 constexpr auto result_right = sum_right(9, 7, 5, 3, 1); std::cout << "Result of right: " << result_right << '\n'; // Result of right : 25 } std::cout << '\n'; { auto result_left = sum_left("Ulya"s, "Yuruk"s, "Uskudar", "Istanbul"); std::cout << "Result of Left: " << result_left << '\n'; // Result of Left : UlyaYurukUskudarIstanbul auto result_right = sum_right("Istanbul", "Uskudar", "Yuruk"s, "Ulya"s); std::cout << "Result of right: " << result_right << '\n'; // Result of right : IstanbulUskudarYurukUlya } std::cout << '\n'; { auto result_left = sum_left("Ahmet"s, "Kandemir", "Pehlivanli", "Istanbul"); std::cout << "Result of Left: " << result_left << '\n'; // Result of Left : AhmetKandemirPehlivanliIstanbul // auto sum_right // (std::string,const char *,const char *,const char *) // ERROR: error C2110: '+': cannot add two pointers // auto result_right = sum_right("Ahmet"s, "Kandemir", "Pehlivanli", "Istanbul"); // std::cout << "Result of right: " << result_right << '\n'; // Result of right : // Burada "Pehlivanli" ve "Istanbul" yazıları "pointer" türüne "decay" olacaktır. // İki göstericiyi bu şekilde toplayamadığımız için sentaks hatası alacağız. } } * Örnek 1.3, Summing variables using Variable Templates: #include template constexpr int Sum = (... + Vals); template constexpr int SumSquare = (... + (Vals * Vals)); int main() { std::cout << Sum<1> << '\n'; // 1 std::cout << Sum<1, 9> << '\n'; // 10 std::cout << Sum<1, 123, 321> << '\n'; // 445 std::cout << '\n'; std::cout << SumSquare<1> << '\n'; // 1 /* # Function Call # (1*1) */ std::cout << SumSquare<1, 9> << '\n'; // 82 /* # Function Call # ((1*1 + 9*9)) */ std::cout << SumSquare<1, 123, 321> << '\n'; // 118171 /* # Function Call # ((1*1 + 123*123) + 321*321) */ } * Örnek 2, Dividing variables: #include #include template auto f_div_r(Ts&&... params) { return (std::forward(params) / ...); // Unary Right Fold } template auto f_div_l(Ts&&... params) { return (... / std::forward(params)); // Unary Right Fold } int main() { std::cout << f_div_r(500, 50, 5, 2) << '\n'; // 20 // (500 / (50 / (5/2))) std::cout << f_div_l(500, 50, 5, 2) << '\n'; // 1 // (((500 / 50) / 5) / 2); } * Örnek 3, Counting words: #include #include #include #include #include template auto matches(const C& range, const Ts& ...params) { return ( std::count( begin(range), end(range), params ) + ... ); /* // -----> (1) # Function Call # ( (std::cout(begin(), end(), p1) + (std::cout(begin(), end(), p2) + (std::cout(begin(), end(), p3) + (std::cout(begin(), end(), p4) + std::cout(begin(), end(), p5))))) ); // -----> (2) # Function Call # ( (std::cout(begin(), end(), p1) + std::cout(begin(), end(), p2)) ); */ } int main() { std::vector ivec{ 23, 4, 78, 12, 9, 23, 4, 2, 13, 78, 12, 9, 23, 9, 23, 4 }; std::cout << matches(ivec, 4, 2, 13, 45, 120) << '\n'; // 5 // -----> (1) // Sırasıyla "4", "2", "13", "45" ve "120" rakamlarını // "ivec" içerisinde kaçar adet olduğuna bakacak. Sonuç // olarak da bu adetlerin toplam değerini bize döndürecek. // "4" : 3 // "2" : 1 // "13" : 1 // "45" : 0 // "120" : 0 // Toplam: 5 std::cout << "Enter a sentence: "; std::string str; std::getline(std::cin, str); std::cout << matches(str, 'u', 'y') << '\n'; // 4 // -----> (2) // Girilen cümledeki "u" ve "y" harflerinin toplam // kaç adet olduğunu geri döndürecektir. // Ulya Yuruk Uskudar // "u" : 3 // "y" : 1 // Toplam: 4 } * Örnek 4.0, "Unary Fold" için "&&", "||" ve "," operatörlerinin kullanımı: #include #include auto fold_and(auto... args) { return (args && ...); } auto fold_or(auto... args) { return (args || ...); } auto fold_comma(auto... args) { return (args , ...); } int main() { std::boolalpha(std::cout); std::cout << fold_and() << '\n'; // True std::cout << fold_or() << '\n'; // False static_assert(std::is_void_v); // Holds } * Örnek 4.1, #include #include constexpr auto all(std::convertible_to auto... args) { return (... && args); // (((p1 && p2) && p3) && p4) } int main() { constexpr bool result = all(true, 10 < 20, 5 == 5); // true } * Örnek 4.2, #include template bool all_in_range(const T& min, const T& max, Args&&... params) { /* // Sınıflar söz konusu olduğunda işlevsel olabilir: return ( ( min <= std::forward(params) && max >= std::forward(params) ) && ... ); */ return ( ( min <= params && max >= params ) && ... ); } int main() { std::boolalpha(std::cout); std::cout << all_in_range(1, 20, 2, 4, 6, 7, 9) << '\n'; // True // "2, 4, 6, 7, 9" değerleri, "[1,20]" aralığında mı? std::cout << all_in_range(10, 200, 20, 40, 60, 70, 900) << '\n'; // False // "20, 40, 60, 70, 900" değerleri, "[10,200]" aralığında mı? } * Örnek 4.3, #include #include #include template bool insert(S& r_set, Args&&... args) { return ( r_set.insert( std::forward(args) ).second && ... ); /* Burada "S" için "std::set" geldiğini varsayarsak; * "std::set" in ".insert()" fonksiyonu bir "std::pair" * döndürmektedir. Bu "std::pair" in "first" ve "second" * değerleri sırasıyla; * Ekleme yapılmışsa, eklenen öğeye ilişkin iteratör ve "true" * değerlerine sahip olur. * Ekleme yapılmamışsa, halihazırdaki öğeye ilişkin iteratör ve * "false" değerlerine sahip olur. */ } int main() { std::boolalpha(std::cout); std::set my_set{ 34, 56, 98 }; std::cout << "Size: " << my_set.size() << '\n'; // Size: 3 std::cout << insert(my_set, 17, 9, 1993) << '\n'; // true std::cout << "Size: " << my_set.size() << '\n'; // Size : 6 std::cout << insert(my_set, 24, 7, 1995) << '\n'; // true std::cout << "Size: " << my_set.size() << '\n'; // Size : 9 std::cout << insert(my_set, 10, 11, 1995) << '\n'; // false std::cout << "Size: " << my_set.size() << '\n'; // Size : 11 } * Örnek 4.4, #include #include #include void print(const auto&... args) { std::cout << "Function Signature: " << __FUNCSIG__ << '\n' << "Values: "; ((std::cout << args << ' '), ...); } int main() { std::string name{ "Ulya Yuruk, Istanbul" }; std::bitset<16> bs{ 34567u }; print(28.2, name, bs, "Uskudar", 1995); /* // Function Signature: void print < double, class std::basic_string< char, struct std::char_traits, class std::allocator >, class std::bitset<16>, char[8], int > ( const double &, const class std::basic_string< char, struct std::char_traits, class std::allocator > &, const class std::bitset<16> &, const char (&)[8], const int & ) // Result: Values: 28.2 Ulya Yuruk, Istanbul 1000011100000111 Uskudar 1995 */ } * Örnek 4.5, #include #include constexpr int sum(std::same_as auto... args) { int result{}; ((result += args), ...); return result; } int main() { constexpr auto x = sum(17, 9, 1993, 10, 11, 1995); // 4035 } * Örnek 4.6.0, #include template void print_impl(T x) { std::cout << "print_impl(" << x << ")\n"; } template void print(Ts... args) { (print_impl(args), ...); } int main() { // OUTPUT //print_impl(1) //print_impl(2) //print_impl(3) //print_impl(4) //print_impl(5) print(1, 2, 3, 4, 5); } * Örnek 4.6.1, #include #include #include #include // Check if "T" has ".begin()" and ".end()" functions. template concept hasIterator = requires (T a) { a.begin(); a.end(); }; template void impl(const T& x) { if constexpr (hasIterator) { std::cout << "["; for (auto& elem : x) std::cout << elem << ' '; std::cout << "]"; } else if constexpr (std::is_pointer_v) std::cout << *x; else std::cout << x; std::cout << ' '; } void print(const auto&... args) { /* # Function Call # * (impl(p5), (impl(p4), (impl(p3), (impl(p2), impl(p1))))) */ ((impl(args)), ...); } int main() { std::array arr{ 9, 8, 7 }; std::vector vec{ 1.5, 2.0, 2.5, 3.0 }; int* pi{ new int{13} }; print(345, 4.5, arr, vec, pi); // 345 4.5 [9 8 7 ] [1.5 2 2.5 3 ] 13 delete pi; } * Örnek 4.7, #include template void fold_print(First&& f, Args&&... args) { std::cout << f; auto print_with_comma = [](const auto& v) { std::cout << ", " << v; }; (..., print_with_comma(std::forward(args))); std::cout << '\n'; } int main() { fold_print("Ulya", 10, 11, 1995, "Yuruk"); // Ulya, 10, 11, 1995, Yuruk } * Örnek 4.8.0, #include #include template void push_back_right(std::vector& v, Args&&... args) { (v.push_back(std::forward(args)), ...); } template void push_back_left(std::vector& v, Args&&... args) { (..., v.push_back(std::forward(args))); } int main() { std::vector ivec{}; push_back_right(ivec, 10, 11, 1995); for (auto i : ivec) std::cout << i << ' '; // 10 11 1995 std::cout << '\n'; push_back_left(ivec, 17, 9, 1993); for (auto i : ivec) std::cout << i << ' '; // 10 11 1995 17 9 1993 std::cout << '\n'; } * Örnek 4.8.1, #include #include template decltype(auto) push_back(std::vector& vec, Ts&&... args) { (..., vec.push_back(std::forward(args))); return (vec); } int main() { std::vector ivec; push_back(ivec, 10, 11, 1995, 17, 9, 1993).push_back(2024); for (auto i : ivec) std::cout << i << ' '; // 10 11 1995 17 9 1993 2024 } * Örnek 4.9.0, A hash for our custom class. #include #include #include struct Myclass { int mx; double my; std::string mz; }; // Way-I: A hasher for our custom class. template<> struct std::hash { std::size_t operator()(const Myclass& other) { // We defined this function according to our desire, or known algorithms. return std::hash{}(other.mx) / std::hash{}(other.my) + std::hash{}(other.mz); } }; //// Way-II: A hasher class for our custom class. //struct MyclassHasher { // std::size_t operator()(const Myclass& other) { // // We defined this function according to our desire, or known algorithms. // // return std::hash{}(other.mx) / std::hash{}(other.my) + std::hash{}(other.mz); // } //}; int main() { std::cout << "Hash Value of 31: " << std::hash{}(31) << '\n'; // Hash Value of 31: 3744932218 std::cout << "Hash Value of 3.1: " << std::hash{}(3.1) << '\n'; // Hash Value of 3.1 : 2791122276 std::string name{ "Ulya Yuruk" }; std::cout << "Hash Value of name: " << std::hash{}(name) << '\n'; // Hash Value of name : 891913182 Myclass mx; std::cout << "Hash Value of mx: " << std::hash{}(mx) << '\n'; // Hash Value of mx : 2166136262 } * Örnek 4.9.1, #include #include #include template void hashCombine(std::size_t& seed, const T& val) { seed ^= std::hash()(val) + 0x9e3779b9 + (seed << 6) + (seed >> 2); } template std::size_t combinedHashValue(const Ts&... args) { std::size_t seed = 0; // Initial seed value. (..., hashCombine(seed, args)); // Chain call to hashCombine(). return seed; } struct Myclass { int mx; double my; std::string mz; }; // Way-I: A hasher for our custom class. template<> struct std::hash { std::size_t operator()(const Myclass& other) { // We defined this function according to our desire, or known algorithms. return combinedHashValue(other.mx, other.my, other.mz); } }; //// Way-II: A hasher class for our custom class. //struct MyclassHasher { // std::size_t operator()(const Myclass& other) { // // We defined this function according to our desire, or known algorithms. // return combinedHashValue(other.mx, other.my, other.mz); // } //}; int main() { std::cout << "Hash Value of 31: " << std::hash{}(31) << '\n'; // Hash Value of 31: 3744932218 std::cout << "Hash Value of 3.1: " << std::hash{}(3.1) << '\n'; // Hash Value of 3.1 : 2791122276 std::string name{ "Ulya Yuruk" }; std::cout << "Hash Value of name: " << std::hash{}(name) << '\n'; // Hash Value of name : 891913182 Myclass mx; std::cout << "Hash Value of mx: " << std::hash{}(mx) << '\n'; // Hash Value of mx : 4276989417 } * Örnek 4.9.2, #include #include #include template inline void hash_combine(std::size_t& seed, const T& val) { seed ^= std::hash()(val) + 0x9e3779b9 + (seed << 6) + (seed >> 2); } template inline void hash_val(std::size_t& seed, const T& val) { hash_combine(seed, val); } template inline void hash_val(std::size_t& seed, const T& value, const Ts&... args) { hash_combine(seed, value); hash_val(seed, args...); } template inline std::size_t hash_val(const Ts& ... args){ std::size_t seed = 0; hash_val(seed, args...); return seed; } struct Myclass { int mx; double my; std::string mz; }; // A hasher for our custom class. template<> struct std::hash { std::size_t operator()(const Myclass& other) { // We defined this function according to our desire, or known algorithms. return hash_val(other.mx, other.my, other.mz); } }; int main() { std::cout << "Hash Value of 31: " << std::hash{}(31) << '\n'; // Hash Value of 31: 31 std::cout << "Hash Value of 3.1: " << std::hash{}(3.1) << '\n'; // Hash Value of 3.1: 8546506192830196718 std::string name{ "Ulya Yuruk" }; std::cout << "Hash Value of name: " << std::hash{}(name) << '\n'; // Hash Value of name: 5884387088948685134 Myclass mx; std::cout << "Hash Value of mx: " << std::hash{}(mx) << '\n'; // Hash Value of mx: 6142520555506922560 } * Örnek 4.10, Comparison. #include #include template... Ts> constexpr auto min(const T& a, const T& b, const Ts& ... args) { auto m = a < b ? a : b; if constexpr (sizeof...(args) > 0) { auto cmp = [&](const auto& value){ if (value < m) m = value; }; (..., cmp(args)); } return m; } int main(){ constexpr auto res = min(10, 11, 1995, 17, 9, 1993); // 9 } * Örnek 5.0, Usage of "Binary Fold" #include #include #include template void print(Ts&&... args){ (std::cout << ... << std::forward(args)) << '\n'; } int main() { print( 1, 2, 3, std::string{"Ulya Yuruk"}, std::bitset<8>{28u} ); // 123Ulya Yuruk00011100 } * Örnek 5.1.0, #include template... Ts> constexpr int subtract_v1(int value, Ts... args){ return (value - ... - args); // (((value-p1)-p2)-p3) } template... Ts> constexpr int subtract_v2(int value, Ts... args){ return (args - ... - value); // (p1-(p2-(p3-value))) } int main() { constexpr auto result_v1 = subtract_v1(50, 20, 10, 15); // (((50-20)-10)-15) = 5 std::cout << result_v1 << '\n'; constexpr auto result_v2 = subtract_v2(50, 20, 10, 15); // (20-(10-(15-50))) = -25 std::cout << result_v2 << '\n'; } * Örnek 5.1.1, #include constexpr int subtract_v1(int value, std::same_as auto... args){ return (value - ... - args); // ((((value-p1)-p2)-p3) } constexpr int subtract_v2(int value, std::same_as auto... args){ return (args - ... - value); // (p1-(p2-(p3-value))) } int main() { constexpr auto result_v1 = subtract_v1(50, 20, 10, 15); // (((50-20)-10)-15) = 5 std::cout << result_v1 << '\n'; constexpr auto result_v2 = subtract_v2(50, 20, 10, 15); // (20-(10-(15-50))) = -25 std::cout << result_v2 << '\n'; } * Örnek 5.2, #include // Way - I template constexpr auto sum(Ts&&... args){ return (0 + ... + std::forward(args)); } // Way - II // template // constexpr auto summ(const T& base_value = 0, Ts&&... args){ // return (base_value + ... + std::forward(args)); // } // Way - III // template // constexpr auto summ(const T& base_value = 0, std::same_asauto&&... args){ // return (base_value + ... + std::forward(args)); // } int main() { constexpr auto x1 = sum(1, 3, 5, 7, 9); // 25 constexpr auto x2 = sum(1, 3, 5, 7); // 16 constexpr auto x3 = sum(1, 3, 5); // 9 constexpr auto x4 = sum(1, 3); // 4 constexpr auto x5 = sum(1); // 1 constexpr auto x6 = sum(); // 0 } Şimdi de "Fold Expression" a ilişkin "idiom" lara bir göz atalım: >>>> Parametre paketindeki ilk argümanı elde etmek. * Örnek 1, #include #include #include template auto get_first_element(Ts... args) { std::common_type_t result; // ---> (1) : std::common_type_t result; // ---> (2) : std::common_type_t result; ((result = args, true) || ...); // ---> (1) : ((result = p1, true) || (result = p2, true)) // "(result = p1, true)" ifadesi, paketteki ilk elemanı "result" değişkenine // atayacak. Fakat ifadenin geri döndürdüğü değer ise "true" değeri olacak. "true" // olması hasebiyle "||" operatörü kısa devre mekanizmasını işletecek. Dolayısıyla // açılımın geri kalanına bakılmayacak. // ---> (2) : ((result = p1, true) || (result = p2, true)) return result; } int main(){ auto x = get_first_element(2, 4.5); // ---> (1) auto y = get_first_element(1995, 2005); // --->(2) } * Örnek 2, "std::tuple" kullanarak. #include #include template auto get_first_element(Ts... args) { // std::tuple my_tuple{args...}; std::tuple my_tuple{args...}; return std::get<0>(my_tuple); } int main(){ auto x = get_first_element(2, 4.5, "Ahmet", 17, 1995, 4654.789); // ---> (1) std::cout << "x: " << x << '\n'; // x: 2 } >>>> Parametre paketindeki son argümanı elde etmek. * Örnek 1, #include template auto get_first_element(Ts... args) { return (args, ...); } int main(){ auto x = get_first_element(2, 4.5); // ---> (1) auto y = get_first_element(1995, 2005); // --->(2) } * Örnek 2, "std::tuple" kullanarak. #include #include template auto get_last_element(Ts... args) { // std::tuple my_tuple{args...}; std::tuple my_tuple{args...}; return std::get(my_tuple); } int main(){ auto z = get_last_element(199.5, 20.05, "Ulya", 789, 987u, "Y"); std::cout << "z: " << z << '\n'; // z: Y } >>>> Parametre paketindeki elemanları ters sırayla bir fonksiyona göndermek. * Örnek 1, #include void bar(int x) { std::cout << x << ' '; } void foo(auto... args){ int dummy; (dummy = ... = (bar(args), 0)); // # Function Call # /* Step - I: (((dummy = bar(p1), 0) = bar(p2), 0) = bar(p3), 0); Burada "=" operatöründen dolayı ilk olarak "bar(p3), 0)" ifadesi çağrılacak. Bu ifadenin geri dönüş değeri "0" olacak, çünkü "," operatöründen dolayı. ... */ } int main(){ foo(0, 1, 2); // 2 1 0 } >>>> Uygun şartları sağlayanların toplam adedini bulma: * Örnek 1, #include #include constexpr bool pred(int x) { return x % 3 == 0; } constexpr std::size_t pred_count(auto... args){ return (std::size_t{} + ... + (static_cast(pred(args)))); } int main(){ std::cout << pred_count(2, 18, 1, 3, 9, 0, 10, 12) << '\n'; // 5 } >>>> En küçük olanını bulma: * Örnek 1, #include #include auto fold_min(auto... args) { auto min = (args, ...); // Paketteki son öğe en küçük olarak seçildi. ((args < min ? min = args : 0), ...); // Daha sonra yukarıdaki ifade şu şekilde açılacak; // Buradaki "(args < min ? min = args : 0)" ifadesinin // sonucu; // if "args < min" is true, then "min = args". // else, then "0". return min; } int main(){ std::cout << fold_min(10, 11, 1995, 17, 9, 1993, 24, 8, 1995) << '\n'; // 8 } >>>> "Multi Lambda Idiom" : * Örnek 1, #include #include #include template struct MultiLambda : L... { // Parametre paketindekiler, taban sınıf olarak // ele alınacak; yani "multiple inheritence". using L::operator()...; // Taban sınıfların ".operator()" fonksiyonları // bu sınıf içerisinde görülür hale getirildi. // Aksi halde taban sınıfların hepsinde bu isim // aynı anda bulunacağından, "ambiguity" oluşacaktı. }; int main(){ // "MultiLambda" bir sınıf şablonu olduğundan, // şablon parametrelerini bildirmeliyiz. Fakat // "CTAD" devreye girdiğinden, gerek kalmaadı. // Argüman olarak "lambda" ifadeleri kullandığımızdan, // taban sınıf olarak bu ifadelere ilişkin "closure-type" // türler kullanılacak. Taban sınıfların hepsinde de // ".operator()()" olduğunu da unutmayalım. constexpr MultiLambda action{ [](int i){ std::cout << i << '\n'; }, [](double d){ std::cout << d << '\n'; }, [](bool b){ std::cout << std::boolalpha << b << '\n'; }, [](std::string s){ std::cout << s << '\n'; } }; action(12); // 12 // Burada ise parametresi "int" olan ".operator()" // fonksiyonu çağrılacaktır("Function Overload Resolution"). using namespace std::string_literals; std::tuple t(1, true, "Ulya Yuruk"s, 3.0); std::apply( [action](auto... v){ (action(v), ...);}, t ); // "apply" fonksiyonu argüman olarak bir "callable" ve bir // "std::tuple" nesnesi alır. "std::tuple" içerisindeki öğeleri // ilgili "callable" a gönderir. // # OUTPUT # // 1 // true // Ulya Yuruk // 3 } Şimdi de "Fold Expression" a ilişkin "utility" başlık dosyasındaki "Template Meta Programming" kavramına ilişkin araçlara değinelim. Bu araçlar C++14'e kadar uzanmaktadır. >>>> "integer_sequence" : Bir sınıf şablonudur. Birinci parametresi "Type Template Parameter", ikinci parametresi ise "non-type parameter pack". * Örnek 1, Temsili İmplementasonu: #include #include void foo(auto) {} template struct IntegerSequence { static constexpr std::size_t size = sizeof...(Vals); // Paketteki öğe sayısı. using ValueType = T; }; int main(){ IntegerSequence myIS; std::cout << myIS.size << '\n'; // 5 IntegerSequence::ValueType myIS_ValueType; std::cout << myIS_ValueType << '\n'; // 0 // Error: "IntegerSequence" ifadesi // bir tür belirttiği için sadece tür belirtebildiğim yerlerde kullanabilirim. // foo(IntegerSequence); } * Örnek 2, #include #include template void print_sequence(std::integer_sequence seq) { std::cout << "Types in the sequence consist of <" << typeid(decltype(seq)::value_type).name() << ">\n"; std::cout << "[" << seq.size() << "]: "; ((std::cout << vals << ' '), ...); } int main(){ print_sequence(std::integer_sequence{}); // // [6]: 10 11 1995 17 9 1993 } Diğer yandan bu sınıfa ilişkin tür eş isimleri de bildirilmiştir. Bunlardan sırasıyla "index_sequence", "make_integer_sequence ", "make_index_sequence " ve "index_sequence_for" isimleridir. * Örnek 1.0, "index_sequence" in Temsili İmplementasonu: Burada artık tür bilgisini belirtmiyoruz. #include #include template struct IntegerSequence { static constexpr std::size_t size = sizeof...(Vals); // Paketteki öğe sayısı. using ValueType = T; }; template using IndexSequence = IntegerSequence; int main(){ IndexSequence<1, 2, 3, 4, 5> myIS; std::cout << myIS.size << '\n'; // 5 IndexSequence<1, 2, 3, 4, 5, 7, 30, 1995>::ValueType myIS_ValueType; std::cout << myIS_ValueType << '\n'; // 0 } * Örnek 1.1, #include #include int main(){ std::index_sequence<1, 2, 3, 4, 5> myIS; std::cout << myIS.size() << '\n'; // 5 std::index_sequence<1, 2, 3, 4, 5, 7, 30, 1995>::value_type myIS_ValueType; std::cout << myIS_ValueType << '\n'; // 0 } * Örnek 1.2, #include #include template void print_sequence(std::integer_sequence seq) { std::cout << "[" << seq.size() << "]: "; ((std::cout << vals << ' '), ...); } int main(){ std::index_sequence<10, 2, 33, 4, 55> myIS; print_sequence(myIS); // [5]: 10 2 33 4 55 0 std::index_sequence<1, 2, 3, 4, 5, 7, 30, 1995>::value_type myIS_ValueType; std::cout << myIS_ValueType << '\n'; // 0 } * Örnek 2.0, "make_integer_sequence": Yine tür bilgisidir. Tür bilgisinin kullanıldığı yerlerde kullanılır. #include #include #include template void print_sequence(std::integer_sequence seq) { std::cout << "[" << seq.size() << "]: "; ((std::cout << vals << ' '), ...); } int main(){ // Holds static_assert( std::is_same_v, std::integer_sequence> ); // [7]: 0 1 2 3 4 5 6 print_sequence(std::make_integer_sequence{}); } * Örnek 3, "make_index_sequence" : Bu da "make_integer_sequence" in tür isminin yazılmadığı versiyonudur. #include #include #include template void print_sequence(std::integer_sequence seq) { std::cout << "[" << seq.size() << "]: "; ((std::cout << vals << ' '), ...); } int main(){ // Holds static_assert( std::is_same_v, std::integer_sequence> ); // [10]: 0 1 2 3 4 5 6 7 8 9 print_sequence(std::make_index_sequence<10>{}); } * Örnek 4, "index_sequence_for" : Farklı türleri kullanarak oluşturulur. #include #include template void print_sequence(std::integer_sequence seq) { std::cout << "[" << seq.size() << "]: "; ((std::cout << vals << ' '), ...); } int main(){ print_sequence(std::index_sequence_for{}); // [3]: 0 1 2 } Özetle burada amaç, sekanslara tür eş isim oluşturmaktır. Şimdi de bu tür eş isimlerinden faydalanarak yapabileceklerimize değinelim; * Örnek 1.0, "std::tuple" içerisindekileri bir çıkış akımına yazdırmak: #include #include #include #include template void print_tuple_impl(std::basic_ostream& os, const Tuple& t, std::index_sequence) { // --->(2) ((os << (Is == 0 ? "" : ", ") << std::get(t)), ...); // Yukarıdaki pattern: "(os << (Is == 0 ? "" : ", ") << std::get(t))". // İlgili ifade aşağıdaki gibi olacaktır: // ((((os << (p1) << std::get(t)), (os << (p2 == 0 ? "" : ", ") << std::get(t))), (os << (p3 == 0 ? "" : ", ") << std::get(t))), (os << (p4 == 0 ? "" : ", ") << std::get(t))) /* ((os << (Is == 0 ? "" : ", ") << std::get(t)), ...); ( ( os << (Is == 0 ? "" : ", ") << std::get(t) ), ... ); */ } // "std::tuple" nesnesini bir "steam" e yazdırır. template auto& operator<<(std::basic_ostream& os, const std::tuple& t) { os << '('; // --->(1) print_tuple_impl(os, t, std::index_sequence_for{}); // "std::index_sequence_for{}" ifadesi aslında // "std::index_sequence<0, 1, 2, 3>" ifadesine açılacak ki // bu da aslında "std::integer_sequence" // ifadesi demektir. Böylelikle bir indeks bilgisi elde etmiş olacağız. return os << ')'; } int main() { std::tuple tp{ 2.3, "necati", 'A', 555 }; // --->(0) std::cout << tp << '\n'; std::cout << std::make_tuple(2, 6, 1.1, "Yuruk"); } * Örnek 1.1, #include #include #include #include template void print_tuple_impl(std::basic_ostream& os, const Tuple& t, std::index_sequence) { ((os << (Is == 0 ? "" : ", ") << std::get(t)), ...); } template auto& operator<<(std::basic_ostream& os, const std::tuple& t) { os << '('; print_tuple_impl(os, t, std::index_sequence_for{}); return os << ')'; } template auto a2t_impl(const Array& a, std::index_sequence) { return std::make_tuple(a[I]...); // return std::make_tuple(a[0], a[1], a[2], a[3]); } // array to tuple conversion: template> auto a2t (const std::array& a) { return a2t_impl(a, Indices{}); // "Indices" is type of "std::index_sequence<0,1,2,3>". } int main() { std::array arr = {1, 2, 3, 4}; auto tp = a2t(arr); static_assert(std::is_same_v>); std::cout << tp << '\n'; // (1, 2, 3, 4) } * Örnek 2, #include #include template void f(Ts&&... args) { ((std::cout << args << ' '), ...); } template void process_impl(const Tuple& t, std::index_sequence) { f(std::get(t)...); } template void process(const Tuple& t) { // Way - I // constexpr auto t_size = std::tuple_size::value; // process_impl( t, std::make_index_sequence() ); // Way - II // process_impl( t, std::make_index_sequence::value>() ); // Way - III process_impl( t, std::make_index_sequence>() ); } int main() { std::tuple tuple(1, 'u', 2.3); process(tuple); // 1 u 2.3 } * Örnek 3, #include #include #include #include namespace detail { template decltype(auto) Apply_impl(F&& f, Tuple&& tpl, std::index_sequence) { return std::forward(f)( std::get(std::forward(tpl))... // This is the pattern. No Fold Expression, only Pack Expansion ); } } template decltype(auto) Apply(F&& f, Tuple&& tpl) { // Forwarding Reference return detail::Apply_impl( std::forward(f), std::forward(tpl), std::make_index_sequence>>{} ); } int f(int i, char c, double d) { std::cout << i << ' ' << c << ' ' << d << '\n'; return 1; } int main() { auto tp = std::make_tuple(42, 'x', 3.14); int ret = Apply(f, tp); // 42 x 3.14 std::cout << ret << '\n'; // 1 } * Örnek 4, #include #include #include #include #include #include namespace detail { template auto make_array_impl(const T& value, std::index_sequence) { return std::array{ (I,value)... }; // Not Fold Expression, but Pack Expansion // "sizeof...(I)" : Number of elements in the parameter pack. // Pattern => (I,value)... // "std::array{ (I,value)... };" will expand to: // "std::array{ (0,value), (1,value), (2,value), (3,value), (4,value) };" // Then the "," operator will yield "value". So, the next'd be: // "std::array{ 31, 31, 31, 31, 31 };" } } template std::array make_array(const T& value) { return detail::make_array_impl(value, std::make_index_sequence()); } int main() { auto arr = make_array<5>(31); for (const auto& i : arr) std::cout << i << '\n'; std::cout << '\n'; } * Örnek 5, #include #include #include #include template decltype(auto) tuple_subset(const Tuple& t, std::index_sequence) { return std::make_tuple(std::get(t)...); // Pattern: "std::get(t)" // return std::make_tuple( // std::get<0>(t), std::get<2>(t), std::get<3>(t) // ); } int main() { auto tp1 = std::make_tuple(11, 1.1, "Onbir", 111u, '1'); auto tp2 = tuple_subset(tp1, std::index_sequence<0, 2, 3>{}); static_assert(std::is_same_v>); std::cout << '<' << std::get<0>(tp2) << ',' << std::get<1>(tp2) << ',' << std::get<2>(tp2) << ">\n"; // <11,Onbir,111> } >> Şimdi de derleyicilerin bir şablon karşısındaki tutumlarını inceleyelim; >>> "Specialization" : Derleyicinin bir şablonu açması, yani onu "instantiate" etmesi demektir. Dolayısıyla bir şablonun bir tür(ler)e göre açılımını kastederken hem "Specialization" terimini hem de "Instantiation" terimini kullanabiliriz. Ancak buradaki "Specialization" kelimesini "Explicit(Full) Specialization" veya "Partial Specialization" kelimelerindeki "Specialization" kelimesi ile birbirine karıştırmayalım. Bir tanesi bir şablonun bir tür nezdindeki açılımını kastederken, diğeri ise bir şablonun özelleştirilmesine ilişkindir. Diğer yandan derleyici bir şablonu açarken, yani o şablondan bir "Specialization" üretirken, yani o şablonu "Instantiate" ederken, şu aşamalardan geçmektedir; >>>> "Name-Lookup" Süreci: >>>>> "Point of Instantiation": >>>>>> "Dependant Name" : Bu kavram o ismin, isim arama sırasında, şablon parametresine ilişkin olup olmadığını belirtmek için kullanılır. * Örnek 1, template void foo(T x) { bar(x); // Yukarıdaki fonksiyon çağrısı, "foo" çağrısı yapılmasa bile, // sentaks hatasına yol açmaz çünkü "x" ismi şablon parametresine // ilişkin bir isimdir. İşte "x" için "Dependant Name" kavramı // kullanılır. bar(12); // Ancak bu çağrı sentaks hatasına yol AÇACAKTIR. } * Örnek 2, #include template void foo(T x) { bar(x); // "x" is a dependent name. } class Neco{}; void bar(Neco) { std::cout << "bar(Neco)\n"; } void bar(int) { std::cout << "bar(int)\n"; } int main() { Neco mx; // Output => bar(Neco) foo(mx); // Because of "Point of Instantiation" // error: ‘bar’ was not declared in this scope, // and no declarations were found by argument-dependent lookup // at the point of instantiation. // note: ‘void bar(int)’ declared here, later in the translation unit // foo(10); } >>>> "Template-ID" Belirlenmesi: Burada ise şablon parametrelerinin hangi türler olduğununun saptandığı evredir. Bu belirleme sürecinde ise karşımıza üç farklı yol çıkmaktadır. "Explicit" olarak bizlerin türlerin hangi tür olduğunu belirtmesiyle, "Default Template Argument" kullanılması, fonksiyon çağrılarında kullanılan argüman(lar)dan hareketle tür çıkarımı yapılması("Template Argument Deduction"). * Örnek 1, "Explicit" olarak bizlerin belirtmesi: template void foo() {} int main() { foo(); } * Örnek 2, "Default Template Argument" kullanılması: template void foo() {} int main() { foo(); } * Örnek 3, "Template Argument Deduction": template void foo(T x) {} int main() { foo('a'); } >>>> "Substitution" Evresi: Artık fonksiyonun imzasının kesinleştiği evredir. Şablon parametrelerinin yerlerine yerleştirildiği evredir. Artık fonksiyonun parametrik yapısının tamamen belirlendiği evredir. Örneğin, geri dönüş türü belirlenir. Tabii bu yerleştirme aşamasında ilk olarak "explicit" olarak belirttiğimiz türler yerleştirilir, daha sonra diğerleri. Eğer bu aşamada geçersiz bir durum oluşursa, mesela bir türün olmaması gibi, derleyici doğrudan sentaks hatası vermek yerine, "Substitution Failure is not an Error", ilgili fonksiyonu "Function Overload Resolution" evresinde kullanılacak kümeden ÇIKARTIYOR. İşte "concepts" kavramı hayatımıza girmeden evvel şablonların kısıtlanması "Substitution Failure is not an Error(SFINAE)" yöntemi ile yapılmaktaydı. * Örnek 1, template typename T::value_type foo(T x) {} int main() { foo(12); // no matching function for call to ‘foo(char)’ /* * İlk olarak "foo" ismi arandı ve şablon ismi olduğu öğrenildi. ("Name Lookup") * Daha sonra fonksiyon çağrısındaki ifadeden yola çıkılarak "T" için "int" türü geldiği * anlaşıldı. ("Template-ID" Belirlenmesi) * Fonksiyonun geri dönüş değeri için "int" türünün "value_type" türü olduğu belirlendi * fakat böyle bir şey söz konusu olmadığından, ilgili fonksiyon yukarıda bahsedilen * kümeden çıkartıldı. ("Substitution") * Kümede başka bir fonksiyon olmadığından, programımız derleme hatası vermiştir. Eğer uygun * başka fonksiyonlar kümede olsaydı, "Function Overlaod Resolution" işlemi uygulanacaktı. */ } Diğer yandan "Instantiation" iki farklı biçimde yapılabilmektedir; "Explicit Instantiation" ve "Implicit Instantiation". >>>> "Explicit Instantiation": Derleyiciye açıkça, "Bu şablonu, bu şablon argümanıyla, aç" demektir. Kendi bünyesinde ayrı sentaks kuralları barındırır. İlerleyen vakitlerde değinilecektir. Bu yaklaşımı "Explicit Specialization" / "Partial Specialization" ile KARIŞTIRMAMALIYIZ. Bir tanesi şablonu açmak için kullanılan yöntemken, diğer bir şablonu özelleştirme yöntem(ler)idir. Sadece fonksiyon şablonlarına has değil; diğer şablon tipleri için de kullanılabilir. "Manuel Instantiate Talimatı" olarak da geçer. * Örnek 1.0, Yapılış biçimi aşağıdaki gibidir; #include template void foo(T x) { std::cout << typeid(T).name() << '\n'; } template void foo(int); // An Explicit Specialization for "T" is "int". template void foo<>(double); // Another Explicit Specialization for "T" is "double". template void foo(char); // Another Explicit Specialization for "T" is "char". int main() { //... } * Örnek 1.1, #include template struct Neco {}; template struct Neco; // An Explicit Specialization for "T" is "int". int main() { //... } * Örnek 1.2, #include template struct Neco { void foo() {} }; template void Neco::foo(); // An Explicit Specialization for "T" is "float". int main() { //... } * Örnek 2, // ========== file.hpp template void foo(); // A declaration. Preventing instantiation. // ========== file.tpp (".tpp" is another convension of ".hpp") #include "file.hpp" template void foo() { // A definition. // ... => // Implementation. } // ========== file.cpp #include "file.tpp " template void foo(); // An Explicit Instantiation. "T" will be "int". Diğer yandan şunu da hatırlatmakta fayda vardır. Bir başlık dosyasının "#include" edildiği her bir "Translation Unit", derlemeye tabii tutulmaktadır. Örneğin "file.hpp" başlık dosyası 100 farklı dosyada "#include" edilmiş olsun. Derleme aşamasında her bir dosya derlemeye tabii tutulacağından, iş bu başlık dosyası da 100 defa derlenmiş olacaktır. * Örnek 1.0.0, Aşağıdaki örnekte de görüleceği üzere "tpl_func()" iki farklı "object file" içerisinde de bulunmaktadır. // ----> (0), tpl_func.h: "tpl_func" fonksiyonunun tanımı yer almaktadır. template void tpl_func() { //... } // ----> (1), file_x.cpp: Bu dosyanın derlenmesiyle bir "object file" oluşur. İsmi "file_x.o" olsun. #include "tpl_func.h" void fx() { //... tpl_func(); // Bu noktada fonksiyon çağrısı yapıldığından "Instantiation" yapılacak. } /* // ----> (1), // "file_x.o" 000000 W void tpl_func() 000000 T fx(); */ // ----> (2), file_y.cpp: Bu dosyanın derlenmesiyle ayrı bir "object file" oluşur. İsmi "file_y.o" olsun. #include "tpl_func.h" void fy() { //... tpl_func(); // Bu noktada fonksiyon çağrısı yapıldığından "Instantiation" yapılacak. } /* ----> (2), // "file_y.o" 000000 W void tpl_func() 000000 T fy(); */ // ----> (3), // İlgili "object" dosyaları incelediğimizde, ayrı ayrı "tpl_func()" // olduğunu görürüz. Yani ilgili fonksiyonun kodu iki defa derlenmiş. Buradaki // "W" ifadesi "Weak Reference" anlamındadır. Dolayısıyla link aşamasında // bunlardan sadece birisi alınmaktadır. Eğer 100 adet dosya içerisinde "tpl_func()" // olsaydı, ilgili fonksiyonun kodu 100 defa derlenecektir. Bu da derleme zamanına ilişkin // yük oluşturmaktadır. * Örnek 1.0.1, Aşağıdaki örnekte ise derleyiciye o modül içerisinde "Instantiate" YAPTIRMAMA talimatı vermiş oluyoruz. // tpl_func.h template void tpl_func() { //... } // file_x.cpp // #include "tpl_func.h" void fx() { //... tpl_func(); // Bu noktada fonksiyon çağrısı yapıldığından "Instantiation" yapılacak. } // file_y.cpp // #include "tpl_func.h" extern template void tpl_func(); // This is NOT "Explicit Instantiation", but "Extern Template Declaration". // Derleyiciye "Instantiation" YAPMAMA TALİMATI VERDİK. Bu modül içerisinde // derleyici "Instantiation" YAPMAYACAKTIR. Artık "Instantiation", sadece "file_x.cpp" // içerisindeki "tpl_func()" çağrıdan dolayı yapılacaktır. void fy() { //... tpl_func(); } // file_x.o 000000 W void tpl_func() 000000 T fx(); // file_y.o 000000 T fx(); // Görüleceği üzere sadece bir tane "tpl_func()" olduğunu görüyoruz. İşte // "file_y.cpp" içerisindeki "Extern Template Declaration" ı başka modüllerde de // yapsaydık, o modüllerdeki "tpl_func()" çağrıları sonucunda "Instantiate" // yapılmayacaktır. Böylelikle derleme zamanına ilişkin yükü azaltmış oluyoruz. int main() { } Şimdi bu iki örneği özetlersek; -> "extern template void tpl_func();" bildirimi ile derleyiciye diyoruz ki bu bildirimi gördüğün "translation unit" de eğer "tpl_func" çağrısı yapılmışsa, bu çağrı için "Instantiation" YAPMA, yani senin o fonksiyonun kodunu derlemeni İSTEMİYORUM. Dolayısıyla ilgili bildirimin mevcut olmadığı ve "tpl_func" çağrısının yapıldığı "translation unit" (ler)de "Instantiation" yapılacaktır. Peki bu durumda biz şöyle bir şey yapsak nasıl olur? -> Bir başlık dosyasına şablonu yerleştiririm. Eğer o şablonun belirli tür(ler)den açılımlarını kullanacaksam, yani o baştan belliyse, kaynak dosyaların hepsine(bir tanesi hariç) "extern template void tpl_func();" bildirimini eklerim. O hariç olan kaynak dosyaya da "Manuel Instantiate" talimatını yerleştiririm. Bir diğer deyişle "extern template void tpl_func();" bildirimini başlık dosyasına eklerim. Bu başlık dosyasını "#include" eden bütün ".cpp" dosyaları da haliyle bu bildirimi de eklemiş olacaklar. Sonra, harici bir tane ".cpp" dosyası alıp, içerisine "Manuel Instantiate" talimatı yerleştiririm. Günün sonunda toplamda "n" tane dosya "#include" işlemi gerçekleştirmişse, "n" tanesinde "extern template void tpl_func();" bildirimi, o harici olan bir tanesinde de "Manuel Instantiate" bildirimi olmuş olur. Dolayısıyla sadece bir tane dosyada ilgili fonksiyonun kodu derlenmiş olacak, diğer dosyalardaki çağrılar ise derlenmiş olanı referans alacaklar. Şimdi burada da şöyle bir dezavantaj açığa çıkmaktadır; derleyici sadece bir defa derleme yapacağından, fonksiyonun kodu sadece bir defa açılacaktır. Dolayısıyla derleyici, diğer dosyalar ile birleştirip de optimizasyon yapma olanağı ORTADAN KALKMIŞ OLDU. * Örnek 1, BURADAKİ KODU TEKRAR İNCELE; NECATİ ERGİN GITHUB. //////////////////////// // tpl_func.h template void tpl_func { //... } extern template void tpl_func(); //////////////////////// // tpl_func.cpp #include "tpl_func.h" // With this directive, the following one is // also being included; // extern template void tpl_func(); template void tpl_func(); // "Manuel Instantiation" //////////////////////// // file_x.cpp #include "tpl_func.h" // With this directive, the following one is // also being included; // extern template void tpl_func(); void fx() { //... tpl_func(); } //////////////////////// // file_y.cpp #include "tpl_func.h" // With this directive, the following one is // also being included; // extern template void tpl_func(); void fy() { //.. tpl_func(); } -> Pekiyi yukarıdaki gibi başlık dosyasına "extern template void tpl_func();" yerleştirmek yerine, sadece belli kaynak dosyalarına bu bildirimi yerleştirsek nasıl olur? Açıkçası böyle yapmak çok risklidir. Çünkü "Instantiate" yapılacak kaynak dosyadaki fonksiyon çağrısı "inline" olarak ele alınırsa, derlenmiş dosyada "tpl_func" için bir kod YER ALMAYACAKTIR. Dolayısıyla "extern template void tpl_func();" bildirimlerinin mevcut olduğu diğer bütün modüller, derleme sekansı bittikten sonra, tanımı olmayan bir fonksiyonu referans almış olacaklar. Yani "Dangling Reference" problemi. Yani link aşamasında patlayacağız. Dolayısıyla bizlerin ilgili fonksiyonu "inline" OLARAK ELE ALINMAMASINI BİR ŞEKİLDE SAĞLAMALIYIZ. Maalesef "inline" olarak ele almayı engelleyecek bir bildirim şekli standart DEĞİL. Tamamen derleyicilerin ayarlarıyla ilgili. Aşağıdaki örnekleri inceleyelim; * Örnek 1.0, Aşağıdaki programda "linker" program hata verebilir. //////////////////////// // tpl_func.h template void tpl_func() { //... } //////////////////////// // file_x.cpp #include "tpl_func.h" void fx() { //... // Gcc ve Cland derleyicilerinde optimizasyon seçeneği // "-O2" seçilirse "linker" program, aşağıdaki fonksiyon // çağrısına ilişkin ilgili fonksiyonun tanımsız olduğuna // dair, hata mesajı verebilir. tpl_func(); } //////////////////////// // file_y.cpp #include "tpl_func.h" extern template void tpl_func(); void fy() { //.. tpl_func(); } //////////////////////// // file_x.o // 000000 T fx(); // file_y.o // 000000 T fy(); // "Object" dosyalardan da görüleceği üzere, // "file_x.cpp" içerisindeki "tpl_func();" // çağrısı "inline" olarak ele alınmıştır. * Örnek 1.1, İşte "tpl_func.h" dosyasındaki fonksiyon tanımını aşağıdaki biçimde yaparsak, "inline" olarak ele alınmayacaktır. Fakat bu yöntem derleyiciye bağlı bir yöntemdir. //////////////////////// // tpl_func.h template void __attribute__((noinline)) tpl_func { //... } //////////////////////// // file_x.cpp #include "tpl_func.h" void fx() { //... // Gcc ve Cland derleyicilerinde optimizasyon seçeneği // "-O2" seçilirse "linker" program, aşağıdaki fonksiyon // çağrısına ilişkin ilgili fonksiyonun tanımsız olduğuna // dair, hata mesajı verebilir. tpl_func(); } //////////////////////// // file_y.cpp #include "tpl_func.h" extern template void tpl_func(); void fy() { //.. tpl_func(); } //////////////////////// // file_x.o // 000000 W void tpl_func() // 000000 T fx(); // file_y.o // 000000 T fy(); Dolayısıyla günün sonunda ilgili başlık dosyasına "extern template void tpl_func();" bildirimini eklemek makul bir yöntem. Ek olarak iş bu yöntem, üçüncü parti kütüphanelere de uygulanabilir. Şöyleki; * Örnek 1, //////////////////////// // thid_party.h template void tpl_func { //... } //////////////////////// // tpl_func.h #include "thid_party.h" extern template void tpl_func(); //////////////////////// // tpl_func.cpp #include "tpl_func.h" // With this directive, the following one is // also being included; // extern template void tpl_func(); template void tpl_func(); // "Manuel Instantiation" //////////////////////// // file_x.cpp #include "tpl_func.h" // With this directive, the following one is // also being included; // extern template void tpl_func(); void fx() { //... tpl_func(); } //////////////////////// // file_y.cpp #include "tpl_func.h" // With this directive, the following one is // also being included; // extern template void tpl_func(); void fy() { //.. tpl_func(); } Buraya kadar anlatılanları sınıf şablonları için de uygulayabiliriz. Şöyleki; * Örnek 1, //////////////////////// // tClass.h template class tClass { //... }; //////////////////////// // file_x.cpp #include "tClass.h" void fx() { //... tClass bc; } //////////////////////// // file_y.cpp #include "tClass.h" extern template class tClass; void fy() { //... tClass bc; } Hakeza standart kütüphanedeki öğeler ise benzer yöntemi kullanmaktadır. Şöyleki; * Örnek 1, namespace std{ template, typename Allocator = allocator> class basic_string { //... }; extern template class basic_string; extern template class basic_string; } >>>> "Implicit Instantiation": Fonksiyon çağrısı yapıldığı için, ya da belirli bir sınıf türü kullanıldığı için, derleyicinin "Instantiation" yapmaya zorlanmasıdır. Aslında bu zamana kadar şablonları kullandığımız yerdeki kullanım biçimimiz, "Implicit Instantiation" yaklaşımıdır. Şimdi de şu soruya yanıt arayalım; hangi durumlarda "Instantiation" gerekiyor, hangi durumlarda gerekmiyor. Çünkü derleyici(ler) her durumda bir "Instantiation" YAPMAMAKTADIR. Bazı koşulların da sağlanması gerekmektedir. Aşağıdaki örnekleri inceleyelim; * Örnek 1.0.0, Aşağıdaki örnekte "p" için bir "instantiation" oluşmayacak. #include template struct Neco{ Neco() { std::cout << "Neco\n"; } void foo(){ std::cout << "foo\n"; } void bar(){ std::cout << "bar\n"; } void baz(){ std::cout << "baz\n"; } }; int main() { // No instantiation at all. Neco* p{}; // instantiation for only "foo" func. No instantiation for other func.(s) p->foo(); // Output: foo //... // instantiation for only "baz" func. No instantiation for "bar" func. p->baz(); // Output: baz } * Örnek 1.0.1, "cppinsights.com" çıktısı: #include template struct Neco { inline Neco() { std::operator<<(std::cout, "Neco\n"); } inline void foo() { std::operator<<(std::cout, "foo\n"); } inline void bar() { std::operator<<(std::cout, "bar\n"); } inline void baz() { std::operator<<(std::cout, "baz\n"); } }; /* First instantiated from: insights.cpp:17 */ #ifdef INSIGHTS_USE_TEMPLATE template<> struct Neco { inline Neco(); inline void foo() { std::operator<<(std::cout, "foo\n"); } inline void bar(); inline void baz(); }; #endif int main() { Neco * p = {}; p->foo(); return 0; } * Örnek 2, "static" veri elemanları "main" fonksiyon çağrısından evvel hayata geldiği için, yine burada ilgili şablon "instantiate" edilecektir. #include template struct Neco{ static T ms; }; template T Neco::ms = 0; int main() { Neco n0; std::cout << n0.ms << '\n'; // 0 Neco n1; std::cout << n1.ms << '\n'; // 0 Neco n2; std::cout << n2.ms << '\n'; // 0 n2.ms = 2.3; std::cout << n0.ms << '\n'; // 0 std::cout << n1.ms << '\n'; // 2.3 std::cout << n2.ms << '\n'; // 2.3 } * Örnek 3, Aşağıdaki örneği inceleyelim; template class C; // #1: Declaration Only C* p = 0; // #2: OK. Since a definition for C is not needed. template class C { public: void f(); // #3: Member Declaration }; // #4: Class Template Definiton is now completed. void g(C& c) { // #5: OK. Class Template Definiton is NOT needed. c.f(); // #6.0: Require Class Template Definiton. A definition // is needed for "C::f()" in this translation unit. C* ptr = new C; // #6.1: Require Class Template Definiton. A definition // is needed for "C::f()" in this translation unit. } template void C::f() { // Required definition needed @ step 6. //... } int main() { } * Örnek 4.0, template struct Nec{ using type = typename T::value_type; }; int main() { // No Instantiation: Açılım olmadığı için, // "Nec" sınıf içerisindeki "using" bildirimi // sentaks hatası oluşturmayacak. Nec* p{}; } * Örnek 4.1, template struct Nec{ using type = typename T::value_type; }; int main() { // In instantiation of ‘struct Nec’: // ‘int’ is not a class, struct, or union type Nec p{}; // Böylesi bir tanımlama şablon açılımı gerektirdiği için, // ilgili "using" bildirimi sentaks hatası oluşturdu. } * Örnek 5, template T foo(T x) { using type = typename T::value_type; return x*x; } int main() { // No Instantiation will happen here. decltype(foo(12)) x = 5; // Çünkü burada derleyici, "foo" fonksiyonunun // imzasını oluşturması gerekmektedir. Dolayısıyla // şablonu açmadı. } >> Diğer yandan parametre paketlerine ilişkin "C++20 Idioms for parameter packs by David Mazières" isimli kaynaktan da fayda sağlayabiliriz. > "if constexpr" : Derleyiciye, derleme zamanında kod seçimi yaptırmaya yarar. "static if" olarak da belirtilir. Argüman olarak "Compiler Time Expression" alır. Şablonlara yönelik bir araçtır ama çalışma zamanında bir fonksiyon içerisinde de kullanabiliriz. Fakat kurallarda farklılık oluşacaktır. "If-Else" merdiveni de oluşturabiliriz. Bu durumda "else-if" durumlarında mutlaka "constexpr" belirtecini yazmalıyız. Şimdi de aşağıdaki örnekleri inceleyelim; * Örnek 1, #include template void func(T tx) { if constexpr (std::is_integral_v) { // "T" bir tam sayı türü ise bu kısım derlemeye alınacak. if (tx != 0) func(tx--); } else { // Aksi halde bu kısım. // Error: error: there are no arguments to ‘undeclared_f’ that depend on a template parameter, // so a declaration of ‘undeclared_f’ must be available // undeclared_f(); undeclared(tx); // OK } } int main() { // Gönderilen argümanın ne olduğundan bağımsız; yukarıdaki "else" bloğu da sentaks kurallarına // tabii tutulacaktır. Dolayısıyla "else" bloğundaki "undeclared_f" ismi SENTAKS HATASI // oluşturacaktır. Çünkü ilgili isim "non-dependent on template parameter, it must be dependent // on template parameter". func(2); } * Örnek 2, #include struct Neco{}; int main() { Neco nec; if constexpr (sizeof(int) > 1) ++nec; // no match for ‘operator++’ (operand type is ‘Neco’) else --nec; // no match for ‘operator--’ (operand type is ‘Neco’) // Ortada bir şablon olmadığından, "if" ve "else" // bloklarında sentaks kuralları irdelenir. } * Örnek 3, #include #include template void func(T& tx) { if (tx > 0) { if constexpr (std::is_integral_v) { ++tx; } else { --tx; } } } int main() { int ival = 5; double dval = 2.5; func(ival); func(dval); std::cout << "ival: " << ival << ", dval: " << dval << '\n'; // ival: 6, dval: 1.5 } * Örnek 4, #include // Return value is "int", or "double". constexpr auto func() { if constexpr (sizeof(int) > 4u) return 1; else return 0.f; } // Return value is "int", or "void". auto foo() { if constexpr (sizeof(int) > 4u) return 1; } int main() { //... } * Örnek 5, #include #include #include template auto get_val(T t) { if constexpr (std::is_pointer_v) return *t; else return t; } int main() { int ival{ 87 }; std::cout << get_val(ival) << '\n'; // 87 double dval{ 8.7 }; std::cout << get_val(dval) << '\n'; // 8.7 int* ipval{ &ival }; std::cout << get_val(ipval) << '\n'; // 87 double* dpval{ &dval }; std::cout << get_val(dpval) << '\n'; // 8.7 } * Örnek 6, #include #include #include template std::string as_string(T x) { if constexpr (std::is_same_v) return x; else if constexpr (std::is_arithmetic_v) return std::to_string(x); else return std::string(x); } class Myclass {}; int main() { std::cout << as_string(42) << '\n'; // 42 std::cout << as_string(4.2) << '\n'; // 4.200000 std::cout << as_string("Kirkiki") << '\n'; // Kirkiki std::cout << as_string(std::string("kirkIki")) << '\n'; // kirkIki // Error: no matching function for call to ‘std::__cxx11::basic_string::basic_string(Myclass&)’ // "std::string" sınıfındaki "ctor." fonksiyonlar "Myclass" türünden argüman almadıklarından, sentaks hatası oluştu. // std::cout << as_string(Myclass{}) << '\n'; } * Örnek 7.0, "Tag Dispatch": #include #include #include #include namespace details{ // Implementation for Random Access Iterator template void Advance_Impl(Iter& pos, Dist n, std::random_access_iterator_tag) { pos += n; } // Implementation for Random Bi-Directional Iterator template void Advance_Impl(Iter& pos, Dist n, std::bidirectional_iterator_tag) { if (n >= 0) while (n--) ++pos; else while(n++) --pos; } // Implementation for Input Iterator template void Advance_Impl(Iter& pos, Dist n, std::input_iterator_tag) { while(n--) ++pos; } } template void Advance(Iter& pos, Dist n) { using Cat = typename std::iterator_traits::iterator_category; // Including "C-Array" // using Cat = typename Iter::iterator_category; // Excluding "C-Array" details::Advance_Impl(pos, n, Cat{}); // Üçüncü parametrenin türüne göre yukarıdaki uygun olan fonksiyon // seçilecektir. } int main() { std::vector ivec{ 1, 2, 3, 4, 5 }; auto viter = ivec.begin(); Advance(viter, 3); std::cout << *viter << '\n'; // 4 std::list ilist{ 5, 4, 3, 2, 1 }; auto liter = ilist.begin(); Advance(liter, 4); std::cout << *liter << '\n'; // 1 } * Örnek 7.1, "Tag Dispatch" mekanizmasının alternatifi olabilir. #include #include #include #include #include template void Advance(Iter& pos, Dist n){ using Cat = typename std::iterator_traits::iterator_category; if constexpr (std::is_same_v) { pos += n; } else if constexpr (std::is_same_v) { if (n > 0) while(n--) ++pos; else while(n++) --pos; } else { while(n--) ++pos; } } int main() { std::vector ivec{ 1, 2, 3, 4, 5 }; auto viter = ivec.begin(); Advance(viter, 3); std::cout << *viter << '\n'; // 4 std::list ilist{ 5, 4, 3, 2, 1 }; auto liter = ilist.begin(); Advance(liter, 4); std::cout << *liter << '\n'; // 1 } * Örnek 8.0, "Recursive" çağrı: template constexpr int fibbo() { return fibbo() + fibbo(); } template<> constexpr int fibbo<1>(){ return 1; } template<> constexpr int fibbo<0>(){ return 0; } int main() { constexpr auto x = fibbo<5>(); // 120 } * Örnek 8.1, "Recursive" çağrılara alternatif olabilir template constexpr int fibbo() { if constexpr (N >= 0) return fibbo() + fibbo(); else return N; } int main() { constexpr auto x = fibbo<5>(); // 120 } * Örnek 9.0, #include #include template std::string to_str(T t) { return std::to_string(t); } std::string to_str(const std::string& t) { return t; } std::string to_str(const char* t) { return t; } std::string to_str(bool b) { return b ? "true" : "false"; } int main() { std::cout << to_str("Ulya") << '\n'; std::cout << to_str(std::string("Yuruk")) << '\n'; std::cout << to_str(13) << '\n'; std::cout << to_str(5.9) << '\n'; std::cout << to_str(true) << '\n'; } * Örnek 9.1, #include #include #include template std::string to_str(T t){ if constexpr (std::is_convertible_v) return t; else if constexpr (std::is_same_v) return t ? "True" : "False"; else return std::to_string(t); } int main() { std::cout << to_str("Ulya") << '\n'; std::cout << to_str(std::string("Yuruk")) << '\n'; std::cout << to_str(13) << '\n'; std::cout << to_str(5.9) << '\n'; std::cout << to_str(true) << '\n'; } * Örnek 10, "print" fonksiyonunun bir diğer versiyonu: #include #include #include template void print(const T& x, const Ts&... args) { if constexpr (sizeof...(args) == 0) std::cout << x << '\n'; // Paketteki öğe sayısı sıfır ise bu kod; else std::cout << x << ", "; // Aksi halde bu kod. // Paketteki öğe sayısı sıfır DEĞİL İSE, // derleyici paketteki öğeler bitene kadar // "Compile Time Recursivetly" biçiminde // "print" fonksiyonunu yazacaktır. if constexpr (sizeof...(args) != 0) print(args...); } int main() { print(12, 3.4, "Ulya Yuruk"); // 12, 3.4, Ulya Yuruk } * Örnek 11, #include #include #include template void copy_array(T(&dest)[N], const T(&source)[N]) { // Bu fonksiyona geçilen dizinin türleri ve boyutları // aynı olmak zorundadır. if constexpr (std::is_trivially_copyable_v) // "byte" olarak kopyalanmanın "memcpy" ile mümkün olduğu sorgulanıyor. std::memcpy(dest, source, N * sizeof(T)); else std::copy(source, std::end(source)), dest); // Eğer mümkün değilse, buradaki "copy" fonksiyonu çağrılacak. } * Örnek 12.0, Kendi sınıflarımı "Structural Binding" ile kullanabilmek için "std::get" arayüzünü implemente etmemiz gerekiyor. İşte buna da bir alternatif olarak kullanılabilir. #include #include #include class Neco { public: template auto get(); // Gerek "explicit specialization" gerek "partial specialization" için fonksiyonun tanımına lüzum YOKTUR. int ival{ 17500 }; double dval{ 17.5 }; std::string name{ "Ulya Yuruk" }; std::vector ivec{ "Ulya Yuruk", "Uskudar", "Istanbul" }; }; template<> auto Neco::get<0>() { return ival; } template<> auto Neco::get<1>() { return dval; } template<> auto Neco::get<2>() { return name; } template<> auto Neco::get<3>() { return ivec; } int main(){ const auto&[age, wage, name, address] = Neco{}; std::cout << "Age: " << age << '\n'; // Age: 17500 std::cout << "Wage: " << wage << '\n'; // Wage: 17.5 std::cout << "Name: " << name << '\n'; // Name: Ulya Yuruk std::cout << "Address: "; // Address: for(const auto& i : address) std::cout << i << ' '; // Ulya Yuruk Uskudar Istanbul std::cout << '\n'; } * Örnek 12.1, #include #include #include class Neco { public: template auto get() { if constexpr (N == 0) return ival; else if constexpr (N == 0) return dval; else if constexpr (N == 0) return name; else if constexpr (N == 0) return ivec; } int ival{ 17500 }; double dval{ 17.5 }; std::string name{ "Ulya Yuruk" }; std::vector ivec{ "Ulya Yuruk", "Uskudar", "Istanbul" }; }; int main(){ const auto&[age, wage, name, address] = Neco{}; std::cout << "Age: " << age << '\n'; // Age: 17500 std::cout << "Wage: " << wage << '\n'; // Wage: 17.5 std::cout << "Name: " << name << '\n'; // Name: Ulya Yuruk std::cout << "Address: "; // Address: for(const auto& i : address) std::cout << i << ' '; // Ulya Yuruk Uskudar Istanbul std::cout << '\n'; } * Örnek 13, "if constexpr", kısa devre davranışına NEDEN OLMAZ. Kısa devre davranışı için ya iç içe "if constexpr" ya da normal "if" deyimi kullanmalıyız. #include #include #include #include template void foo(T x, T y) { if constexpr (std::is_integral::value && std::numeric_limits::min() < 10) { std::cout << '(' << typeid(T).name() << ") is integral and minimum numeric limit is less than 10\n"; } } int main() { foo(13, 31); // (i) is integral and minimum numeric limit is less than 10 std::cout << '\n'; foo(std::string{"Ulya"}, std::string{"Yuruk"}); // error: numeric_limits are not defined for strings. std::cout << '\n'; } Öte yandan "if constexpr" ile "static_assert" i birlikte kullanırken şöyle bir problem ile karşılaşabiliriz; Diyelim ki "T" için "integral" bir tür olduğunda ayrı bir kod, "floating_point" olduğunda ayrı bir kod parçasının seçilmesini, bu ikisi haricindeki durumlar için sentaks hatası oluşmasını isteyelim. Bunu gerçekleştirecek de şöyle bir kod yazmış olalım; * Örnek 1, #include #include template void foo (T x) { if constexpr (std::is_integral_v) std::cout << "integral.\n"; else if constexpr (std::is_floating_point_v) std::cout << "floating_point.\n"; else static_assert(false, "Either T must be an integral type, or a floating_point type"); } int main() { // error: static assertion failed: Either T must be an integral type, or a floating_point type } Örnekte de görüleceği üzere, "foo" fonksiyonunu çağırmamış olmamıza rağmen "static_assert" tutmadı. Diğer yandan bazı derleyiciler söz konusu olduğunda, yukarıdaki KOD DERLENECEKTİR DE. Bizim buradaki nihai amacımız, derleyiciden bağımsız bir şekilde, "static_assert" in ilk parametresinin "always false" olmasını sağlatmaktır. İşte bunun için yukarıdaki kodu aşağıdaki gibi güncelleyebiliriz; * Örnek 1, #include #include template void foo (T x) { if constexpr (std::is_integral_v) std::cout << "integral.\n"; else if constexpr (std::is_floating_point_v) std::cout << "floating_point.\n"; else static_assert(sizeof(T) != sizeof(T), "Either T must be an integral type, or a floating_point type"); } int main() { } Artık derleyiciden bağımsız bir şekilde yukarıdaki kod derlenecektir ve "foo" fonksiyonunu çağırmadığımız müddetçe herhangi bir sentaks hatası ALMAYACAĞIZ. Şimdi de bu kodu, "foo" fonksiyonunu çağırarak test edelim; * Örnek 1, #include #include template void foo (T x) { if constexpr (std::is_integral_v) std::cout << "integral.\n"; else if constexpr (std::is_floating_point_v) std::cout << "floating_point.\n"; else static_assert(sizeof(T) != sizeof(T), "Either T must be an integral type, or a floating_point type"); } class Myclass{}; struct Neco{}; int main() { foo(31); // integral. foo(3.1); // floating_point. // In instantiation of ‘void foo(T) [with T = Myclass]’: // error: static assertion failed: Either T must be an integral type, or a floating_point type // foo(Myclass{}); // In instantiation of ‘void foo(T) [with T = Neco]’: // error: static assertion failed: Either T must be an integral type, or a floating_point type // foo(Neco{}); } Fakat bu çözüm yolu da kodun okunmasını biraz zorlaştırmaktadır. Yani ilk bakışta neden "sizeof(T) != sizeof(T)" sorgulamasının yapıldığı pek net değildir. İşte buna çözüm olarak da şöyle bir yöntem geliştirilmiş; * Örnek 1, #include #include template struct always_false : std::false_type{}; template constexpr bool always_false_v = always_false::value; template void foo (T x) { if constexpr (std::is_integral_v) std::cout << "integral.\n"; else if constexpr (std::is_floating_point_v) std::cout << "floating_point.\n"; else static_assert(always_false_v, "Either T must be an integral type, or a floating_point type"); } class Myclass{}; struct Neco{}; int main() { foo(31); // integral. foo(3.1); // floating_point. // In instantiation of ‘void foo(T) [with T = Myclass]’: // error: static assertion failed: Either T must be an integral type, or a floating_point type foo(Myclass{}); // In instantiation of ‘void foo(T) [with T = Neco]’: // error: static assertion failed: Either T must be an integral type, or a floating_point type foo(Neco{}); } Hatta yukarıdaki örnekteki sınıf şablonu yerine "Variable Template with Template Parameter Pack" kullanabiliriz; * Örnek 1, #include #include template constexpr bool always_false_v = false; template void foo (T x) { if constexpr (std::is_integral_v) std::cout << "integral.\n"; else if constexpr (std::is_floating_point_v) std::cout << "floating_point.\n"; else static_assert(always_false_v, "Either T must be an integral type, or a floating_point type"); } class Myclass{}; struct Neco{}; int main() { foo(31); // integral. foo(3.1); // floating_point. // In instantiation of ‘void foo(T) [with T = Myclass]’: // error: static assertion failed: Either T must be an integral type, or a floating_point type foo(Myclass{}); // In instantiation of ‘void foo(T) [with T = Neco]’: // error: static assertion failed: Either T must be an integral type, or a floating_point type foo(Neco{}); } Sonuç olarak bizim buradaki nihai amacımız "static_assert(false, ...)" yazdığımız zaman bazı derleyiciler direkt sentaks hatası verirken bazılarının vermemesi. İşte buradaki "false" ifadesi yerine alternatif olarak yukarıdaki yaklaşımları kullanabiliriz. > "std::type_identity" : C++20 ile dile eklenmiştir ve karşılık geldiği tür için tür çıkarımını "disable" etmektedir. Anımsanacağı üzere derleyici "T" için tür çıkarımı yaparken bazı durumlarda "T" türüne karşılık farklı türler gelmekte, bu da "ambigous" a neden olmaktadır. * Örnek 1, #include template void func(std::vector& ivec, T&& elem) {} int main() { std::vector ivec; // Burada, // "ivec" için "T" nin türü "int" olacaktır. // "elem" için "T" nin türü "int" olacaktır çünkü sağ // taraf ifadelerini "Universal Reference" a gönderdiğimizde // türün kendisi yönünde çıkarım yapılmaktadır. func(ivec, 12); int x{ 123 }; // Burada, // "ivec" için "T" nin türü "int" olacaktır. // "elem" için "T" nin türü "int&" olacaktır çünkü sol // taraf ifadelerini "Universal Reference" a gönderdiğimizde // o türe referans bir tür şeklinde çıkarım yapılmaktadır. // "T" hem "int" hem de "int&" olamayacağı için sentaks // hatası alacağız. func(ivec, x); } * Örnek 2, #include template void func(std::vector& ivec, T elem) {} int main() { std::vector ivec; // Burada "ivec" için "T" tür çıkarımı "int" yönünde. // "32" için "T" yine "int" olacak. Herhangi bir sorun // yok. func(ivec, 32); std::vector svec; func(svec, "Ulya Yuruk"); // Burada "svec" için "T" tür çıkarımı "std::string" // yönünde olacak. // "Ulya Yuruk" için "T" tür çıkarımı "const char*" // yönünde olacak. "T" için iki farklı tür olduğundan // sentaks hatası alacağız. } İşte bu "ambigous" probleminin çözüm yollarından birisi de "std::type_identity" tür eş ismini kullanmaktır. Tipik implementasyonu aşağıdaki gibidir; * Örnek 1, #include #include template struct TypeIdentity { using Type = T; }; template using TypeIdentity_t = typename TypeIdentity::Type; // C++20 ile birlikte bildirimde "typename" kullanma zorunluluğu kalktı. template void func(std::vector& ivec, TypeIdentity_t elem) {} int main() { std::vector ivec; func(ivec, 32); std::vector svec; func(svec, "Ulya Yuruk"); } Şimdi de kullanım senaryolarına bakalım; * Örnek 1.0, #include #include #include #include template void func(std::vector& ivec, std::type_identity_t elem) { std::cout << typeid(T).name() << '\n'; } int main() { std::vector ivec; func(ivec, 32); // int std::vector svec; func(svec, "Ulya Yuruk"); // string } * Örnek 1.1, #include #include #include #include template void func(std::type_identity_t elem, std::vector& ivec) { std::cout << typeid(T).name() << '\n'; } int main() { std::vector ivec; func(32, ivec); // int std::vector svec; func("Ulya Yuruk", svec); // string } > "9. Cpp Idioms/Patterns > CRTP" : "Curiously recurring template pattern" olarka geçer. Türemiş sınıflar, taban sınıfın arayüzündeki fonksiyonları kullanabiliyorlar(eğer kalıtımın yapılış biçimi el verirse). Taban sınıfın iş bu fonksiyonlarının implementasonunda, türemiş sınıfa ait öğeler de kullanılabilir. Yani taban sınıftan türemiş sınıfa bir şekilde kod enjekte etmiş oluyoruz. Buradaki kilit nokta türemiş sınıf, taban sınıfın türemiş sınıf açılımından, elde edilmektedir. Şöyleki; * Örnek 1, template class Base { //... }; class Derived : public: Base { //... }; Buradaki kalıtımın yapılış şeklinin, yani "public","protected","private" yapılması, doğrudan "CRTP" ile ilgili değildir. Duruma göre üçünden birisi de olabilir. Bu araç "static polymorphism" için alternatif bir araçtır, yani "virtual dispatch" mekanizmasını elimine ederek, kod seçiminin derleme zamanında yapılmasını sağlayabiliriz. Yani "runtime polymorphism" i "compiletime polymorphism" e dönüştürüyoruz. Pekala "runtime polymorphism" den kaçınmak için "std::variant" da kullanabilir eğer şartlar uygunsa. Şimdi de örnekler üzerinden devam edelim; * Örnek 1.0.0, Aşağıda kısıtlı bir "CRTP" örüntüsüne ilişkin örnek verilmiştir. Bu yaklaşım ile biz aslında bütün "Counter" sınıfının ilgili türden açılımlarına ilişkin bilgi edinmiş oluruz. Çünkü o açılımların hepsi ayrı bir taban sınıf olarak kullanılmaktadır. #include #include template class Counter { public: Counter() { ++total_live_objects; ++total_created_objects; } Counter(const Counter&) { ++total_live_objects; ++total_created_objects; } ~Counter() { --total_live_objects; } static std::size_t get_live_objects() { return total_live_objects; } static std::size_t get_created_objects() { return total_created_objects; } private: inline static std::size_t total_live_objects{}; inline static std::size_t total_created_objects{}; }; class Derived_I : Counter { // "private" kalıtım yaptığımız için // "Counter" sınıfındaki fonksiyonları // sınıf dışında kullanamayız. Eğer // kullanmak istiyorsak, aşağıdaki // "using" bildirimlerini eklememiz // ya da "public" kalıtım yapmamız // gerekiyor. public: // Artık ilgili fonksiyonların isimleri bizimkine // enjekte edildi. using Counter::get_live_objects; using Counter::get_created_objects; }; class Derived_II : public Counter { }; int main() { // Burada "CRTP" örüntüsü kullanarak, taban // sınıftaki "static" veri elemanlarının, // bütün türemiş sınıflar nezdinde, AYRI // olmasını sağladık. Çünkü "Derived_I" // sınıfını oluştururken derleyici // "Counter" ın "Derived_I" açılımını YAZDI // ve "Derived_I" sınıfını da bu açılımdan // türetti. Yani "Derived_I" için "Counter" // açılımı kullanıldı. Aynı şey "Derived_II" için de // geçerlidir. std::cout << "Live Objects of Derived_I > " << Derived_I::get_live_objects() << '\n'; // Live Objects of Derived_I > 0 std::cout << "Total Objects of Derived_I > " << Derived_I::get_created_objects() << '\n'; // Total Objects of Derived_I > 0 Derived_I mx, my; { Derived_I mz; } std::cout << "Live Objects of Derived_I > " << Derived_I::get_live_objects() << '\n'; // Live Objects of Derived_I > 2 std::cout << "Total Objects of Derived_I > " << Derived_I::get_created_objects() << '\n'; // Total Objects of Derived_I > 3 std::cout << "\n========================\n\n"; std::cout << "Live Objects of Derived_II > " << Derived_II::get_live_objects() << '\n'; // Live Objects of Derived_II > 0 std::cout << "Total Objects of Derived_II > " << Derived_II::get_created_objects() << '\n'; // Total Objects of Derived_II > 0 Derived_II ma, mb; { Derived_II mc; } std::cout << "Live Objects of Derived_II > " << Derived_II::get_live_objects() << '\n'; // Live Objects of Derived_II > 2 std::cout << "Total Objects of Derived_II > " << Derived_II::get_created_objects() << '\n'; // Total Objects of Derived_II > 3 } * Örnek 1.0.1, Pekala aşağıdaki yaklaşımı da kullanabiliriz, yukarıdakine alternatif olarak. Çünkü "closure-type" varsayılan tür olarak kullanılmış. Dolayısıyla "Derived_I" ve "Derived_II" sınıflarının taban sınıfları da ayrıdır. #include #include // template // Non-type Template Parameter, needs C++20 template // Type Template Parameter class Counter { public: Counter() { ++total_live_objects; ++total_created_objects; } Counter(const Counter&) { ++total_live_objects; ++total_created_objects; } ~Counter() { --total_live_objects; } static std::size_t get_live_objects() { return total_live_objects; } static std::size_t get_created_objects() { return total_created_objects; } private: inline static std::size_t total_live_objects{}; inline static std::size_t total_created_objects{}; }; class Derived_I : Counter<> { public: using Counter::get_live_objects; using Counter::get_created_objects; }; class Derived_II : public Counter<> { }; int main() { std::cout << "Live Objects of Derived_I > " << Derived_I::get_live_objects() << '\n'; // Live Objects of Derived_I > 0 std::cout << "Total Objects of Derived_I > " << Derived_I::get_created_objects() << '\n'; // Total Objects of Derived_I > 0 Derived_I mx, my; { Derived_I mz; } std::cout << "Live Objects of Derived_I > " << Derived_I::get_live_objects() << '\n'; // Live Objects of Derived_I > 2 std::cout << "Total Objects of Derived_I > " << Derived_I::get_created_objects() << '\n'; // Total Objects of Derived_I > 3 std::cout << "\n========================\n\n"; std::cout << "Live Objects of Derived_II > " << Derived_II::get_live_objects() << '\n'; // Live Objects of Derived_II > 0 std::cout << "Total Objects of Derived_II > " << Derived_II::get_created_objects() << '\n'; // Total Objects of Derived_II > 0 Derived_II ma, mb; { Derived_II mc; } std::cout << "Live Objects of Derived_II > " << Derived_II::get_live_objects() << '\n'; // Live Objects of Derived_II > 2 std::cout << "Total Objects of Derived_II > " << Derived_II::get_created_objects() << '\n'; // Total Objects of Derived_II > 3 } * Örnek 1.1, Aşağıda "CRTP" kullanılmadan kalıtım yapılmanın etkilerini göreceğiz. Görüldüğü üzere tek bir ortak sınıf "Counter" kullanıldığı için ilgili "static" veri elemanları, türemiş sınıflar nezdinde, tekil hale gelmiştir. Bu yaklaşım ile biz aslında bütün "Counter" sınıflarına ilişkin bilgi edinmiş oluruz. #include #include class Counter { public: Counter() { ++total_live_objects; ++total_created_objects; } Counter(const Counter&) { ++total_live_objects; ++total_created_objects; } ~Counter() { --total_live_objects; } static std::size_t get_live_objects() { return total_live_objects; } static std::size_t get_created_objects() { return total_created_objects; } private: inline static std::size_t total_live_objects{}; inline static std::size_t total_created_objects{}; }; class Derived_I : Counter { public: using Counter::get_live_objects; using Counter::get_created_objects; }; class Derived_II : public Counter { }; int main() { std::cout << "Live Objects of Derived_I > " << Derived_I::get_live_objects() << '\n'; // Live Objects of Derived_I > 0 std::cout << "Total Objects of Derived_I > " << Derived_I::get_created_objects() << '\n'; // Total Objects of Derived_I > 0 Derived_I mx, my; { Derived_I mz; } std::cout << "Live Objects of Derived_I > " << Derived_I::get_live_objects() << '\n'; // Live Objects of Derived_I > 2 std::cout << "Total Objects of Derived_I > " << Derived_I::get_created_objects() << '\n'; // Total Objects of Derived_I > 3 std::cout << "\n========================\n\n"; std::cout << "Live Objects of Derived_II > " << Derived_II::get_live_objects() << '\n'; // Live Objects of Derived_II > 2 std::cout << "Total Objects of Derived_II > " << Derived_II::get_created_objects() << '\n'; // Total Objects of Derived_II > 3 Derived_II ma, mb; { Derived_II mc; } std::cout << "Live Objects of Derived_II > " << Derived_II::get_live_objects() << '\n'; // Live Objects of Derived_II > 4 std::cout << "Total Objects of Derived_II > " << Derived_II::get_created_objects() << '\n'; // Total Objects of Derived_II > 6 } * Örnek 2.0.0, "CRTP" yapısını kurarken yazım hatalarına karşı dikkatli olmalıyız. #include // CRTP Base: template class Base{ public: Base() { std::cout << typeid(Der).name() << '\n'; } }; class Der_A : public Base { public: Der_A() { std::cout << typeid(this).name() << '\n'; } }; // Aslında amacımız "Base" türünden kalıtım yapmaktı. Yazım hatasından dolayı // "Der_A" yazdık. Sonuç olarak herhangi bir sentaks hatası oluşmadı ancak "Der_B" ile // "Der_A" aslında aynı sınıflardır. class Der_B : public Base { public: Der_B() { std::cout << typeid(this).name() << '\n'; } }; int main(){ // 5Der_A // P5Der_B Der_A da; // 5Der_A // P5Der_B Der_B db; } * Örnek 2.0.1, Taban sınıf olarak kullanılan sınıf şablonunda, şablon parametresine "friend" vasfı verebiliyoruz. Eğer taban sınıfın kurucu işlevini de "private" kısma alırsak, ilgili şablon parametresine gelecek tür başka bir sınıftan olursa sentaks hatasına yol açmış oluruz. Böylelikle yazım hatasına karşı koruma getiririz. #include // CRTP Base: template class Base{ private: friend Der; Base(){ std::cout << typeid(Der).name() << '\n'; } }; class Der_A : public Base { }; // error: ‘Base::Base() [with Der = Der_A]’ is private within this context class Der_B : public Base { }; int main(){ Der_B bx; // Taban sınıftakı "ctor." fonksiyonu "private" oldu. Türemiş sınıf ise // bunu çağıracaktır. Bu durumda ilgili "ctor." fonksiyonunun çağrılabilir // olması gerekmekte. Yapmış olduğumuz "friend" bildirimi, sadece türemiş // sınıfı kapsamakta. Dolayısıyla eğer türemiş sınıf söz konusu değilse, // yani "Der" şablon parametresine karşılık gelen sınıf söz konusu değilse, // "private" bölüme erişim hakkımız olmayacaktır. Ancak "Der" in yerine // geçecek sınıf için pekala bu erişim hakkı OLACAKTIR. } * Örnek 2.1, Sonuç ekranından da görüleceği üzere bizler türemiş sınıf nesneleri ile taban sınıf fonksiyonuna çağrı yapıyoruz. Fakat kendi sınıfımızdaki versiyonu çağrılmakta. #include // CRTP Base: template class Base{ public: void Interface() { // Some code here... std::cout << "Base::Interface\n"; // Some code here... static_cast(this)->Implementation(); // Tabii buradaki çağrı, türemiş sınıflarda da "Implementation" // isimli bir fonksiyon olmasına dayanmaktadır. Burada // "Dependent Name" kullanıldığından, sentaks hatası // oluşmayacaktır. Tabii buradaki dönüşümü gösterici yerine // referansa da yapabilirdik. // Some code here... } static void sInterface(){ // Some code here... std::cout << "Base::sInterface\n"; // Some code here... Der::sImplementation(); // Tabii buradaki çağrı, türemiş sınıflarda da "sImplementation" // isimli bir fonksiyon olmasına dayanmaktadır. Burada // "Dependent Name" kullanıldığından, sentaks hatası // oluşmayacaktır. // Some code here... } private: //... }; class Der_A : public Base { public: void Implementation() { std::cout << "Der_A::Implementation\n"; } static void sImplementation(){ std::cout << "Der_A::sImplementation\n"; } }; class Der_B : public Base { public: void Implementation() { std::cout << "Der_B::Implementation\n"; } static void sImplementation(){ std::cout << "Der_B::sImplementation\n"; } }; int main(){ Der_A adx; adx.Interface(); // Base::Interface // Der_A::Implementation Der_B bdx; bdx.Interface(); // Base::Interface // Der_B::Implementation } * Örnek 2.2, Aşağıda da şöyle bir şey yapalım; türemiş sınıfların birer "print" işlevi gören fonksiyonları olduğuna güvenerek, türemiş sınıflara, bir "interface" enjekte edelim. Bu "interface" sayesinde bünyelerindeki "print" fonksiyonunun "N" kez çağrılması sağlansın. #include // CRTP Base: template class Base{ public: void Interface(std::size_t n) const { while (n--) static_cast(*this).Implementation(); } private: //... }; class Der_A : public Base { public: void Implementation() const { std::cout << "Der_A::Implementation\n"; } }; class Der_B : public Base { public: void Implementation() const{ std::cout << "Der_B::Implementation\n"; } }; int main(){ Der_A ax; ax.Interface(3); // Der_A::Implementation // Der_A::Implementation // Der_A::Implementation std::cout << "\n===================\n\n"; Der_B bx; bx.Interface(2); // Der_B::Implementation // Der_B::Implementation } * Örnek 3, Şimdi de türemiş sınıflara şöyle bir "Interface" ekleyelim; "<" operatör fonksiyonunu "overload" ederek "==" ve "!=" karşılaştırmalarını mümkün hale getirelim. #include #include template struct Equality { const Der& derived() const { return static_cast(*this); } friend bool operator==(const Equality& lhs, const Equality& rhs) { return not(lhs.derived() < rhs.derived()) and not(rhs.derived() < lhs.derived()); } friend bool operator!=(const Equality& lhs, const Equality& rhs) { return not(lhs == rhs); } }; class Person : public Equality { public: Person(const std::string& name = "NoName") : m_name{ name } {} bool operator<(const Person& other) const { return m_name < other.m_name; } private: std::string m_name{ "Ulya Yuruk" }; }; int main() { Person p1{ "Ulya" }, p2{ "Ayse" }; std::cout << std::boolalpha << (p1 == p2) << '\n'; // false std::cout << std::boolalpha << (p1 != p2) << '\n'; // true // Görüldüğü üzere taban sınıftaki "operator==" ve "operator!=" // fonksiyonlarına çağrı yapılmış. O çağrı sonucunda da türemiş // sınıftaki ".operator<" fonksiyonu çağrılmıştır. } * Örnek 4.0, Şimdi de daha gerçekçi bir örnek yapalım; #include #include #include template class Container { private: // Aşağıdaki iki fonksiyon tamamiyle yazım kolaylığı // sağlaması, ortak kodu tek bir yerde toplamak amacıyla // yazılmıştır. "private" olmaları hasebiyle de türemiş // sınıflarca çağrılamayacaklar. Der& derived() { return static_cast(*this); } const Der& derived() const { return static_cast(*this); } public: // Türemiş sınıflara aşağıdaki "Interface" verilecektir. Ancak bunun // için türemiş sınıflar ".begin()", ".end()" ve ".operator*()" // fonksiyonlarına sahip olması gerekmektedir. Yani fonksiyonların var // olması GEREKİYOR. decltype(auto) front() { return *derived().begin(); } decltype(auto) back() { return *std::prev(derived().end()); } decltype(auto) size() const { return std::distance(derived().begin(), derived().end()); } decltype(auto) operator[] (std::size_t i) { return *std::next(derived().begin(), i); } }; #include template class DynArray : public Container> { private: std::size_t size_; std::unique_ptr updata_; public: DynArray(std::size_t size) : size_{ size }, updata_{ std::make_unique(size_) } {} T* begin() { return updata_.get(); } const T* begin() const { return updata_.get(); } T* end() { return updata_.get() + size_; } const T* end() const { return updata_.get() + size_; } }; int main() { DynArray arr(10); arr.front() = 2; arr[2] = 5; arr.back() = 67; for(auto i: arr) std::cout << i << ' '; // 2 0 5 0 0 0 0 0 0 67 std::cout << '\n'; std::cout << "Size: " << arr.size() << '\n'; // Size: 10 } * Örnek 5.0, Yine gerçekçi örneklerle devam edelim; #include #include template class Singleton { private: inline static std::unique_ptr m_instance{}; inline static std::once_flag m_once{}; protected: Singleton() {} public: ~Singleton() {} Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; static TDerived& GetInstance() { // Bu fonksiyon ilk çağrıldığında, programın akışı // "call_once" fonksiyonuna bir kez girecek. "GetInstance" // fonksiyonuna yapılan diğer çağrılarda programın akışı // direkt "return" ifadesine gidecek. Böylelikle o türden // sadece bir adet nesnemiz olmuş olacak. std::call_once( Singleton::m_once, [](){ Singleton::m_instance.reset(new TDerived()); } ); return *m_instance; } }; #include class Myclass : public Singleton { public: Myclass() { std::cout << "Myclass ctor.\n"; } ~Myclass() { std::cout << "Myclass dtor.\n"; } void foo() { std::cout << "Myclass::foo().\n"; } }; int main() { Myclass::GetInstance().foo(); // Myclass ctor. // Myclass::foo(). // Myclass dtor. } * Örnek 6.1.0, Yine bir başka gerçekçi örneklerle devam edelim #include #include // CRTP Base template class Writer { public: void Write (const char* str) const { static_cast(this)->Write_Imp(str); } }; class FileWriter : public Writer { public: friend class Writer; // Bu "friend" bildiriminden dolayı taban sınıf, // iş bu türemiş sınıfın "private" bölümüne erişim // sağlayabilmektedir. FileWriter(const char* pFileName) : m_file{ std::fopen(pFileName, "w") } { using namespace std::literals; if (!m_file) throw std::runtime_error{ "file: "s + pFileName + "cannot being created\n" }; } ~FileWriter() { fclose(m_file); } private: void Write_Imp(const char* str) const { // Bu fonksiyonun "private" bölümde olmasına // dikkat edin. Taban sınıf, türemiş sınıfın "private" // bölümündeki bir fonksiyonu çağırabilmektedir. std::fprintf(m_file, "%s\n", str); } private: FILE* m_file; }; class ConsoleWriter : public Writer { public: friend class Writer; private: void Write_Imp(const char* str) const { printf("%s\n", str); } }; int main() { ConsoleWriter cw; cw.Write("Ulya"); // Ulya FileWriter fw{ "deneme.txt" }; fw.Write("Yuruk"); // Yuruk } * Örnek 6.1.1, Pekala bizler taban sınıf olarak birden fazla sınıf kullanabiliriz. Yani "Multiple Inheritence". #include #include // CRTP Base I template class FileWriter { public: void WriteToFile (const char* str) const { static_cast(this)->WriteToFile_Impl(str); } }; // CRTP Base II template class ConsoleWriter { public: void WriteToConsole (const char* str) const { static_cast(this)->WriteToConsole_Impl(str); } }; class Writer : public FileWriter, public ConsoleWriter { public: void WriteToFile_Impl(const char* str) const { FILE* fp = std::fopen("deneme.txt", "w"); fprintf(fp, "%s\n", str); fclose(fp); } void WriteToConsole_Impl(const char* str) const { printf("%s\n", str); } }; int main() { Writer wr_1; wr_1.WriteToFile("Ulya Yuruk"); wr_1.WriteToConsole("Uskudar"); } * Örnek 7, Birden fazla sınıfı kullanabildiğimiz gibi, türemiş sınıfı da şablon sınıf olarak kullanabiliriz. #include template struct MakeDouble { Der get_double_version() const { const auto& self = static_cast(*this);; // Bu sınıfı kullanacak müşteriler toplama operatörünün // operandı olabilmelidir. return self + self; } }; template struct MakeTriple { Der get_triple_version() const { const auto& self = static_cast(*this);; // Bu sınıfı kullanacak müşteriler toplama operatörünün // operandı olabilmelidir. return self + self + self; } }; template class Val : public MakeDouble>, public MakeTriple> { public: Val(const T& val) : m_val{ val } {} Val operator+(const Val& other) const { return m_val + other.m_val; } void print() const { std::cout << m_val << '\n'; } private: T m_val; }; int main() { using namespace std::literals; Val x(2023); auto r1 = x.get_double_version(); r1.print(); // 4046 Val y("Ulya Yuruk"s); auto r2 = y.get_triple_version(); r2.print(); // Ulya YurukUlya YurukUlya Yuruk } * Örnek 8.0, Şimdi de parametre paketi kullanalım. Böylelikle "n" tane sınıfı taban sınıf olarak kullanabiliriz. #include template class A { public: void f1() { std::cout << "A::f1\n"; static_cast(this)->f1_impl(); } }; template class B { public: void f2() { std::cout << "B::f2\n"; static_cast(this)->f2_impl(); } }; template class C { public: void f3() { std::cout << "C::f3\n"; static_cast(this)->f3_impl(); } }; // Her bir elemanı "Template Template Parameter" // olan, bir "Template Parameter Pack". Yani bu // pakette toplam "n" tane "Template Template Parameter" // var ve her bir tanesi bir adet "Type Template Parameter" // argümana sahip. Dolayısıyla "AB" sınıf şablonunu açarken, // yukarıdaki "A" ve "B" gibi olan sınıf şablonlarından "n" // tanesini taban sınıf olarak kullanabiliriz. template< templatetypename... Alpha > class ABC: public Alpha>... { // Burada ">>" sembolünün, // solundaki "..." atomu "ABC" sınıf şablonunun // "Variadic Template" olmasından ötürü; // sağındaki "..." atomu ise "Multiple Inheritence" // yapabilmek için. public: void f1_impl(){ std::cout << "AB::f1_impl\n"; } void f2_impl(){ std::cout << "AB::f2_impl\n"; } void f3_impl(){ std::cout << "AB::f3_impl\n"; } }; int main() { ABC mx; mx.f1(); mx.f2(); mx.f3(); /* # OUTPUT # A::f1 AB::f1_impl B::f2 AB::f2_impl C::f3 AB::f3_impl */ } * Örnek 8.1, İşte "n" tane sınıfı taban sınıf olarak kullanabileceğimiz, böylelikle istediğimiz "interface" i sunan sınıfları, kendi sınıfımızda toplayabileceğimiz, gerçekçi bir örnek; #include // CRTP Base I template class FileWriter { public: void WriteToFile (const char* str) const { static_cast(this)->WriteToFile_Impl(str); } }; // CRTP Base II template class ConsoleWriter { public: void WriteToConsole (const char* str) const { static_cast(this)->WriteToConsole_Impl(str); } }; template< templatetypename... WriterType > class FileConsoleWriter: public WriterType>... { public: void WriteToFile_Impl (const char* str) const { if (str) WriteToFile_Handler(str); } void WriteToConsole_Impl (const char* str) const { if (str) WriteToConsole_Handler(str); } private: void WriteToFile_Handler (const char* str) const { FILE* fp = std::fopen("deneme.txt", "w"); fprintf(fp, "%s\n", str); fclose(fp); } void WriteToConsole_Handler (const char* str) const { printf("%s\n", str); } }; using FCWriter = FileConsoleWriter; int main() { FCWriter wr1; wr1.WriteToFile_Impl("Ulya Yuruk"); FCWriter wr2; wr2.WriteToConsole_Impl("Uskudar, Istanbul"); } * Örnek 8.2.0, Ancak bunun da şöyle bir alternatif yolu daha vardır; "mixing" kullanmak. Yani bu sefer türemiş sınıflar, taban sınıftaki fonksiyonu çağırıyor, pekala taban sınıfta öyle bir fonksiyon olduğuna güvenerek. Yani aslında tersten gidiyoruz: #include template class Skill_A : public Base { public: void handle_A () { Base::init_handle_A(); // Bu fonksiyon çağrısı, taban sınıfta "init_handle_A" // isimli bir fonksiyon olduğuna güvenilerek yazılmıştır. //... std::cout << "Skill_A::handle_A\n"; } }; template class Skill_B : public Base { public: void handle_B () { Base::init_handle_A(); // Bu fonksiyon çağrısı, taban sınıfta "init_handle_A" // isimli bir fonksiyon olduğuna güvenilerek yazılmıştır. Base::init_handle_B(); // Bu fonksiyon çağrısı, taban sınıfta "init_handle_B" // isimli bir fonksiyon olduğuna güvenilerek yazılmıştır. //... std::cout << "Skill_B::handle_B\n"; } }; template class Skill_C : public Base { public: void handle_C () { Base::init_handle_A(); // Bu fonksiyon çağrısı, taban sınıfta "init_handle_A" // isimli bir fonksiyon olduğuna güvenilerek yazılmıştır. Base::init_handle_B(); // Bu fonksiyon çağrısı, taban sınıfta "init_handle_B" // isimli bir fonksiyon olduğuna güvenilerek yazılmıştır. Base::init_handle_C(); // Bu fonksiyon çağrısı, taban sınıfta "init_handle_C" // isimli bir fonksiyon olduğuna güvenilerek yazılmıştır. //... std::cout << "Skill_C::handle_C\n"; } }; class Base { public: void init_handle_A() { std::cout << "Base::init_handle_A\n"; } void init_handle_B() { std::cout << "Base::init_handle_B\n"; } void init_handle_C() { std::cout << "Base::init_handle_C\n"; } }; int main() { // Şimdi buradaki "Skill_A", "Skill_B" ve // "Skill_C" türleri ayrı birer sınıf türleridir. Skill_A{}.handle_A(); // "Skill_A" açılımında taban sınıf aslında "Base" // isimli sınıftır. std::cout << "\n=========================\n"; Skill_B{}.handle_B(); // "Skill_B" açılımında taban sınıf aslında "Base" // isimli sınıftır. std::cout << "\n=========================\n"; Skill_C{}.handle_C(); // "Skill_C" açılımında taban sınıf aslında "Base" // isimli sınıftır. } * Örnek 8.2.1, Yani bizler aslında "Base" sınıfına, başka sınıflar oluşturarak, yeni özellikler kazandırmış olduk. #include template class Skill_A : public Base { public: void handle_A () { Base::init_handle_A(); // Bu fonksiyon çağrısı, taban sınıfta "init_handle_A" // isimli bir fonksiyon olduğuna güvenilerek yazılmıştır. //... std::cout << "Skill_A::handle_A\n"; } }; template class Skill_B : public Base { public: void handle_B () { Base::init_handle_A(); // Bu fonksiyon çağrısı, taban sınıfta "init_handle_A" // isimli bir fonksiyon olduğuna güvenilerek yazılmıştır. Base::init_handle_B(); // Bu fonksiyon çağrısı, taban sınıfta "init_handle_B" // isimli bir fonksiyon olduğuna güvenilerek yazılmıştır. //... std::cout << "Skill_B::handle_B\n"; } }; template class Skill_C : public Base { public: void handle_C () { Base::init_handle_A(); // Bu fonksiyon çağrısı, taban sınıfta "init_handle_A" // isimli bir fonksiyon olduğuna güvenilerek yazılmıştır. Base::init_handle_B(); // Bu fonksiyon çağrısı, taban sınıfta "init_handle_B" // isimli bir fonksiyon olduğuna güvenilerek yazılmıştır. Base::init_handle_C(); // Bu fonksiyon çağrısı, taban sınıfta "init_handle_C" // isimli bir fonksiyon olduğuna güvenilerek yazılmıştır. //... std::cout << "Skill_C::handle_C\n"; } }; class Base { public: void init_handle_A() { std::cout << "Base::init_handle_A\n"; } void init_handle_B() { std::cout << "Base::init_handle_B\n"; } void init_handle_C() { std::cout << "Base::init_handle_C\n"; } }; using BaseWithSkill_A = Skill_A; using BaseWithSkill_B = Skill_B; using BaseWithSkill_C = Skill_C; using BaseWithSkill_AB = Skill_B>; using BaseWithSkill_BC = Skill_B>; using BaseWithSkill_CA = Skill_C>; using BaseWithSkill_ABC = Skill_A>>; int main() { BaseWithSkill_A a; a.handle_A(); // Base::init_handle_A // Skill_A::handle_A std::cout << "\n=========================\n"; BaseWithSkill_B b; b.handle_B(); // Base::init_handle_A // Base::init_handle_B // Skill_B::handle_B std::cout << "\n=========================\n"; BaseWithSkill_C c; c.handle_C(); // Base::init_handle_A // Base::init_handle_B // Base::init_handle_C // Skill_C::handle_C std::cout << "\n=========================\n"; BaseWithSkill_AB ab; ab.handle_A(); ab.handle_B(); // Base::init_handle_A // Skill_A::handle_A // Base::init_handle_A // Base::init_handle_B // Skill_B::handle_B std::cout << "\n=========================\n"; BaseWithSkill_BC bc; bc.handle_B(); bc.handle_C(); // Base::init_handle_A // Base::init_handle_B // Skill_B::handle_B // Base::init_handle_A // Base::init_handle_B // Base::init_handle_C // Skill_C::handle_C std::cout << "\n=========================\n"; BaseWithSkill_CA ca; ca.handle_C(); ca.handle_A(); // Base::init_handle_A // Base::init_handle_B // Base::init_handle_C // Skill_C::handle_C // Base::init_handle_A // Skill_A::handle_A std::cout << "\n=========================\n"; BaseWithSkill_ABC abc; abc.handle_A(); abc.handle_B(); abc.handle_C(); // Base::init_handle_A // Skill_A::handle_A // Base::init_handle_A // Base::init_handle_B // Skill_B::handle_B // Base::init_handle_A // Base::init_handle_B // Base::init_handle_C // Skill_C::handle_C } * Örnek 8.2.2, Görüleceği üzere oluşturduğumuz sınıflar aynı DEĞİL. #include #include template class SkillA : public T {}; template class SkillB : public T {}; template class SkillC : public T {}; class Nec {}; using type_1 = SkillA>>; // ABC using type_2 = SkillA>>; // ACB using type_3 = SkillB>>; // BAC using type_4 = SkillC>>; // CAB using type_5 = SkillB>>; // BCA using type_6 = SkillC>>; // CBA int main() { static_assert( !std::is_same_v && !std::is_same_v && !std::is_same_v && !std::is_same_v && !std::is_same_v ); // HOLDS } * Örnek 8.2.3, Şimdi gerçekçi bir örnek ile "mixing" yapısını irdeleyelim; #include #include #include template struct RepeatPrint: Printable { explicit RepeatPrint(const Printable& printable) : Printable(printable) {} using Printable::Printable; // Inherited "Ctor." : Taban sınıfın "ctor." // fonksiyonunu türemiş sınıfta kullanabileceğiz. void repeat(unsigned int n) const { while (n-- > 0) this->print(); // Buradaki "this" kullanımı, isim arama için, zorunludur; şablon olduğu için. } }; template RepeatPrint repeat_print(const Printable& printable) { return RepeatPrint(printable); } class Name { public: Name(std::string first_name, std::string second_name) : m_first_name{ std::move(first_name) }, m_second_name{ std::move(second_name) } {} void print() const { std::cout << m_second_name << ' ' << m_first_name << '\n'; } private: std::string m_first_name; std::string m_second_name; }; using RepeatPrintableNames = RepeatPrint; // "RepeatPrint" sınıfı, "Name" sınıfından, kalıtım // yoluyla oluşturulmuş olacak. int main() { RepeatPrintableNames my_name{ Name{"Ulya", "Yuruk"} }; // "RepeatPrint" içerisindeki "explicit" olarak betimlenen // ve taban sınıf türünden parametreye sahip "ctor." // fonksiyondan dolayı, "Name" türünden bir argüman ile // çağrı yapabildik. // Yuruk Ulya repeat_print(my_name).repeat(1); // "repeat_print" fonksiyonunun geri dönüş değeri "RepeatPrint" // türünden. Geri dönüş değerini de biz, "my_name" ile hayata getirmiş // olduk. "RepeatPrint" olması hasebiyle, "repeat" fonksiyonunu // çağırabildik. // Yuruk Ulya // Yuruk Ulya my_name.repeat(2); // Hakeza bu şekilde de bir çağrı yapabilirdik. RepeatPrintableNames my_surname{ "Uskudar", "Istanbul" }; // "RepeatPrint" içerisindeki "using" bildiriminden dolayı, // taban sınıfın "ctor." fonksiyonuna çağrı yapabildik. Çünkü // o "ctor." fonksiyon ismi bizim alanımıza enjekte edildi. // Istanbul Uskudar // Istanbul Uskudar // Istanbul Uskudar my_surname.repeat(3); } * Örnek 9.0, Aşağıda "chaining" mekanizmasına bir örnek verilmiştir; #include #include class Printer { public: Printer(std::ostream& pstream) : m_stream(pstream) {} template Printer& print(const T& t, const char Del = '\0') { m_stream << t << Del; return *this; } private: std::ostream& m_stream; }; int main() { Printer(std::cout).print("Ulya ").print("Yuruk.", '\n'); // Ulya Yuruk. } * Örnek 9.1.0, Şimdi de onu "CRTP" örüntüsüne dönüştürelim; "CRTP with chaining". #include #include class Printer { public: Printer(std::ostream& pstream) : m_stream(pstream) {} template Printer& print(const T& t, const char Del = '\0') { m_stream << t << Del; return *this; } private: std::ostream& m_stream; }; class ConsolePrinter : public Printer{ public: ConsolePrinter() : Printer(std::cout) {} ConsolePrinter& set_color(int color) { std::cout << "Console Color is set: " << color << '\n'; return *this; } }; int main() { ConsolePrinter().print("Ulya").set_color(12).print("Yuruk\n"); // ".print()" fonksiyonu "Printer" tür döndürmekte, dolayısıyla // ".set_color()" fonksiyonu da "Printer" içerisinde aranacak // fakat bulunamayacak. } * Örnek 9.1.1, #include #include template class Printer { public: Printer(std::ostream& pstream) : m_stream(pstream) {} template ConcretePrinter& print(const T& t, const char Del = '\0') { m_stream << t << Del; return static_cast(*this); } private: std::ostream& m_stream; }; class ConsolePrinter : public Printer{ public: ConsolePrinter() : Printer(std::cout) {} ConsolePrinter& set_color(int color) { std::cout << ""; return *this; } }; int main() { ConsolePrinter().print("Ulya").set_color(12).print("Yuruk\n"); // UlyaYuruk } * Örnek 10.0, "Virtual Dispatch" eliminasyonu, yani derleme zamanında kod seçimi, yani "Static Polymorphism"; #include class Animal { public: virtual void call() = 0; }; class Cat: public Animal { public: virtual void call() override { std::cout << "A cat sound!\n"; } }; class Dog: public Animal { public: virtual void call() override { std::cout << "A dog sound!\n"; } }; class Lamb: public Animal { public: virtual void call() override { std::cout << "A Lamb sound!\n"; } }; int main() { Cat cat; cat.call(); // A cat sound! Dog dog; dog.call(); // A dog sound! Lamb lamb; lamb.call(); // A Lamb sound! } * Örnek 10.1, #include template class Animal { public: void call() { static_cast(this)->call_impl(); } }; class Cat: public Animal { public: void call_impl() { std::cout << "A cat sound!\n"; } }; class Dog: public Animal { public: void call_impl() { std::cout << "A dog sound!\n"; } }; class Lamb: public Animal { public: void call_impl() { std::cout << "A Lamb sound!\n"; } }; template void pet_call(Animal& x) { x.call(); } int main() { Cat cat; cat.call(); // A cat sound! pet_call(cat); // A cat sound! Dog dog; dog.call(); // A dog sound! pet_call(dog); // A dog sound! Lamb lamb; lamb.call(); // A Lamb sound! pet_call(lamb); // A Lamb sound! /* Ancak "Cat", "Dog" ve "Lamb" türleri sınıf şablonundan türediğinden, bu türleri "std::vector" gibi veri yapısında tutamıyoruz. "std::variant" türünü kullanabiliriz ki o da zaten "virtual dispatch" için ayrı bir alternatif. Ancak şöyle bir şey yapabiliriz; ".call()" çağrısını bir fonksiyon şablonuna alır, o fonksiyon üzerinden, ".call()" çağrısını yaptırtabiliriz. */ } > "10. Cpp Idioms/Patterns > "Pipeline operator" : Anımsanacağı üzere C dilindeki "enum" türlerini, sadece bir "bit" leri "1" olacak şekilde, maskeleme amacıyla da kullanmaktayız. Şöyleki; * Örnek 1, enum Color { yellow = 1, // 0001 red = 2, // 0010 blue = 4, // 0100 }; int main() { auto purple = red | blue; auto green = yellow | blue; auto orange = yellow | red; } Ancak bu tip "enum" türleri de şöyle handikaplara sahiptirler; "unscoped" olmaları, "int" türüne örtülü olarak dönüşmeleri, vb. Modern C++ ile birlikte "scoped enum" kavramının da dile eklenmesiyle bu handikaplar giderildi. Fakat bu sefer de "scoped enum" türünü, yukarıdaki örnekte olduğu gibi, varsayılan olarak "|" operatörüne operand yapamıyoruz. İşte bu problemi çözmek için de "global" bir "operator |" fonksiyonu tanımlıyoruz. Hatırlarsanız "scoped enum" türleri bir sınıf olmadığından, üye fonksiyon kavramı SÖZ KONUSU DEĞİLDİR. İşte "operator |" fonksiyonunu şu şekilde tanımlayabiliriz; * Örnek 1, #include #include enum class Color { yellow = 1, // 0001 red = 2, // 0010 blue = 4, // 0100 }; Color operator | (Color lhs, Color rhs) { using underlying_type = std::underlying_type_t; // "Color" için sadece bildirim de olabilirdi, dolayısıyla // arka plandaki türü öğrenmek için bu bildirim gerekmektedir. auto result = static_cast(lhs) | static_cast(rhs); return static_cast(result); } std::ostream& operator<<(std::ostream& os, Color color) { using underlying_type = std::underlying_type_t; return os << static_cast(color); } int main() { std::cout << Color::yellow << '\n'; // 1 std::cout << Color::red << '\n'; // 2 std::cout << Color::blue << '\n'; // 4 std::cout << "\n==============\n\n"; Color purple = Color::red | Color::blue; std::cout << purple << '\n'; // 2 + 4 = 6 auto green = Color::yellow | Color::blue; std::cout << green << '\n'; // 1 + 4 = 5 auto orange = Color::yellow | Color::red; std::cout << orange << '\n'; // 1 + 2 = 3 } > "11. Cpp Idioms/Patterns > "Using 'Command Line Arguments' in one-line" : Komut satırı argümanlarını, "std::vector" içerisinde saklamak isteyelim; * Örnek 1, #include #include #include int main(int argc, char** argv) { // Ulya Yuruk , Istanbul , Uskudar std::vector args{ argv, argv + argc }; for (auto i: args) std::cout << i << ' '; // ./a.out Ulya Yuruk , Istanbul , Uskudar std::cout << '\n'; } > "12. Cpp Idioms/Patterns > 'printf' function": "print" fonksiyonu gibi formatlama özelliklerine sahip ancak ekrana yazma yerine bir "std::string" döndüren bir fonksiyon yazalım; * Örnek 1, #include #include #include #include // C dilindeki "Variadic" parametreli fonksiyon: std::string printf_to_string(const char* fmt, ...) { va_list args_main; va_list args_copy; va_start(args_main, fmt); // Inits "args_main". va_copy(args_copy, args_main); // Copyies "args_main" to "args_copy". std::string str( vsnprintf(nullptr, 0, fmt, args_copy) + 1, // Toplam karakter adedi '\0' // Her bir karakter ); // "Fill Ctor." va_end(args_copy); // Stops "args_copy". vsprintf(str.data(), fmt, args_main); // Fills the "str". va_end(args_main); // Stops "args_main". str.pop_back(); // Yazının sonundaki '\0' karakteri de metne dahil edildiğinden, // onu çıkartıyoruz. return str; } int main(int argc, char** argv) { char name[] = "Ulya Yuruk"; int age = 28; double wage = 25000.5; printf("Name: %s, Age: %d, Wage: %.1f", name, age, wage); // Name: Ulya Yuruk, Age: 28, Wage: 25000.5 std::cout << "\n=================\n"; auto the_string = printf_to_string("Name: %s, Age: %d, Wage: %.1f", name, age, wage); std::cout << the_string; // Name: Ulya Yuruk, Age: 28, Wage: 25000.5 } > "Explicit Object Parameter" : C++23 ile dile eklenmiştir. "non-static" üye fonksiyonların gizli bir parametre değişkeni vardır ve o sınıf türünden bir göstericidir. Eğer fonksiyon "const" ise iş bu gizli parametre de "const" olur, yani "const T*". İşte bu gizli parametreyi açık hale getirebiliriz ve ismi ile kullanabiliriz. Fonksiyonun birinci parametresi olmalıdır. Kullanım biçimi şu şekildedir; -> "this" anahtar sözcüğünü yazıyoruz. -> Sonrasında sınıfın türünü yazıyoruz. -> Eğer referans semantiği ile kullanacaksak "&", gösterici semantiği kullanacaksak "*" atomunu yazıyoruz. Pekala bu iki semantiği KULLANMAYADABİLİRİZ. Bu durumda nesnenin bir kopyası üzerinde işlem yapmış oluruz. -> Bu aşamada parametreyi "L-Value Reference", "R-Value Reference", "const L-Value Reference", "const R-Value Reference" olarak da kullanabiliriz. -> Son olarak da bu parametre için bir isim yazıyoruz. Dolayısıyla fonksiyonun imzasındaki ilk parametre aşağıdaki gibi olabilir; "this T self" "this T& self" "this T* self" "this const T& self" "this const T* self" "this T&& self" "this const T&& self" Ancak bu şekilde bir parametre kullanırsak, veri elemanlarını direkt isimleriyle veya "this" göstericileri ile niteleyerek kullanamayız. * Örnek 1.0, #include class Myclass { public: Myclass(int x) : mx{ x } {} void set(int x) { mx = x; } void print() const { std::cout << mx << '\n'; } private: int mx{}; }; int main() { Myclass mc(20); } * Örnek 1.1, #include class Myclass { public: Myclass(int x) : mx{ x } {} void set(this Myclass& self, int x) { // mx; // ERROR // this.mx; // ERROR self.mx; // OK: "self" demek "*this" demektir. } void print() const { std::cout << mx << '\n'; } private: int mx{}; }; int main() { Myclass mc(20); mc.print(); // 20 mc.set(31); mc.print(); // 31 } * Örnek 1.2, #include class Myclass { public: Myclass(int x) : mx{ x } {} void foo(this Myclass self) { self.mx = 999; } void print() const { std::cout << mx << '\n'; } int mx{ 35 }; }; int main() { Myclass a; std::cout << a.mx << '\n'; // 35 a.foo(); std::cout << a.mx << '\n'; // 35 } Tabii "Explicit Object Parameter" ile, fonksiyon imzalarında belittiğimiz "Reference Qualifiers" yerine de kullanabiliriz; * Örnek 1.0, #include class Neco { public: // Bu fonksiyon sadece "L-Value Reference" türler tarafından çağrılabilir. void foo() & { std::cout << "foo() &\n"; } // Bu fonksiyon sadece "R-Value Reference" türler tarafından çağrılabilir. void foo() && { std::cout << "foo() &&\n"; } // Bu fonksiyon sadece "const L-Value Reference" türler tarafından çağrılabilir. void foo() const & { std::cout << "foo() const &\n"; } // Bu fonksiyon sadece "const R-Value Reference" türler tarafından çağrılabilir. void foo() const && { std::cout << "foo() const &&\n"; } }; int main() { // "L-Value Reference" Neco nec1; nec1.foo(); // foo() & // "R-Value Reference" Neco{}.foo(); // foo() && // "const L-Value Reference" const Neco nec2; nec2.foo(); // foo() const & // "const R-Value Reference" std::move(nec2).foo(); // foo() const && } * Örnek 1.1, #include class Neco { public: // Bu fonksiyon sadece "L-Value Reference" türler tarafından çağrılabilir. void foo(this Neco&) { std::cout << "foo() &\n"; } // Bu fonksiyon sadece "R-Value Reference" türler tarafından çağrılabilir. void foo(this Neco&&) { std::cout << "foo() &&\n"; } // Bu fonksiyon sadece "const L-Value Reference" türler tarafından çağrılabilir. void foo(this const Neco&) { std::cout << "foo() const &\n"; } // Bu fonksiyon sadece "const R-Value Reference" türler tarafından çağrılabilir. void foo(this const Neco&&) { std::cout << "foo() const &&\n"; } }; int main() { // "L-Value Reference" Neco nec1; nec1.foo(); // foo() & // "R-Value Reference" Neco{}.foo(); // foo() && // "const L-Value Reference" const Neco nec2; nec2.foo(); // foo() const & // "const R-Value Reference" std::move(nec2).foo(); // foo() const && } Şimdi de "Explicit Object Parameter" kullanmanın avantajlarını irdeleyelim; >> Şablonlarda kod tekrarının önüne geçmektedir. Şöyleki; * Örnek 1.0, Görüldüğü gibi dört fonksiyonun yaptığı iş aynı. Sadece kendisini çağıran nesne değişmekte. #include template class Optional { bool has_value() const; // For "non-const L-Value": constexpr T& value() & { if (has_value()) return this->m_value; throw std::bad_optional_access(); } // For "const L-Value": constexpr const T& value() const & { if (has_value()) return this->m_value; throw std::bad_optional_access(); } // For "non-const R-Value": constexpr T&& value() && { if (has_value()) return std::move(this->m_value); throw std::bad_optional_access(); } // For "const R-Value": constexpr T&& value() const && { if (has_value()) return std::move(this->m_value); throw std::bad_optional_access(); } //... }; * Örnek 1.1, İş bu fonksiyonları tek bir fonksiyon haline şu şekilde getirebiliriz; #include template class Optional { bool has_value() const; template constexpr auto&& value(this Self&& self) { // Forwarding Reference if(self.has_value()) return std::forward(self).m_value; throw std::bad_optional_access(); } //... }; * Örnek 2, struct Neco { template void foo(this T&& self) { //... } }; int main() { Neco mynec; mynec.foo(); // "T" => "Neco&". // "self" => "Neco&". const Neco cmynec; cmynec.foo(); // "T" => "const Neco&". // "self" => "const Neco&". std::move(mynec).foo(); // "T" => "Neco". // "self" => "Neco&&". std::move(cmynec).foo(); // "T" => "const Neco". // "self" => "const Neco&&". } >> Yine şablon kullanarak, "CRTP" için alternatif oluşturabiliriz. * Örnek 1, #include #include struct Base { template void foo(this Self&& self) { std::cout << typeid(self).name() << '\n'; } }; struct Der_I : Base { }; struct Der_II : Base { }; struct Der_III : Base { }; int main() { Base my_base; my_base.foo(); // 4Base Der_I der_I; der_I.foo(); // 5Der_I Der_II der_II; der_II.foo(); // 6Der_II Der_III der_III; der_III.foo(); // 7Der_III } * Örnek 2.0, // CRTP Base template struct postfix_inc { Der operator++(int) { auto& self = static_cast(*this); Der tmp(self); ++self; // ".operator++()" fonksiyonunun "overload" edildiğine güvenilerek // bu fonksiyon yazılmıştır. return tmp; } }; struct Nec : postfix_inc { Nec& operator++(); }; int main() {} * Örnek 2.1, struct postfix_inc { template auto operator++(this Self&& self, int) { auto tmp(self); ++tmp; // ".operator++()" fonksiyonunun "overload" edildiğine güvenilerek // bu fonksiyon yazılmıştır. return tmp; } }; struct Nec : postfix_inc { Nec& operator++(); }; >> "Recursive Lambda" oluşturmayı daha kolay hale getirmektedir. Normalde direkt olarak "Recursive Lambda" mevcut değildir, bir takım teknikler ile oluşturmaktayız. Artık daha kolay hale getirmektedir. * Örnek 1, #include int main() { auto f = [](this auto&& self, int n){ //... if (n == 0) return 1; else return n * self(n-1); }; std::cout << f(6) << '\n'; // 720 } * Örnek 2, #include int main() { auto gcd = [](this auto self, int a, int b) -> int { return b == 0 ? a : self(b, a % b); }; std::cout << gcd(12, 40) << '\n'; // 4 } > Hatırlatıcı Notlar: >> "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. * Ö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 /*================================================================================================================================*/ (32_04_11_2023) & (33_05_11_2023) > "strong types" : "chrono" kütüphanesindeki "duration" türleri çok güzel bir örnek olarak verilebilir. Gerçek hayattaki şeyler için dilin "primitive type" larını sarmalanması konusudur. Örneğin, mesafe bilgisi için bizler "float" değişken kullandığımızı varsayalım. Bu değişken için "int" türden bir sabit atarsak, şöyle problemler ile karşılaşabiliriz; -> Örtülü tür dönüşümü gerçekleşebilir. -> Okunabilirlik azalır. İşte bu gibi problemleri gidermek adına, tıpkı "chrono" kütüphanesindeki "milliseconds" tür eş isminde olduğu gibi, temel türlerimizi bir şekilde sarmalıyoruz. "Assembly" düzeyinde yine "primitive type" lar üzerinde işlem yapılırken, dil katmanında sarmalanmış halleri üzerinde işlem yapılmaktadır. * Örnek 1, İşte bir milisaniyeyi temsil etmesi için standart kütüphanedeki "std::chrono::milliseconds" isimli bir tür kullanılmıştır. #include #include #include int main() { std::cout << "Type: " << typeid(std::chrono::milliseconds).name() << '\n'; // Type: class std::chrono::duration<__int64,struct std::ratio<1,1000> > } Şimdi de bizler bu amaca yönelik olarak bir "stront_type" oluşturalım; * Örnek 1, "double" türünün "Kilogram" ile temsil edilmesi; #include #include #include class Kilogram { public: class PreventUsage {}; explicit constexpr Kilogram(PreventUsage, double value) : m_val{ value } {} double get() const { return m_val; } friend constexpr Kilogram operator+(const Kilogram& lhs, const Kilogram& rhs) { return Kilogram{ Kilogram::PreventUsage{}, lhs.m_val + rhs.m_val }; } //... private: double m_val; }; constexpr Kilogram operator""_kg(long double value) { return Kilogram{ Kilogram::PreventUsage{}, static_cast(value) }; } constexpr Kilogram operator""_g(long double value) { return Kilogram{ Kilogram::PreventUsage{}, static_cast(value / 1000) }; } constexpr Kilogram operator""_mg(long double value) { return Kilogram{ Kilogram::PreventUsage{}, static_cast(value / 1000000) }; } int main() { constexpr auto result = 2.9_kg + 734.7234_g + 123456.654321_mg; // 3.758180054321 std::cout << "Type: " << typeid(result).name() << '\n'; // Type: class Kilogram } Şimdi de bizler daha "generic" bir versiyonunu yazalım. Bu versiyon sayesinde "Kilogram" için ayrı, "Meter" için ayrı bir sınıf/sınıf şablonu yazmamıza gerek kalmasın, "birim" bilgisi için tek bir sınıf şablonu kullanalım; -> İlk olarak sınıf şablonumuzu tanımlıyoruz: -> Daha sonra bu sınıf şablon türümüz için tür eş isim(ler)i bildiriyoruz. Aşağıda bu iki adıma ilişkin örnek verilmiştir; * Örnek 1, #include template struct NamedType { //... }; using Kilogram = NamedType; using Meter = NamedType; int main() { constexpr auto is_same = std::is_same_v; // true } Şimdi "Kilogram" ve "Meter" türleri derleyici açısından da birbirinin aynısı olarak ele alınmıştır. Fakat bizim istediğimiz bu gibi durumlarda türlerin aynı olmamasını sağlamaktır. Bunu gerçekleştirmek için; -> Şablonumuza ikinci bir parametre daha ekliyoruz. Bu parametre etiketlem için kullanılacaktır. Aşağıda bu adıma yönelik örnekler verilmiştir; * Örnek 1.0, Aşağıdaki örnekte yukarıda bahsedilen amaca ulaşılmıştır. Fakat "KilogramTag", "MeterTag" türleri için pekala "incomplete type" da kullanabiliriz. #include template struct NamedType { //... }; struct KilogramTag {}; struct MeterTag {}; using Kilogram = NamedType; using Meter = NamedType; int main() { constexpr auto is_same = std::is_same_v; // false } * Örnek 1.1, Aşağıda "Tag" türlere ilişkin "incomplete type" kullanılmıştır. #include template struct NamedType { //... }; using Kilogram = NamedType; using Meter = NamedType; int main() { constexpr auto is_same = std::is_same_v; // false } * Örnek 1.2, Ancak tür eş isim bildirimlerinde "incomplete type" kullanmak yerine, şablon parametresini "non-type template parameter" yaparak da aynı sonuca ulaşabiliriz. Böylelikle her tür için, o türe ilişkin ayrı bir "Tag" türü yazmamıza gerek kalmayacaktır. Ancak bunun için C++20 gerekmektedir. #include template struct NamedType { //... }; using Kilogram = NamedType; using Meter = NamedType; int main() { constexpr auto is_same = std::is_same_v; // false } * Örnek 1.3, Eğer bir nedenden dolayı "non-type template parameter" kullanamıyorsak, pekala aynı sonuca "type tempalte parameter" ile de ulaşabiliriz; #include template struct NamedType { //... }; using Kilogram = NamedType; using Meter = NamedType; int main() { constexpr auto is_same = std::is_same_v; // false } Şimdi de sınıfımız için fonksiyonlar belirleyelim; -> "Ctor." fonksiyonlarının "explicit" olması halinde "=" ile yapılan atamalar sentaks hatası oluşturacaktır. -> "getter" fonksiyonlarının referans döndürmesi tavsiye edilir, böylelikle veri elemanının kendisine ulaşmış olacağız. Aşağıda bu adımlara yönelik örnek verilmiştir; * Örnek 1, #include template class NamedType { public: // "Copy Ctor." explicit constexpr NamedType(const T& value) : m_val{ value } {} // "Move Ctor." explicit constexpr NamedType(T&& value) : m_val{ std::move(value) } {} T& get() { return m_val; } const T& get() const { return m_val; } private: T m_val; }; using Kilogram = NamedType; using Meter = NamedType; int main() { constexpr auto is_same = std::is_same_v; // false } Şimdi de bu sınıfımız için "operator" fonksiyonları belirleyelim; -> Burada dikkat etmemiz gereken nokta bir tür için mantıklı olan "operator" fonksiyonu, başka tür için mantıksız gelebilir. Pekiyi bunu nasıl belirleyeceğiz? Tabii ki "CRTP" örüntüsünü kullanarak. Aşağıda bu adımlara yönelik örnek verilmiştir; * Örnek 1, #include // CRTP Base templatetypename> struct crtp { T& underlying() { return static_cast(*this); } const T& underlying() const { return static_cast(*this); } }; // Gives our class Addable Skill. template struct Addable : crtp { T operator+(const T& other) const{ // Bu aşamada türemiş sınıflarda ".get()" fonksiyonunun bulunduğuna // güveniyoruz. // return T(crtp::underlying().get() + other.get()); return T(this->underlying().get() + other.get()); } }; // Gives our class Multipliable Skill. template struct Multipliable : crtp { T operator*(const T& other) const{ // Bu aşamada türemiş sınıflarda ".get()" fonksiyonunun bulunduğuna // güveniyoruz. return T(this->underlying().get() * other.get()); } }; // Gives our class Incrementable Skill. template struct Incrementable : crtp { T& operator+=(const T& other) { // Bu aşamada türemiş sınıflarda ".get()" fonksiyonunun bulunduğuna // güveniyoruz. this->underlying().get() += other.get(); return this->underlying(); } }; // Gives our class Printable Skill. template struct Printable : crtp { std::ostream& print(std::ostream& os) const { return os << this->underlying().get(); } }; //... // Here, other skill-giver class templates, such as Dividable, are defined. //... // This is our Base Class. templatetypename... Skills> class NamedType : public Skills>... { /* * "NamedType" is for class NamedType, aka CRTP. * "Skills>..." is for Multiple Inheritence. */ public: explicit NamedType(const T& value) : m_val{ value } {} explicit NamedType(T&& value) : m_val{ std::move(value) } {} T& get() { return m_val; } const T& get() const { return m_val; } private: T m_val; }; templatetypename... Skills> std::ostream& operator<<(std::ostream& os, const NamedType& x) { return x.print(os); } // These are our classes with desired skills. using Kilogram = NamedType; using Meter = NamedType; int main() { Meter mtr_first{ 31.89 }; Meter mtr_second{ 98.13 }; std::cout << mtr_first << '\n'; // 31.89 std::cout << mtr_second << '\n'; // 98.13 std::cout << mtr_first + mtr_second << '\n'; // 130.02 std::cout << mtr_first * mtr_second << '\n'; // 3129.37 mtr_first += mtr_second; mtr_second += mtr_first; std::cout << mtr_first << '\n'; // 130.02 std::cout << mtr_second << '\n'; // 228.15 } > "std::exchange" : C++14 ile dile eklenmiştir. Bir değişkene yeni değerini atayan, geri dönüş değeri de o değişkenin eski değeri olan bir fonksiyondur. "utility" başlık dosyasındadır. * Örnek 1, Temsili implementasyonu aşağıdaki gibidir; #include template constexpr // Since C++20 T exchange(T& obj, U&& new_value) noexcept ( std::is_nothrow_move_constructible_v && std::is_nothrow_assignable_v ) // Since C++23 { T old_value = std::move(obj); obj = std::forward(new_value); return old_value; } * Örnek 2, Tipik kullanım yerlerinden bir tanesi de "Move Ctor." fonksiyonlardadır. #include #include class Myclass { public: Myclass(Myclass&& other) noexcept : m_val{ std::exchange(other.m_val, 0) } { // Böylelikle hem "other.m_val" in değerini "m_val" değişkenine, // hem de "other.m_val" değişkeninin değerini "0" a çekmiş olduk. // Ek olarak daha az kod yazmış olduk. } Myclass& operator=(Myclass&& other) noexcept { if (this != &other) m_val = std::exchange(other.m_val, 0); return *this; } private: int m_val; }; * Örnek 3, C dilindeki "strcpy" fonksiyonunu da iş bu fonksiyon ile düzenleyebiliriz. #include // In C, char* strcpy_1(char* pdest, const char* psource) { while (*pdest++ = *psource++) ; // Null Statement return pdest; } // In Cpp, char* strcpy_2(char* pdest, const char* psource) { for (;;) { auto source = std::exchange(psource, psource + 1); auto destiny = std::exchange(pdest, pdest + 1); *destiny = *source; if (*destiny == '\0') break; } return pdest; } * Örnek 4, "fibbo" açılımında da kullanılabilir. #include #include int main() { /* # OUTPUT # Fib of 0 = 0 Fib of 1 = 1 Fib of 2 = 1 Fib of 3 = 2 Fib of 4 = 3 Fib of 5 = 5 ... Fib of 85 = 259695496911122585 Fib of 86 = 420196140727489673 Fib of 87 = 679891637638612258 Fib of 88 = 1100087778366101931 Fib of 89 = 1779979416004714189 */ for (auto counter{ 0LL }, index_1{ 0LL }, index_2{ 1LL }; counter < 90; index_1 = std::exchange(index_2, index_1 + index_2), ++counter) { std::print("Fib of {} = {}\n", counter, index_1); } } * Örnek 5, #include #include void foo() { std::cout << "foo\n"; } void bar() { std::cout << "bar\n"; } int main() { auto f_ptr = foo; f_ptr(); // foo auto f_ptr_old = std::exchange(f_ptr, bar); f_ptr_old(); // foo f_ptr(); // bar } > "std::clamp" : İki tane "overload" versiyonu vardır. Dİğer versiyonu, bir adet karşılaştırma kriteri alır ki varsayılan durumda "std::less" dir. "algorithm" başlık dosyasındadır. İşlevi şudur: Bizim bir aralığımız var. Elimizde de değerler var. Elimizdeki değerlerden; -> Aralığın başlangıç değerinden küçük olanlarını, aralığın başlangıç değerine eşitliyoruz. -> Aralığın bitiş değerinden büyük olanlarını, aralığın bitiş değerine eşitliyoruz. -> Aralıktaki diğer değerler kendi değerlerinde kalıyor. * Örnek 1, Temsili implementasyonu aşağıdaki gibidir; template constexpr const T& clamp(const T& value, const T& low, const T& high) { return clamp(value, low, high, std::less{}); } template constexpr const T& clamp(const T& value, const T& low, const T& high, Compare comp) { return comp(value, low) ? low : comp(high, value) ? high : value; } * Örnek 2.0, #include #include int main() { int low = 5; int high = 45; std::cout << std::clamp(0, low, high) << '\n'; // 5 std::cout << std::clamp(31, low, high) << '\n'; // 31 std::cout << std::clamp(100, low, high) << '\n'; // 45 } * Örnek 2.1, #include #include #include int main() { std::array arr{ -3, 12, 9, 21, -6, 56, -8, 5, -4, 12 }; for (auto i : arr) std::cout << i << ' '; // -3 12 9 21 -6 56 -8 5 -4 12 std::cout << '\n'; int low{ 0 }, high{ 15 }; std::transform(begin(arr), end(arr), begin(arr), [low, high](int x) { return std::clamp(x, low, high); }); for (auto i : arr) std::cout << i << ' '; // 0 12 9 15 0 15 0 5 0 12 std::cout << '\n'; } > "13. Cpp Idioms/Patterns > Pimpl Idiom": Anımsanacağı üzere göstericilerde iki farklı "const" türü vardır. Bunlar, "Top Level const" ve "Low Level const" isimleriyle geçmektedir. "Top Level const" söz konusu olduğunda göstericinin bizzat kendisi "const" iken, "Low Level const" söz konusu olduğunda gösterilen nesnenin bizzat kendisi "const" olmaktadır. Şöyleki; * Örnek 1, Göstericiler söz konusu olduğunda durum aşağıdaki gibidir. int main() { // No constness: { int x = 10; int y = 100; int* ptr_x = &x; // "ptr_x" is a POINTER to "x". *ptr_x = 20; // OK: "x" is now "20" ptr_x = &y; // OK: "ptr_x" is a POINTER to "y". *ptr_x = 40; // OK: "y" is now "20" } // Top Level const: { int x = 10; int y = 100; int* const c_ptr_x = &x; // "c_ptr_x" is a "CONST POINTER" to "x". *c_ptr_x = 20; // OK: "x" is now "20" // c_ptr_x = &y; // ERROR: cannot bind "CONST POINTER" to another value. *c_ptr_x = 40; // OK: "x" is now "40" } // Low Level const: { int x = 10; int y = 100; const int* ptr_c_x = &x; // "ptr_c_x" is a "POINTER" to "const x". // *ptr_c_x = 20; // ERROR: cannot change the value of a "const" variable. ptr_c_x = &y; // OK: "ptr_c_x" is a POINTER to "y". // *ptr_c_x = 40; // ERROR: cannot change the value of a "const" variable. } // Full of const: { int x = 10; int y = 100; const int* const c_ptr_c_x = &x; // "c_ptr_c_x" is a "CONST POINTER" to "const x". // *c_ptr_c_x = 20; // ERROR: cannot change the value of a "const" variable. // c_ptr_c_x = &y; // ERROR: cannot bind "CONST POINTER" to another value. // *c_ptr_c_x = 40; // ERROR: cannot change the value of a "const" variable. } } * Örnek 2, İteratörler söz konusu olduğunda da durum aşağıdaki gibidir. #include int main() { // No constness: { std::vector ivec{ 17, 9, 1993 }; std::vector::iterator iter = ivec.begin(); // "iter" is a "ITERATOR" to the "first element of the vector". *iter = 20; // OK: The "first element of the vector" is "20" now. iter = ivec.end() - 1; // OK: "iter" is a ITERATOR to previous of ".end()". *iter = 2024; // OK: "1993" is now "2024" } // Top Level const: { std::vector ivec{ 17, 9, 1993 }; // const auto iter = ivec.begin(); const std::vector::iterator iter = ivec.begin(); // "iter" is a "CONST ITERATOR" to the "first element of the vector". *iter = 20; // OK: The "first element of the vector" is "20" now. // iter = ivec.end() - 1; // ERROR: cannot bind "CONST ITERATOR" to another value. *iter = 2024; // OK: The "first element of the vector" is "2024" now. } // Low Level const: { std::vector ivec{ 17, 9, 1993 }; // auto c_iter = ivec.cbegin(); std::vector::const_iterator c_iter = ivec.begin(); // "iter" is a "CONST_ITERATOR" to the "first element of the vector". // *c_iter = 20; // ERROR: cannot change the value of a "const" variable. c_iter = ivec.end() - 1; // OK: "iter" is a ITERATOR to previous of ".end()". // *c_iter = 2024; // ERROR: cannot change the value of a "const" variable. } // Full of const: { std::vector ivec{ 17, 9, 1993 }; // const auto c_iter = ivec.cbegin(); const std::vector::const_iterator c_iter = ivec.begin(); // "iter" is a "CONST CONST_ITERATOR" to the "first element of the vector". // *c_iter = 20; // ERROR: cannot change the value of a "const" variable. // c_iter = ivec.end() - 1; // ERROR: cannot bind "CONST ITERATOR" to another value. // *c_iter = 2024; // ERROR: cannot change the value of a "const" variable. } } İşte "const" konusundaki bu farklılık lojik açıdan geçersiz ancak sentaks açısından geçerli durumların meydana gelmesine neden olabilmektedir. Şöyleki; * Örnek 1.0, Aşağıdaki örnekteki "Erg" sınıfının "g" fonksiyonu bir "const" fonksiyondur, yani bu fonksiyon çağrısı sonucunda nesnenin problem düzleminde ima ettiği şeyin değişmemesi, yani "state" bilgisinin aynı kalması gerekmektedir. Fakat bünyesinde yapılan "foo_non_const" çağrısından dolayı lojik açıdan geçersiz durum oluşturmaktadır. #include class Neco { public: void foo_non_const() { std::cout << "non_const\n"; } void foo_const() const { std::cout << "const\n"; } }; class Erg { public: void f() { ptr->foo_non_const(); // OK ptr->foo_const(); // OK // c_ptr->foo_non_const(); // cannot call "non-const" functions w/ "const" variables. c_ptr->foo_const(); // OK } void g() const { ptr->foo_non_const(); // OK ptr->foo_const(); // OK // c_ptr->foo_non_const(); // cannot call "non-const" functions w/ "const" variables. c_ptr->foo_const(); } private: Neco* ptr{}; const Neco* c_ptr{}; }; int main() { Erg erg; erg.f(); // non_const // const erg.g(); // non_const // const const Erg c_erg; // c_erg.f(); // cannot call "non-const" functions w/ "const" variables. c_erg.g(); // non_const // const } İşte bu probleme ilişkin şöyle bir çözümler üretilebilir; * Örnek 1, #include class Neco { public: void foo_non_const() { std::cout << "non_const\n"; } void foo_const() const { std::cout << "const\n"; } }; class Erg { public: void f() { to_ptr()->foo_non_const(); // OK to_ptr()->foo_const(); // OK } void g() const { // to_ptr()->foo_non_const(); // cannot call "non-const" functions w/ "const" variables. to_ptr()->foo_const(); // OK } private: const auto* to_ptr() const { return ptr; } auto* to_ptr() { return ptr; } Neco* ptr{}; }; int main() { Erg erg; erg.f(); // non_const // const erg.g(); // non_const // const } * Örnek 2, #include template class const_ptr { public: explicit const_ptr(T* ptr) : m_ptr{ ptr } {} const T& operator*()const { return *m_ptr; } const T* operator->()const { return m_ptr; } T& operator*(){ return *m_ptr; } T* operator->(){ return m_ptr; } private: T* m_ptr; }; class Neco { public: void foo_non_const() { std::cout << "non_const\n"; } void foo_const() const { std::cout << "const\n"; } }; class Erg { public: void f() { ptr->foo_non_const(); // OK ptr->foo_const(); // OK } void g() const { ptr->foo_non_const(); // cannot call "non-const" functions w/ "const" variables. ptr->foo_const(); // OK } private: const_ptr ptr; }; int main() { } Üretilen çözümlerden de görüleceği üzere esas sınıfımızın "private" kısmında sadece gösterici bulunmaktadır. İş bu gösterici ise eskiden "private" bölümde olan şeyleri tutan bir yapı/sınıf türünden. Böylelikle bizler sınıfın "interface" i ile implementasonu arasındaki bağımlılığı kaldırmış oluyoruz. Böylelikle implementasyon tarafında bir değişiklik olduğunda "interface" de değişmeyecek ve bu "interface" i kullanan "client" kodların değiştirilmesine ve yeniden derlenmesine de gerek yoktur. İşte "pimpl" deyimi ile kastedilen de bu mekanizmadır. Peki neden bizler yeniden derleme ihtiyacı hissederiz? Şu nedenlerden dolayı; -> Sınıfımıza yeni veri elemanı eklersek, halihazırdaki bir veri elemanını çıkartırsak ve/veya veri elemanının yerini değiştirirsek sınıfın "binary compatibility" bozulacaktır. Yani bu sınıfın "include" eden kodların yeniden derlenmesi gerekir. Çünkü sınıf nesnesinin "layout" u tamamen değişmektedir. Çünkü veri elemanlarının o sınıf türünden nesne içindeki konumları, dolayısıyla sınıf nesnesinin "storage" ihtiyacı değişmiş olacaktır. Daha önceki duruma göre derlenmiş kodlar, programın çalışma zamanında uyumlu olmayacaklar. Örneğin, yapı nesnesinin adresinden sonraki şu "offset" noktasında şu veri elemanı olduğuna güvenerek kod yazıldığını düşünelim. Fakat bir noktada sınıfa yeni bir veri elemanı eklenmiş, dolayısıyla "offset" noktası da kaymış olsun. Artık "offset" noktası kaymış olacağı için, çalışma zamanında istenmeyen sonuçlar elde edebiliriz. -> Sınıfımızın "private" bölümünde bulunan fonksiyonların başka bölüme alınması veya "private" bölüme yeni fonksiyonların eklenmesi de "binary compatibility" bozacaktır, her ne kadar iş bu fonksiyonlar sınıf nesnesi içerisinde yer kaplamadığı ve dışarıdan çağrılabilir olmasalar da. Çünkü "Function Overload Resolution" dan ötürü. "FO" ya yeni fonksiyonlar da girebileceği için, başka fonksiyonların da seçilme ihtimali olacak. Anımsanacağı üzere önce "FO", daha sonra "access control" yapılmakta. Buradan hareketle diyebiliriz ki bu mekanizmayla birlikte sınıfın "private" bölümünde olan veri elemanları ve üye fonksiyonlar taşınmalıdır. Pekiyi bu mekanizmanın avantajlarına değinecek olursak; -> "interface" ile implementason ayrılacağı için daha stabil, kararlı bir sistem kurabileceğiz. -> Derleme zamanının daha kısa sürmesini sağlatabiliriz. -> Başlık dosyalar normal "text" dosyaları, dolayısıyla programcılar normal bir şekilde gözlemleyebilirler, bu da bizim kullandığımız tekniklerin de gözlemlenebilir olması demek. Eğer yöntemlerimizi gizlemek istiyorsak da bu mekanizmayı kullanabiliriz. Öte yandan bu mekanizmanın da bir takım dezavantajları da yok değildir, dolayısıyla bütün sınıflarımız için bu deyimi kullanmak da mantıksız olur. Şöyleki; -> Çünkü esas sınıfımızın veri elemanı olan göstericinin gösterdiği "impl" şey, dinamik ömürlü olacaktır. Bu da zamanı geldiğinde yetersiz bellek alanı oluşmasına neden olabilir. -> Gösterici kullandığımız için işlemlerimizde ilk olarak "dereference" uygulanması gerekmektedir. -> "private" bölümünde olan fonksiyonlar "inline" olarak ele alınarak bir takım optimizasyonlara tabi tutulabilirdi, ancak gösterici kullandığımız için derleyiciler artık tanımlarını o başlık dosyasında göremeyeceği için, optimizasyon imkanı da kalmamış olacaktır. -> "debug" sürecinin zorlaşması. Diğer yandan bu deyim "Opaque Pointer", "Handle-Body", "D Pointer", "Chesire Cat" ve "Compiler Firewall" olarak da geçer. Şimdi de örnekler ile bu deyimi irdeleyelim; * Örnek 1.0, "without pimpl idiom": // student.h #include #include //... class Student { public: Student(const char* p_name, const char* p_surname); void add_grade(int); void print() const; //... private: std::string m_name; std::string m_surname; std::vector m_grades; //... }; // student.cpp #include "student.h" #include Student::Student(const char* p_name, const char* p_surname) : m_name{ p_name }, m_surname{ p_surname } {} void Student::add_grade(int grade) { m_grades.emplace_back(grade); } void Student::print() const { std::cout << "name : " << m_name << '\n' << "surname: " << m_surname << '\n' << "grades : "; for (auto i: m_grades) std::cout << i << ' '; std::cout << '\n'; } // main.cpp #include "student.h" int main() { Student s{ "Ulya", "Yuruk" }; s.add_grade(17); s.add_grade(9); s.add_grade(1993); s.print(); /* # OUTPUT # name : Ulya surname: Yuruk grades : 17 9 1993 */ } * Örnek 1.1.0, "with pimpl idiom using nested type": // Student.h #include class Student { public: Student(const char*, const char*); Student(const Student&); Student(Student&&) noexcept; Student& operator=(Student); Student& operator=(Student&&) noexcept; /* * Derleyici tarafından "default" edilen "dtor." fonksiyonu * sınıfın veri elemanlarını sonlandıracak. Veri elemanımız * akıllı gösterici türünden olduğu için, onu yok edecek. * Akıllı Gösterici nesnesinin yok edilmesi demek, Akıllı Gösterici'nin * "deleter" ın sarmaladığı göstericiyi "delete" etmesi demektir. Biz * "incomplete" tür kullandığımız için sentaks hatası alacağız. Çünkü * derleyici "Dtor." fonksiyonunu "inline" olarak başlık dosyası içinde * yazacaktır. İşte başlık dosyası içerisinde bir tanım olması sentaks * hatası oluşturacaktır. Aynı durum, eğer diğer özel üye fonksiyonlar da * Akıllı Göstericiler için "complete" tür gerektiriyorsa, diğer özel * üye fonksiyonlar için de geçerlidir. İşte böylesi durumlarda bizler * "Dtor." fonksiyonunda sadece bildiriyor, ".cpp" dosyasında da "default" * ediyoruz. */ ~Student(); void add_grade(int); void print() const; void swap(Student&) noexcept; // Will be used for "Copy-Swap" idiom. //... private: class StudentImpl; StudentImpl* pimpl() { return m_ptr.get(); } const StudentImpl* pimpl() const { return m_ptr.get(); } std::unique_ptr m_ptr; }; // Student.cpp #include "student.h" #include #include #include class Student::StudentImpl { public: StudentImpl(const char* p_name, const char* p_surname) : m_name{ p_name }, m_surname{ p_surname } {} void add_grade(int grade) { m_grades.emplace_back(grade); } void print() const { std::cout << "name : " << m_name << '\n' << "surname: " << m_surname << '\n' << "grades : "; for (auto i: m_grades) std::cout << i << ' '; std::cout << '\n'; } private: // İş bu veri elemanların sınıfın hangi bölümünde olduğu artık önemli değil. std::string m_name; std::string m_surname; std::vector m_grades{}; //... }; Student::Student(const char* p_name, const char* p_surname) : m_ptr{ std::make_unique(p_name, p_surname) } {} Student::Student(const Student& other) : m_ptr{ std::make_unique(*other.m_ptr) } {} Student& Student::operator=(Student other) { // Programın akışı iş başta parametre değişkeni için "Copy Ctor." // fonksiyonuna girecek. Eğer orada "exception" fırlatılırsa, akış // bu bloğa hiç girmeyecek. Eğer fırlatılmazsa girecek. Zaten kendi // fonksiyonumuz olan "swap" da "exception" için garanti verdiğinden, // oradan da fırlatılma olmayacak. Çünkü "std::swap" fonksiyonunun // "exception" fırlatmama garantisi verdiğini, ona güvenebileceğimizi // biliyoruz. Bizimki de zaten "noexcept" olduğundan sıkıntı yok. swap(other); return *this; } Student::Student(Student&&) noexcept = default; Student& Student::operator=(Student&&) noexcept = default; Student::~Student() = default; void Student::add_grade(int value) { pimpl()->add_grade(value); } void Student::print() const { pimpl()->print(); } void Student::swap(Student& other) noexcept { std::swap(m_ptr, other.m_ptr); } // main.cpp #include "student.h" int main() { Student s{ "Ulya", "Yuruk" }; s.add_grade(17); s.add_grade(9); s.add_grade(1993); s.print(); /* # OUTPUT # name : Ulya surname: Yuruk grades : 17 9 1993 */ } * Örnek 1.1.1, "with pimpl idiom without using nested type": // Student.h #include class Student { public: Student(const char*, const char*); Student(const Student&); Student(Student&&) noexcept; Student& operator=(Student); Student& operator=(Student&&) noexcept; /* * Derleyici tarafından "default" edilen "dtor." fonksiyonu * sınıfın veri elemanlarını sonlandıracak. Veri elemanımız * akıllı gösterici türünden olduğu için, onu yok edecek. * Akıllı Gösterici nesnesinin yok edilmesi demek, Akıllı Gösterici'nin * "deleter" ın sarmaladığı göstericiyi "delete" etmesi demektir. Biz * "incomplete" tür kullandığımız için sentaks hatası alacağız. Çünkü * derleyici "Dtor." fonksiyonunu "inline" olarak başlık dosyası içinde * yazacaktır. İşte başlık dosyası içerisinde bir tanım olması sentaks * hatası oluşturacaktır. Aynı durum, eğer diğer özel üye fonksiyonlar da * Akıllı Göstericiler için "complete" tür gerektiriyorsa, diğer özel * üye fonksiyonlar için de geçerlidir. İşte böylesi durumlarda bizler * "Dtor." fonksiyonunda sadece bildiriyor, ".cpp" dosyasında da "default" * ediyoruz. */ ~Student(); void add_grade(int); void print() const; void swap(Student&) noexcept; // Will be used for "Copy-Swap" idiom. //... private: class StudentImpl* pimpl() { return m_ptr.get(); } const class StudentImpl* pimpl() const { return m_ptr.get(); } std::unique_ptr m_ptr; }; // Student.cpp #include "student.h" #include #include #include class StudentImpl { public: StudentImpl(const char* p_name, const char* p_surname) : m_name{ p_name }, m_surname{ p_surname } {} void add_grade(int grade) { m_grades.emplace_back(grade); } void print() const { std::cout << "name : " << m_name << '\n' << "surname: " << m_surname << '\n' << "grades : "; for (auto i: m_grades) std::cout << i << ' '; std::cout << '\n'; } private: // İş bu veri elemanların sınıfın hangi bölümünde olduğu artık önemli değil. std::string m_name; std::string m_surname; std::vector m_grades{}; //... }; Student::Student(const char* p_name, const char* p_surname) : m_ptr{ std::make_unique(p_name, p_surname) } {} Student::Student(const Student& other) : m_ptr{ std::make_unique(*other.m_ptr) } {} Student& Student::operator=(Student other) { // Programın akışı iş başta parametre değişkeni için "Copy Ctor." // fonksiyonuna girecek. Eğer orada "exception" fırlatılırsa, akış // bu bloğa hiç girmeyecek. Eğer fırlatılmazsa girecek. Zaten kendi // fonksiyonumuz olan "swap" da "exception" için garanti verdiğinden, // oradan da fırlatılma olmayacak. Çünkü "std::swap" fonksiyonunun // "exception" fırlatmama garantisi verdiğini, ona güvenebileceğimizi // biliyoruz. Bizimki de zaten "noexcept" olduğundan sıkıntı yok. swap(other); return *this; } Student::Student(Student&&) noexcept = default; Student& Student::operator=(Student&&) noexcept = default; Student::~Student() = default; void Student::add_grade(int value) { pimpl()->add_grade(value); } void Student::print() const { pimpl()->print(); } void Student::swap(Student& other) noexcept { std::swap(m_ptr, other.m_ptr); } // main.cpp #include "student.h" int main() { Student s{ "Ulya", "Yuruk" }; s.add_grade(17); s.add_grade(9); s.add_grade(1993); s.print(); /* # OUTPUT # name : Ulya surname: Yuruk grades : 17 9 1993 */ } > "14. Cpp Idioms/Patterns > Fast Pimpl Idiom": Bu "pimpl" deyiminin en büyük maliyet kalemi şüphesiz dinamik bellek yönetiminde. İşte bu maliyetten kaçınmak adına geliştirilen mekanizmanın adıdır, "fast pimpl idiom". * Örnek 1, // placement.h template void placement_new(void* buffer, std::size_t buffer_size) { new(buffer) T(); } template T* placement_cast(void* buffer) { return reinterpret_cast(buffer); } ///call the destructor of type T at a given address template void placement_delete(void* buffer) { placement_cast(buffer)->~T(); } // logger.h #pragma once #include #include #include class Logger { public: Logger(); ~Logger(); void log(const std::string& str); //other functions private: constexpr static std::size_t size = 1024; std::aligned_storage_t> impl{}; }; // logger.cpp #include "logger.h" #include "placement.h" #include #include class LoggerImpl { public: void log(const std::string& str) { std::cout << "message : " << str << std::endl; ofs << str << std::endl; } private: std::ofstream ofs; }; Logger::Logger() { static_assert(size >= sizeof(LoggerImpl)); placement_new(&impl, sizeof(LoggerImpl)); } Logger::~Logger() { placement_delete(&impl); } void Logger::log(const std::string &str) { placement_cast(&impl)->log(str); } // main.cpp #include "logger.h" int main() { Logger mylogger{}; mylogger.log("necati ergin"); } > "15. Cpp Idioms/Patterns > Copy-Swap Idiom": Sınıfın "operator=()" fonksiyonlarının yazılmasında kullanılır ve şu avantajlara sahiptir; -> İş bu fonksiyonlara "strong exception guarantee" sağlatmak. Anımsanacağı üzere "strong guarantee" verildiğinde o fonksiyon ya işini tam manasıyla yapacak ya da nesnenin durumu o fonksiyon çağrılmadan evvelki durumunda olacak. Bir diğer deyişle "commit-or-roll_back". Bu durumda bizlerin ayrı bir nesneye daha ihtiyacımız var ki işlemleri o ayrı bir nesne üzerinde gerçekleştirip, herhangi bir problem olmadığını gördükten sonra, ayrı nesnemizi orjinal nesnemize atayabilelim. İşte kullanılan bu ayrı nesneden ötürü de maliyetlidir, "strong exception guarantee" sunmak. Dolayısıyla böyle bir garanti verme zorunluluğumuz yoksa, bu maliyetten kaçınmalıyız. -> Ortak kodu tek bir yerde toplamak. -> "Self Assignment" kontrolüne gerek kalmaması, onun getirdiği maliyetin kaldırılması. İşte bunları sağlaması için "std::swap" fonksiyonunu kullanıyoruz. Çünkü bu fonksiyonun "strong exception" garantisi verme hususunda bizlere garanti vermektedir, yani bu fonksiyonun "exception" fırlatmayacağına güvenebiliriz. * Örnek 1, #include #include template class Array { public: Array(T* pa, std::size_t size) : m_pa{ pa }, m_size{ size } {} Array(const Array& other) : m_pa{ new T[other.m_size] }, m_size{ size } { std::copy_n(other.m_pa, m_size, m_pa); } Array(Array&& other) : m_pa{ std::exchange(other.m_pa, nullptr) }, m_size{ std::exchange(other.m_size, 0) } {} ~Array() { if (m_pa) delete[] m_pa; } T& operator[](std::size_t idx) { return m_pa[idx]; } const T& operator[](std::size_t idx) const { return m_pa[idx]; } std::size_t size() const { return m_size; } // A-must function to swapping: friend void swap(Array& lhs, Array& rhs) noexcept { // Aşağıdaki "using" bildiriminden dolayı ilk // olarak "swap" ismi sınıf içerisinde aranacak, // bulunamazsa "std" içerisinde aranacak. Yani veri // elemanlarımızın türleri olan sınıflar kendi özel // "swap" fonksiyonları varsa onlar, onlar uygun // değilse "std" içerisindeki. using std::swap; swap(lhs.m_pa, rhs.m_pa); swap(lhs.m_size, rhs.m_pa) } // Usage of "Copy-Swap" Idiom: /* // Way - I Array& operator=(const Array& other) { Array temp(other); // Bahsi geçen ayrı nesne. swap(*this, temp); return *this; } */ // Way - II Array& operator=(Array other) { // Burada ilkin "Copy ctor." çağrılacak. // Daha sonra bu ".operator=" fonksiyonu. swap(*this, other); // Sonrasında da "swap" fonksiyonu. return *this; } private: T* m_pa; std::size_t m_size; }; int main() { } > Exceptions & Exception Handling: >> "16. Cpp Idioms/Patterns > Polymorphic Exception": Varsayalım ki elimizde bir "exception" nesnesi var ve türü "std::exception" ailesinden bir sınıfa ait ve bizler de elimizdeki bu "exception" nesnesini bir fonksiyona gönderterek, o fonksiyonun yine aynı sınıf türünden bir "exception" göndermesini istiyoruz. Yani dinamik tür koruyarak, bir fonksiyon vasıtasıyla, aynı türden bir "exception" nesnesi daha oluşturmak istiyoruz. * Örnek 1.0, Aşağıdaki bu konuya ilişkin bir problem örneği verilmiştir: #include class Ex_Base {}; class Ex_Der_I : public Ex_Base {}; class Ex_Der_II : public Ex_Der_I {}; class Ex_Der_III : public Ex_Der_II {}; void f(Ex_Base& ex) { //... // --->(1) : Ex_Base // Burada "Object Slicing" olacak. // Dolayısıyla "Ex_Base" türünden // bir nesne oluşturulup gönderilecek. // Dinamik tür korunmamış olacak. // throw ex; // "re-throw" yapılabilmesi için "f" // fonksiyon çağrısının "catch" bloğu // içerisinde, yani yakalanmış bir // nesne sonrasında, yapılmış olması // gerekir. Aksi halde "std::terminate" // çağrılır. // throw; } int main() { Ex_Der_II ex; try { f(ex); // --->(1) } catch(Ex_Der_III) { std::cout << "Ex_Der_III\n"; } catch(Ex_Der_II) { std::cout << "Ex_Der_II\n"; } catch(Ex_Der_I) { std::cout << "Ex_Der_I\n"; } catch(Ex_Base) { std::cout << "Ex_Base\n"; } catch(...) { std::cout << "Unknown\n"; } } * Örnek 1.1, Taban sınıfı "virtual" hale getirerek "virtual dispatch" mekanizmasından fayda sağlayabiliriz. #include class Ex_Base { public: virtual ~Ex_Base() = default; // A-must for virtual classes. virtual void raise() { throw *this; } }; class Ex_Der_I : public Ex_Base { public: virtual void raise() override { throw *this; } }; class Ex_Der_II : public Ex_Der_I { public: virtual void raise() override { throw *this; } }; class Ex_Der_III : public Ex_Der_II { public: virtual void raise() override { throw *this; } }; void f(Ex_Base& ex) { //... ex.raise(); } int main() { Ex_Der_II ex; try { f(ex); // Ex_Der_II } catch(Ex_Der_III) { std::cout << "Ex_Der_III\n"; } catch(Ex_Der_II) { std::cout << "Ex_Der_II\n"; } catch(Ex_Der_I) { std::cout << "Ex_Der_I\n"; } catch(Ex_Base) { std::cout << "Ex_Base\n"; } catch(...) { std::cout << "Unknown\n"; } } * Örnek 1.2, Pekala "f" fonksiyonunu fonksiyon şablonu haline getirerek de benzer sonucu elde edebilirdik. #include class Ex_Base {}; class Ex_Der_I : public Ex_Base {}; class Ex_Der_II : public Ex_Der_I {}; class Ex_Der_III : public Ex_Der_II {}; template void f(T&& ex) { //... throw T(ex); } int main() { Ex_Der_II ex; try { f(ex); // Ex_Der_II } catch(Ex_Der_III) { std::cout << "Ex_Der_III\n"; } catch(Ex_Der_II) { std::cout << "Ex_Der_II\n"; } catch(Ex_Der_I) { std::cout << "Ex_Der_I\n"; } catch(Ex_Base) { std::cout << "Ex_Base\n"; } catch(...) { std::cout << "Unknown\n"; } } >> "std::exception_ptr" : C++11 ile dile eklendi. Amacı bir hatayı yakalamak ancak hatanın işlenmesini başka bir bağlama aktarmak, aktarmasak bile daha sonra işlemek adına bir yerde saklamaktır. Bu sınıf türüyle ilişkili şu fonksiyonlar da vardır; "std::current_exception()", "std::rethrow_exception()", "std::make_exception_ptr". Bu fonksiyonlardan, >>> "std::current_exception" fonksiyonu, en son yakalanan hata nesnesini geri döndürür. Bu fonksiyonun geri döndürdüğü nesneyi, "std::exception_ptr" türünden bir nesnede saklayabiliriz. >>> "std::rethrow_exception" fonksiyonu, argüman olarak "std::exception_ptr" türündeki nesnenin tuttuğu hata nesnesini "rethrow" eder. >>> "std::make_exception_ptr" fonksiyonu, bir fabrika fonksiyondur ve bize "std::exception_ptr" türünden nesne döndürür. Bu fonksiyonlar yine dinamik türün korunmasını sağlamaktadır. Şimdi örnekler ile irdeleyelim; * Örnek 1, Aşağıda yakalamış olduğumuz hata nesnesini daha sonra işleme aldık. #include #include #include void handle_exception(std::exception_ptr ex_ptr) { try { if (ex_ptr) std::rethrow_exception(ex_ptr); } catch(const std::exception& ex) { std::cout << "Caught exception: " << ex.what() << '\n'; } } int main() { /* # OUTPUT # main basladi main devam ediyor Caught exception: basic_string::at: __n (which is 2005) >= this->size() (which is 4) main bitecek */ std::cout << "main basladi\n"; std::exception_ptr ex_ptr{}; // "nullable" type. try { std::string name{"Ulya"}; auto c = name.at(2005); // An exception'll be thrown. } catch (...) { ex_ptr = std::current_exception(); } std::cout << "main devam ediyor\n"; handle_exception(ex_ptr); std::cout << "main bitecek\n"; } * Örnek 2, Aşağıdaki örnekte ise "ex_ptr", iki "thread" arasında aracı rolü işlemiştir. #include #include #include std::exception_ptr ex_ptr = nullptr; void func(int x) { std::cout << "func(int x) was called. x = " << x << '\n'; try { if (x % 2 == 0) throw std::invalid_argument{ "Invalid Argument" }; } catch (...) { ex_ptr = std::current_exception(); } std::cout << "func(int x) was ended. x = " << x << '\n'; } int main() { /* # OUTPUT # func(int x) was called. x = 26 func(int x) was ended. x = 26 Exception Caught: Invalid Argument */ std::thread t{ func, 26 }; t.join(); try { if (ex_ptr) std::rethrow_exception(ex_ptr); } catch (const std::exception& ex) { std::cout << "Exception Caught: " << ex.what() << '\n'; } } * Örnek 3, Aşağıdaki örnekte ise gönderilen hata nesneleri bir "vector" içerisinde toplanmış, daha sonra işlenmiştir. #include #include #include #include std::vector g_ex_ptr_vec; std::mutex g_mutex; void f1 () { throw std::exception{ "f1" }; } void f2 () { throw std::exception{ "f2" }; } void f3 () { try { f1(); } catch (...) { std::lock_guard{ g_mutex }; g_ex_ptr_vec.push_back(std::current_exception()); } } void f4 () { try { f2(); } catch (...) { std::lock_guard{ g_mutex }; g_ex_ptr_vec.push_back(std::current_exception()); } } int main() { std::thread t1(f3); std::thread t2(f4); t1.join(); t2.join(); for(const auto& i: g_ex_ptr_vec) { try { if (i) std::rethrow_exception(i); } catch (const std::exception& ex) { std::cout << "Exception Caught: " << ex.what() << '\n'; } } } * Örnek 4.0, "make_exception_ptr" temsili implementasyonu aşağıdaki gibidir. #include #include #include template std::exception_ptr MakeExceptionPtr(T ex) noexcept { try { throw ex; } catch (...) { return std::current_exception(); } } int main() { // OUTPUT : Ex: Run Time Error auto ex_ptr = MakeExceptionPtr(std::runtime_error{ "Run Time Error" }); try { std::rethrow_exception(ex_ptr); } catch (const std::exception& ex) { std::cout << "Ex: " << ex.what() << '\n'; } } * Örnek 4.1, "make_exception_ptr" kullanımı; #include #include #include int main() { // OUTPUT : Ex: Run Time Error auto ex_ptr = make_exception_ptr(std::runtime_error{ "Run Time Error" }); try { std::rethrow_exception(ex_ptr); } catch (const std::exception& ex) { std::cout << "Ex: " << ex.what() << '\n'; } } >> "Nested Exception" Kavramı: Alt seviyeli fonksiyonlardan gönderilen hata nesnelerini yakalıyoruz, yapabileceğimiz şey(ler) varsa yapıyoruz, yapamadığımız şeyler için de hata nesnesinin türünü değiştirip tekrardan gönderiyoruz. Buna da hata nesnesinin "translate" edilmesi denmektedir. Fakat bazı durumlarda bu mekanizma bilgi kaybına da neden olabilir. İşte buna çözüm olarak da alt seviyeden gelen hata nesnesini sarmalayarak üst seviyelere gönderiyoruz. Böylelikle daha dışarıda olanlar üst seviyeyle ilgiliyken, içeride olanlar alt seviye ile ilgili olacak. * Örnek 1.0, Aşağıdaki örnekte alt seviyeden gelen hata nesneleri hakkında bilgi üst seviyelere iletilememiştir. #include #include #include void foo() { //... throw std::runtime_error{ "exception from foo\n" }; } void bar() { try { //... foo(); //... } catch(const std::exception& ex) { throw std::runtime_error{ "exception from bar\n" }; } } void baz() { try { //... bar(); //... } catch(const std::exception& ex) { throw std::runtime_error{ "exception from baz\n" }; } } int main() { // OUTPUT : Ex: exception from Baz try { baz(); } catch (const std::exception& ex) { std::cout << "Ex: " << ex.what() << '\n'; } } * Örnek 1.1, Aşağıdaki örnekte ise üst seviyelere gönderilen hata mesajlarına bilgi ekleyerek gönderilmiştir. Böylelikle üst seviyelere daha çok bilgi aktarıldı. #include #include #include #include void foo() { //... throw std::runtime_error{ "exception from foo" }; } void bar() { try { //... foo(); //... } catch(const std::exception& ex) { throw std::runtime_error{ std::string{ "exception from bar + " } + ex.what() }; } } void baz() { try { //... bar(); //... } catch(const std::exception& ex) { throw std::runtime_error{ std::string{ "exception from baz + " } + ex.what() }; } } int main() { // OUTPUT : Ex: exception from baz + exception from bar + exception from foo try { baz(); } catch (const std::exception& ex) { std::cout << "Ex: " << ex.what() << '\n'; } } * Örnek 1.2.0, İşte Modern C++ ile dile eklenen "std::throw_with_nested" ve "std::rethrow_if_nested" gibi araçlar sayesinde daha modern bir çözüm üretmiş olduk. #include #include #include #include void foo() { //... throw std::runtime_error{ "exception from foo" }; } void bar() { try { //... foo(); //... } catch(const std::exception& ex) { std::throw_with_nested(std::length_error{ "exception from bar" }); } } void baz() { try { //... bar(); //... } catch(const std::exception& ex) { std::throw_with_nested(std::out_of_range{ "exception from baz" }); } } int main() { /* # OUTPUT # Outer Ex: exception from baz Middle Ex: exception from bar Inner Ex: exception from foo */ try { baz(); } catch (const std::exception& ex) { std::cout << "Outer Ex: " << ex.what() << '\n'; try { // "Catch for outer shell" std::rethrow_if_nested(ex); } catch (const std::exception& ex) { std::cout << "Middle Ex: " << ex.what() << '\n'; try { // "Catch for middle shell" std::rethrow_if_nested(ex); } catch (const std::exception& ex) { std::cout << "Inner Ex: " << ex.what() << '\n'; try { // "Catch for inner shell" std::rethrow_if_nested(ex); } catch (const std::exception& ex) { std::cout << "Base Ex: " << ex.what() << '\n'; } } } } } * Örnek 1.2.1, Pekala "main" çağrısındaki iç içe "try-catch" bloklarını "recursive" fonksiyon çağrısıyla da halledebilirdik; #include #include #include #include void print_ex_elements(const std::exception& ex) { std::cout << ex.what() << '\n'; try { // Eğer içi boşsa, hata nesnesi gönderilmeyecek. // Böylelikle "recursive" çağrı sonlanacak. std::rethrow_if_nested(ex); } catch (const std::exception& nested) { std::cout << "Nested Ex: "; print_ex_elements(nested); } } void foo() { //... throw std::runtime_error{ "exception from foo" }; } void bar() { try { //... foo(); //... } catch(const std::exception& ex) { std::throw_with_nested(std::length_error{ "exception from bar" }); } } void baz() { try { //... bar(); //... } catch(const std::exception& ex) { std::throw_with_nested(std::out_of_range{ "exception from baz" }); } } int main() { /* # OUTPUT # exception from baz Nested Ex: exception from bar Nested Ex: exception from foo */ try { baz(); } catch (const std::exception& ex) { print_ex_elements(ex); } } > "17. Cpp Idioms/Patterns > Attorney Client Idiom": "friend" bildirimleriyle ilgilidir. Anımsanacağı üzere sınıflar "friend" bildirimleri ile kendi "private" kısımlarını başka kodlara açmaktadır. Ancak kısıtlama getiremiyoruz; ya bütün "private" kısmım ya da hiç biri. İşte bir takım yöntemler ile kısıtlama getirebiliriz. * Örnek 1.0, Aşağıdaki kodda bulunan "friend" bildirimini yorum satırı olmaktan çıkartırsak, ilgili sentaks hataları da gitmiş olacaktır. #include class Neco { // -----> (1) friend class Erg; private: void foo() { std::cout << "private foo\n"; } void bar() { std::cout << "private bar\n"; } void baz() { std::cout << "private baz\n"; } }; class Erg { public: void func(Neco other) { // -----> (1) other.foo(); // ERROR: ‘void Neco::foo()’ is private within this context other.bar(); // ERROR: ‘void Neco::bar()’ is private within this context other.baz(); // ERROR: ‘void Neco::baz()’ is private within this context } }; int main() { /* // -----> (1) # OUTPUT # private foo private bar private baz */ Erg e1; e1.func(Neco{}); } * Örnek 1.1, #include class Neco { void foo() { std::cout << "private foo\n"; } void bar() { std::cout << "private bar\n"; } void baz() { std::cout << "private baz\n"; } friend class Neco_Erg; }; class Neco_Erg { static void func_to_foo(Neco other) { other.foo(); } // "Erg" sınıfı "Neco_Erg" sınıfının "private" bölümüne // erişebilir. Bizler "private" bölümdeki "func_to_foo" // fonksiyonuna sadece ".foo()" çağrısı eklediğimiz için // sadece o fonksiyon çağrıldı. Böylelikle "Erg" sınıfı // "Neco" sınıfının bütün "private" bölümüne değil, sadece // kendisine sunulan bölümüne erişti. Artık "Neco_Erg" // sınıfına ekleyeceğimiz fonksiyonlar üzerinden "Neco" // sınıfı "Erg" sınıfına kısmi "friend" verebilir. friend class Erg; }; class Erg { public: void func(Neco other) { Neco_Erg::func_to_foo(other); } }; int main() { /* # OUTPUT # private foo */ Erg e1; e1.func(Neco{}); } /*================================================================================================================================*/ (34_11_11_2023) & (35_12_11_2023) > "std::regex" : "Regular Expression" ifadesinin kısaltması, Düzenli İfadelerin İngilizce'sidir. Tamamiyle yazılar ile ilgili bir konudur. C++11 ile dile eklenmiştir. Bir "regex" metni, bir yazının uyması gereken kuralları betimleyen bir metindir. Özel bir notasyona sahiptir. Bu notasyon ile bir yazıya ilişkin kural seti betimlenir. Tıpkı "st::format" veya "scanf" fonksiyonlarındaki notasyon gibi. İşte bu notasyonlar üzerinden şöyle kazanımlar elde edebiliriz; -> "validation", bir yazının kurallara uyup uymadığının sınanması. -> "search", bir yazı içerisinde arama işlemleri için. -> "replace", bir yazı üzerinde değişiklik yapmak için. Tabii bu avantajların yanında en büyük dezavantaj, bu kütüphanenin MALİYETLİ olmasıdır. Yani "std::string" ve türevi sınıflardaki fonksiyonlar ile yapabileceğimiz şeyleri "std::regex" sınıfındaki fonksiyonlar ile YAPTIRMAKTAN KAÇINMALIYIZ. Bu noktada önümüzde iki yol çıkmaktadır; -> "Regex" notasyonunu öğrenmek ki bu notasyonu gerek diğer dillerde gerek metin editörlerinde kullanabiliriz. Örneğin, "regex101.com" isimli internet adresinden bu notasyonlar hakkında bilgi edinebiliriz. -> C++ dilinin "Regex" notasyonlarını nasıl ele aldığı; hangi fonksiyonun ne tür işleve sahip olduğu vs. Şimdi de bu yolları sırasıyla irdeleyelim; > "Regex" Notasyonu: Bir notasyon kendi içerisinde bazı özel anlama taşıyan karakterlere sahiptir. Bunlara "meta-character" denir. Eğer özel anlamı haricinde, karakterin bizzat kendisini kullanmak istiyorsak, "\" karakteri ile birlikte kullanmalıyız. Bu "meta-character" haricindeki diğer karakterler, kendi anlamlarındadır. * Örnek 1.0, "." karakterinin özel anlamı "'\n' karakteri hariç bütün karakterler" anlamındadır. Bu durumda, ".abc" biçiminde bir notasyon oluşturursak şu anlama gelecektir; -> Öyle kelimeler ki bunlar bünyesinde dört harfli şöyle kelimeler barındırsın; ilki harfi "\n" hariç herhangi bir harf, ikincisi "a", üçüncüsü "b" ve dördüncüsü "c" harfi olacak. Bu durumda içerisinde "aabc" kelimesini içeren bir kelimeyi bulabiliriz. * Örnek 1.1, "." karakterinin bizzat kendisini kullanmak istiyorsak, onu "\" ile birleştirmeliyiz. Bu durumda "\.abc" biçiminde bir notasyon oluşturursak şu anlama gelecektir; -> Öyle kelimeler ki bunlar bünyesinde dört harfli ".abc" kelimesini barındırsın. * Örnek 1.2, İş bu nedenden dolayı "\" karakterinin bizzat kendisini kullanmak istiyorsak, oluşturacağımız notasyonda "\\" biçiminde yazmalıyız. Bu durumda şu noktaya da dikkat çekmek gerekmektedir; -> C++ dilinde "\" karakteri de bir "meta-character" olarak geçer. Dolayısıyla "string literal" içerisinde bizzat "\" kullanmak için "\\" şeklinde yazıyoruz. Diğer yandan notasyon içerisinde "\" karakterini bizzat kullanmak için de "\\" yazıyoruz. İşte bu notasyonumuzu C++ diline aktarırken, "\" karakterini bizzat kullanmak için, "\\\\" biçiminde yazmalıyız. Bunun dışında "n-tane karakterden herhangi biri olabilir" manasına gelen bir notasyon oluşturmak için "[]" içerisinde iş bu "n" tane karakterin listesini yazıyoruz. * Örnek 1, "[aei]" biçiminde bir notasyon şu anlama gelecektir; -> Öyle kelimeler ki bunlar bünyesinde "a", "e" veya "i" karakterlerinden herhangi birisini barındırabilir. Buradaki mevzu iş bu hecelerin hepsini bünyesinde barındırması değil, bu hecelerden en az birini bünyesinde barındırmasıdır. * Örnek 2, "[aei][ou]" biçimindeki bir notasyon şu anlama gelecektir; -> Öyle kelimeler ki bunlar bünyesinde bir hece "aei" kümesinden, bir hece "ou" kümesinden olmak şartıyla, bu kümelerden oluşturulan iki harfli o kelimeyi barındırsın. Örneğin, bu notasyon ile bünyesinde "eo" kelimesini barındıranlar bulunacaktır. Diğer yandan "n-tane karakterden birisi olmayacak" manasına gelen bir notasyon oluşturmak için "[]" içerisinde il önce "^", daha sonra iş bu "n" tane karakterin listesini yazıyoruz. Böylelikle iş bu karakter listesindekileri BÜNYESİNDE BARINDIRMAYANLAR bulunacaktır. * Örnek 1, "[^mhrt]" biçimindeki bir notasyon şu anlama gelecektir; -> Öyle kelimeler ki bünyesinde "m", "h", "r" veya "t" karakterleri OLMASIN. Buna ilaveten "bir aralık içerisindeki karakterlerden herhangi biri olabilir" manasına gelen bir notasyon oluşturmak için, "[]" içerisine aralığın ilk ve son harflerini yazdıktan sonra bu iki harfin arasına "-" karakterini ekliyoruz. Böylelikle "ilk ve son harf dahil, aralıktaki karakterlerden herhangi birisi olabilir" manasını elde etmiş oluyoruz. * Örnek 1, "[a-e]" biçimindeki bir notasyon şu anlama gelecektir; -> "a" ve "e" heceleri dahil, bu iki hece arasındaki hecelerin herhangi birisini barındırabilir. * Örnek 2, "[a-e][e-k]" biçimindeki bir notasyon şu anlama gelecektir; -> Öyle kelimeler ki bunlar bünyesinde bir hece "a" ve "e" arasındaki karakter kümesinden, bir hece de "e" ve "k" arasındaki karakter kümseinden olmak şartyıla, bu kümelerden oluşturulan iki harfli o kelimeyi barındırsın. Örneğin, bu notasyon ile bünyesinde "cj" kelimesini barındıranlar bulunacaktır. * Örnek 3, "[0-9]" biçimindeki bir notasyon şu anlama gelecektir; -> Öyle kelimeler ki bünyesinde rakam karakterlerinden herhangi birisini barındırsın. Dolayısıyla yazımız içerisindeki sayıları da ayrı ayrı bulacaktır. Öte yandan "bir aralık içerisindeki karakterlerden herhangi biri olmayacak" manasına gelen bir notasyon oluşturmak için, "[]" içerisine ilk olarak "^" karakterini, devamında aralığın ilk ve son harflerini yazdıktan sonra bu iki harfin arasına "-" karakterini ekliyoruz. Böylelikle "bu aralıktakileri içermeyen" manasını elde etmiş oluyoruz. * Örnek 1, "[^0-9]" biçimindeki bir notasyon şu anlama gelecektir; -> Öyle kelimeler ki bünyesinde rakam karakterleri BULUNMASIN. * Örnek 2, "[^0-9a-z]" biçimindeki bir notasyon şu anlama gelecektir; -> Öyle kelimeler ki bünyesinde hem rakam karakterleri hem de küçük harfli alfabedeki kelimeleri BULUNMASIN. Şimdi buraya kadarkileri bir örnekte toplayalım; * Örnek 1, "[mkr0-9A-F]" biçimindeki bir notasyon şu anlama gelecektir; -> "m", "k" veya "r" karakterlerinden herhangi birisini, rakam karaktarlerini ve "A" ve "F" arasındaki büyük harfli karakterleri, ki "A" ve "F" dahildir, bünyesinde barındıran kelime(ler). Burada bir tane "[]" kullanıldığı için tek bir harf üzerinden gidilmektedir. * Örnek 2, "[^mkr0-9A-F]" biçimindeki bir notasyon şu anlama gelecektir; -> Yukarıdaki örnekteki şartları SAĞLAMAYAN kelime(ler). Bunların haricinde, bazı aralık değerleri için özel karakterler de vardır. * Örnek 1, "[0-9]" demek yerine "\d" kullanmak aynı anlama gelecektir. Burada "d" harfinin küçük olması önemlidir. Burada "D" harfinin kullanılması "^" karakteri etkisi yapmaktadır. Dolayısıyla "[^0-9]" demek yerine "\D" kullanmak aynı anlama gelecektir. * Örnek 2, "\s" karakteri boşluk karakterleri anlamındadır. "tab" ile "space" tuşları ile oluşturulan boşluk karakterleri de buna dahildir. "new-line" da yine bu karakter kapsamındadır. "\S" ise boşluk karakteri olmayan anlamındadır. * Örnek 3, "\w" karakteri "Alphanumeric" karakter anlamındadır. "underscore" karakteri de buna dahildir, yani "_" karakteri. "\W" karakteri ise "Alphanumeric" olmayan karakter anlamındadır. Pekala bizler bir kelimeyi de notasyon içerisine koyabiliriz. Bunun için "()" kullanmalıyız. Pekala "()" ile öncelik de oluşturabiliriz. * Örnek 1, "(kan)" şeklindeki notasyon; -> Bünyesinde "kan" kelimesini içerenleri tespit edecektir. * Örnek 2, "(\(kan\))" şeklindeki notasyon; -> Bünyesinde "(kan)" kelimesini içerenleri tespit edecektir. "Regex" notasyonundaki bir diğer önemli kavram da "quantifiers" kavramıdır. Yani kendisinden evvelki "token" dan kaç tane olması gerektiğini belirtmektedir. * Örnek 1.0, "?": kendisinden evvelki "token" dan ya bir tane olacak ya hiç olmayacak. Dolayısıyla "ka?r" biçimindeki bir notasyon; -> Bünyesinde "kar" veya "kr" içeren kelimeleri bulabiliriz. * Örnek 1.1, "[kts]a?[rmn]" notasyonu bizlere öyle kelimeler bulacak ki bunlar; -> "k", "t" veya "s" harflerinden birisini ve -> Yukarıdaki harflerden sonra "a" harfi gelebilir de gelmeyedebilir de ve -> "r", "m" veya "n" harflerinden birisini İÇERMELİDİR. * Örnek 1.2, "(kan)?\d" notasyonu bizlere öyle kelimeler bulacak ki bunlar; -> Bünyesinde "kan" kelimesi oladabilir, olmayadabilir. -> "kan" kelimesi varsa devamında rakam karakteri, "kan" kelimesi yoksa da yalnız rakam karakterini içermelidir. * Örnek 2.0, "*": kendisinden evvelki "token" dan ya sıfır tane ya da daha fazla olacak. Dolayısıyla "ah*" biçimindeki bir notasyon; -> "ah", "ahh", "ahhh" vb. kelimelerini tespit edecektir. * Örnek 2.1, "[krm]*" notasyonu bizlere öyle kelimeler bulacak ki bunlar; -> -> "k", "r" veya "m" harflerinden birisinden ya sıfır yane ya da daha fazlasını bünyesinde barındıracak. * Örnek 3.0, "+": kendisinden evvelki "token" dan ya bir tane ya da fazla olacak. Dolayısıyla "\d+" biçimindeki bir notasyon; -> En az bir tane rakam karakteri olacak, ancak daha fazla da olabilir. * Örnek 4.0, "{n}": kendisinden öncekinden, "n" adedince olmalı. Dolayısıyla "\d{3}" biçimindeki bir notasyon; -> Üç basamaklı rakamları tespit edecektir. * Örnek 4.1, "{n,}": kendisinden öncekinden, en az "n" adedince olmalı. Yine bu "n" değeri de dahildir. Dolayısıyla "\d{3,}" biçimindeki bir notasyon; -> En az üç basamaklı rakamları tespit edecektir. * Örnek 4.2, "{n,m}": kendisinden öncekinden, en az "n" en fazla "m" adedince olmalı. Yine bu "n" ve "m" değerleri de dahildir. Dolayısıyla "\d{3,5}" biçimindeki bir notasyon; -> En az üç, en fazla beş basamaklı rakamları tespit edecektir. Şimdi buraya kadarkileri bir özetleyelim; -> "?", kendisinden öncekinden sıfır ya da bir tane olmalı. -> "*", kendisinden öncekinden sıfır ya da "n" tane olmalı. -> "+", kendisinden öncekinden bir ya da "n" tane olmalı. -> "{n}", kendisinden öncekinden "n" adedince. -> "{n,}", kendisinden öncekinden en az "n" adedince. -> "{n,m}", kendisinden öncekinden en az "n" en fazla "m" adedince. Diğer yandan "quantifiers" kavramı iki alt kavrama sahiptir. Bunlar "greedy" ve "lazy". >> "greedy" : Varsayılan durumdur. "n" taneyi kapsayanlar için kapsayabildiği kadarını kapsar. * Örnek 1, Notasyonumuz "3\.\d+" biçiminde olsun. Böylelikle "\d+" ifadesiyle en az bir tane rakam karakteri olması gerekmekte. Dolayısıyla bulunan kelime içerisinde "." karakterinden sonra 300 tane rakam varsa, bu 300 tanenin hepsi de dahil edilecek. >> "lazy" : İşte yukarıdaki gibi 300 karakterin hepsinin kapsanması yerine, sadece şartı sağlayan ilk karakterin dahil edilmesidir. Bunu gerçekleştirmek için de ilgili "quantifiers" "token" inin peşine "?" karakterini ekliyoruz. Yani minimal şart sağlandığı an gerçekleştirim duracaktır. * Örnek 1, Notasyonumuz "3\.\d+?" biçiminde olsun. Artık sadece "3.4" biçimindeki yazıları bulacaktır. * Örnek 2, Notasyonumuz "ahmet(can)??" biçiminde olsun. Eğer sadece "ahmet(can)?" olsaydı, "can" kelimesinin olduğu veya olmadığı bütün kelimeler kapsam dahilinde olacaktı. Ancan "ahmet(can)??" notasyonundaki minimal şart "can" kelimesinin olmaması olduğundan, sadece "ahmet" ismini bulacaktır. "Regex" notasyonundaki bir diğer önemli kavram da "anchor" kavramıdır. Bu kavram pozisyon, yer belirtmektedir. Şöyleki: * Örnek 1, "^kar" ifadesindeki "^", yazının başına bak demektir. SATIRIN BAŞINA DEĞİL. Yani; -> Yazının başındaki kelime "kar" kelimesini içermelidir. * Örnek 2, "kar$" ifadesindeki "$", yazının sonuna bak demektir. Yani; -> Yazının sonundaki kelime "kar" kelimesini içermelidir. * Örnek 3.0, "\btor" ifadesi, kelimenin başında "tor" kelimesini; "tor\b" ifadesi, kelimenin sonunda "tor" kelimesini; "\Btor" ifadesi kelimenin başında "tor" kelimesi olmayacak; "tor\B" ifadesi kelimenin sonunda "tor" kelimesi olmayacak. Böylelikle "\Btor\B" ifadesi ise başında ve sonunda "tor" olmayanlar manasındadır. Pekiyi bütün bu anlatılanlar sonucunda "if\s*\(.*\)\s*;" notasyonu şu manaya gelmektedir; -> "if" => Başta "if" kelimesi olmalı. -> "\s*" => Devamında sıfır ya da "n" tane boşluk karakteri gelmeli. -> "\(" => Devamında "(" in bizzat kendisi gelmeli demek. -> ".*" => Devamında herhangi bir karakterden sıfır ya da "n" tane gelmeli. -> "\)" => Devamında ")" in bizzat kendisi gelmeli demek. -> "\s*" => Devamında sıfır ya da "n" tane boşluk karakteri gelmeli. -> ";" => En sonunda ";" karakteri olmalı. Bu notasyon ile şu kelimeleri bulabiliriz; "if(x>y);", "if ( x > y ) ;", ... Hatta bu notasyonu kullanarak aşağıdaki örnekteki kod üzerinde arama yapabiliriz; * Örnek 1, https://onlinegdb.com/B_uJjxNHI using "if\s*\(.*\)\s*;" enabled w/ "RegExp Search". int main() { int x = 10; if (x > 5); // <<< ++x; } Diğer yandan "\d{4}\.[A-F]{4}\.\d{4}" notasyonu şu manaya gelmektedir; -> "\d{4}" => Dört adet rakam karakteri. -> "\." => "." karakterinin bizzat kendisi. -> "[A-F]{4}" => "A" ve "F" harfleri arasındaki harflerden dört adet. -> "\." => "." karakterinin bizzat kendisi. -> "\d{4}" => Dört adet rakam karakteri. Bu notasyonu ise "MY_NOTATION" ile göstermek gerekirse; -> "MY_NOTATION\b" => Satır sonundaki iş bu notasyonu sağlayanlar. -> "\bMY_NOTATION" => Kelime başındaki iş bu notasyonu sağlayanlar. -> "\BMY_NOTATION" => Kelime başındaki iş bu notasyonu SAĞLAMAYANLAR. -> "MY_NOTATION\B" => Satır sonundaki iş bu notasyonu SAĞLAMAYANLAR. Şimdi de "()" atomunun detaylarına değinelim. Yukarıda anlatılan özelliklerine ilaveten şu özelliklere de sahiptir; -> Bazı durumlarda öncelik kazandırıyor. -> "grouping" / "capture group" dediğimiz gruplama özelliğine sahip. -> "Back reference" Şöyleki; * Örnek 1, "a\d+" notasyonundaki "+" sadece "\d" yi kapsamaktadır. Fakat "(a\d)+" notasyonundaki ise "a\d" yi kapsamaktadır. Dolayısıyla bir öncelik parantezi oluşturmuş olduk. Aynı zamanda bir "capture-group" da oluşturmuş olduk. Ancak yukarıdaki "quantifiers" için parantez çok önem arz etmektedir. * Örnek 2, Yukarıdaki "MY_NOTATION" isimli notasyondaki "\d{4}\.[A-F]{4}\.\d{4}" ifadesini; -> "(\d{4})\.[A-F]{4}\.\d{4}" ifadesi haline getirirsek artık arka plandaki "regex" motoru "(\d{4})" ifadesini ayrıca tutmaktadır. Dolayısıyla bu ifadeye karşılık gelenleri ayrıca "get" edebileceğiz. -> "(\d{4})\.([A-F]{4})\.\d{4}" ifadesi haline getirirsek artık arka plandaki "regex" motoru "(\d{4})" ifadesini ve "([A-F]{4})" ifadesini ayrıca tutacaktır. Dolayısıyla bu ifadelere karşılık gelenleri ayrıca "get" edebileceğiz. -> "(\d{4})\.([A-F]{4})\.(\d{4})" ifadesi haline getirirsek artık arka plandaki "regex" motoru "(\d{4})" ifadesini, "([A-F]{4})" ifadesini ve "(\d{4})" ifadesini ayrıca tutacaktır. Dolayısıyla bu ifadelere karşılık gelenleri ayrıca "get" edebileceğiz. Ancak yukarıdaki gibi bir kullanımda hem öncelik parantezi işlevi hem de "grouping" işlemi gerçekleştirilecek. Fakat "grouping" işlemi maliyetli bir iş olduğundan, yani sadece öncelik parantezi işlevini kullanacaksak, "(?:)" biçiminde kullanmalıyız. * Örnek 3, İlk "capture-group" da bulunan ifadenin aynısının başka yerde olmasını istiyorsak, bu yöntemi kullanmalıyız. Yukarıdaki "MY_NOTATION" isimli notasyondaki "\d{4}\.[A-F]{4}\.\d{4}" ifadesini "(\d{4})\.([A-F]{4})\1\2" ifadesine çevirelim. Böylece; -> Birinci "capture-group" olan "(\d{4})" ifadesi için karşılık gelen ifadenin birebir aynısı "\1" ifadesinin bulunduğu yerde de olacaktır. -> İkinci "capture-group" olan "([A-F]{4})" ifadesi için karşılık gelen ifadenin birebir aynısı "\2" ifadesinin bulunduğu yerde de olacaktır. Dolayısıyla "(\d{4})\.([A-F]{4})\1\2" ifadesiyle şunları bulabiliriz; "1234.ABCD1234ABCD", "9876.QWER9876QWER", ... * Örnek 1, ".*([a-z]{2}).*\1.*\1" notasyonu; -> ".*" => Herhangi bir karakterden sıfır ya da "n" tane olmalı. -> "([a-z]{2})" => Birinci "capture-group". "a" ile "z" arasındaki harflerden iki tane. -> ".*" => Herhangi bir karakterden sıfır ya da "n" tane olmalı. -> "\1" => Birinci "capture-group" a ilişkin sonucun birebir aynısı olmalı. -> ".*" => Herhangi bir karakterden sıfır ya da "n" tane olmalı. -> "\1" => Birinci "capture-group" a ilişkin sonucun birebir aynısı olmalı. anlamına gelmekte olup, aşağıdaki sonuçlar bulacaktır; "word-for-word", "wine-drinking", ... "Regex" notasyonundaki bir diğer önemli kavram da "alternator" kavramıdır. Burada kullanılan karakter ise "|" karakteridir. Bu karakterden önce gelen veya sonra gelenden bir tanesi seçilebilir anlamındadır, yani "veya" bağlamı taşır. * Örnek 1, "kar|telsel" biçimindeki notasyon bize; -> Ya "kar" kelimesinin ilgili kelime içinde olmasını -> Ya "telsel" kelimesinin ilgili kelime içinde olmasını * Örnek 2, "(kar|tel)sel" biçimindeki notasyon bize; -> Ya "kar" kelimesinin ya da "tel" kelimesinin ilgili kelime içinde olmasını -> "sel" kelimesinin ilgili kelime içinde olmasını * Örnek 3.0, "\b([1-9]|[12][0-9]|3[01])\b" biçimindeki notasyon ise bizlerin bir ay içerisindeki günleri vermektedir; -> "\b" ler ile iş bu notasyonu sağlayanların kelime başında ve satır sonunda olması istenmiş. -> "[1-9]" => Ayın ilk dokuz günü için. -> "[12][0-9]" => Ayın 10. ve 29. günleri ve arasındaki günleri için. İlk basamak "1" veya "2", ikinci basamak "0" ile "9" arasındakilerden bir tanesi. -> "3[01]" => Ayın 30. ve 31. günleri için. İlk basamak "3", ikinci basamak "0" veya "1". * Örnek 3.1, "\b([1-9]|1[012])\b" biçimindeki notasyon ise bizlerin bir yıl içerisindeki ayları vermektedir; -> "\b" ler ile iş bu notasyonu sağlayanların kelime başında ve satır sonunda olması istenmiş. -> "[1-9]" => Senenin ilk dokuz günü için. -> "1[012]" => İlk basamak "1", ikinci basamak ise "0" veya "1" veya "2". "Regex" notasyonundaki iki diğer önemli kavram da "look-ahead" ve "look-back" kavramlarıdır. Ancak bu kavramlar bazı "regex" motorlarınca desteklenmiyor olabilir. >> "look-ahead" : Bu kavram da "positive look-ahead" ve "negative look-ahead" olmak üzere ikiye ayrılır. >>> "positive look-ahead" : Örneğin bizler "necati" ismini arıyor olalım. Bize öyle "necati" isimleri vermeli ki "necati" isminden sonra "ergin" ismi gelmiş olsun. Bizler aramayı direkt olarak "necati ergin" biçiminde yaparsak, sadece "necati ergin" olanları döndürecekti. Fakat aramayı "necati" biçiminde yapıyoruz, sonuç olarak öyle "necati" ler almalıyız ki bunların devamında da "ergin" olsun. Bu biçimde arama yapmak için "(?=)" şablonunu kullanırız. * Örnek 1, Notasyonumuz "necati(?=ergin)" olsun. Bu notasyon ile bizler "ahmetnecatiergin" biçimindeki bir ismi bulabileceğiz. Buradaki kritik nokta "necati" isminin bulunması ve bu bulunanlardan "necati" isminden sonra "ergin" isminin gelmesidir. >>> "negative look-ahead" : "positive" olanından farklı olarak burada da devamında "ergin" gelmesi değil, gelmemesi gerekmektedir. Şablonumuz "(?!)" biçimindedir. * Örnek 1, Notasyonumuz "necati(?!ergin)" olsun. Bu notasyon ile bizler "ahmetnecatisasmaz" biçimindeki bir ismi bulabiliriz. Buradaki kritik nokta "necati" isminin bulunması ve bu bulunanlardan "necati" isminden sonra "ergin" isminin GELMEMESİDİR. >> "look-back" : Bu kavram da "positive look-back" ve "negative look-back" olmak üzere ikiye ayrılır. >>> "positive look-back" : "positive look-ahead" in diğer yönlüsüdür. Yani devamındaki değil, evvelindekinin şartı sağlaması gerekmektedir. Şablon olarak "(?<=)" kullanırız. * Örnek 1, Notasyonumuz "(?<=ergin)necati" olsun. Bu notasyon ile bizler "erginnecatiİstanbul" biçimindeki bir ismi bulabiliriz. >>> "negative look-back" : "positive" olanından farklı olarak burada da evvelindekinin "ergin" olması değil, OLMAMASI gerekmektedir. Şablonumuz "(? "#" => İlk karakter "#" olmalı. -> "[A-Fa-f\d]{6}" => 6 adet "A" ile "F" aralığındaki veya "a" ile "f" aralığındaki karakterlerden veya rakam karakteri gelmeli. -> "[A-Fa-f\d]{3}" => 3 adet "A" ile "F" aralığındaki veya "a" ile "f" aralığındaki karakterlerden veya rakam karakteri gelmeli. >> C++ dilinin "Regex" notasyonlarını nasıl ele alışı: "regex" kütüphanesi dile C++11 ile eklenmiştir. Yukarıda da değinildiği üzere C++ dilinde notasyon kullanırken notasyonlarımızı "regex101.com" sitesine yazdığımız gibi değil, C++ dilinin anlayabilmesi için dönüştürmemiz gerekmektedir. Örneğin, "\d" notasyon bazında rakamları temsil etmektedir. C++ dilinde bunu kullanırken "\\d" biçiminde yazmalıyız ki dil bunu "\d" olarak işleme soksun. Çünkü C++ dilinde "\" karakterini bizzat kullanmak için "\\" yazmalıyız. Eğer notasyonda bizzat "\" karakterini kullanacaksam, "\\d" olarak notasyon oluşturmalıyım. Fakat bunu C++ diline aktarırken "\\\\d" olarak aktarmalıyız. İşte bu problemi gidermek için de "Raw String Literal" kullanabiliriz. * Örnek 1, Benefits of Raw String Literals. #include int main() { // Amaç : "\ahmet" kelimesini yazı içerisinde aramak. // Notasyon : "\\ahmet". // Cpp : "\\\\ahmet". // L-Value std::regex rx{ "\\\\ahmet" }; // "without using 'Raw String Literal'" std::regex rxx{ R("\\ahmet") }; // "with using 'Raw String Literal'" // R-Value std::regex{ "\\\\ahmet" }; // "without using 'Raw String Literal'" std::regex{ R("\\ahmet") }; // "with using 'Raw String Literal'" } * Örnek 2.0.0, "regex_match" fonksiyonu ile sınama yapabiliriz: #include #include #include int main() { // Amaç : Üç adet rakam karakteri, devamında dört adet alfabetik karakter, devamında üç adet rakam karakteri. // Notasyon : "\d{3}[a-z]{4}\d{3}". // Cpp : "\\d{3}[a-z]{4}\\d{3}". std::regex rx{ "\\d{3}[a-z]{4}\\d{3}" }; std::string the_text{ "123ahmo456" }; if (std::regex_match(the_text, rx)) std::cout << "Yes!"; // Yes! else std::cout << "No..."; } * Örnek 2.0.1, #include #include #include #include #include std::vector file_to_vector(const std::string& file_name) { std::ifstream ifs{ file_name }; if (!ifs) { std::cerr << file_name << " could not be opened\n"; throw std::runtime_error{ file_name + " could not be opened" }; } // Mandotary Copy Ellision return std::vector{ std::istream_iterator{ifs}, {}}; } int main() { // Amaç : Kelime başı olsun. "a" ile "f" karakterleri arasındaki karakterlerden en az dört, // en fazla yedi adet olsun. Sonunda da "tion" ile bitsin. // Notasyon : "\b[a-f]{4,7}tion". // Cpp : "\\b[a-f]{4,7}tion". auto vec = file_to_vector("WordList.txt"); std::cout << "[" << vec.size() << "]"; // [1006] std::regex rx{ "\\b[a-f]{4,7}tion" }; std::ofstream ofs{ "out.txt" }; if (!ofs) { std::cerr << "out.txt could not be opened\n"; exit(EXIT_FAILURE); } for (const auto& word: vec) { if (std::regex_match(word, rx)) ofs << word << '\n'; } } * Örnek 2.0.2, #include #include #include #include #include std::vector file_to_vector(const std::string& file_name) { std::ifstream ifs{ file_name }; if (!ifs) { std::cerr << file_name << " could not be opened\n"; throw std::runtime_error{ file_name + " could not be opened" }; } // Mandotary Copy Ellision return std::vector{ std::istream_iterator{ifs}, {}}; } int main() { // Amaç : "co" ile başlayan, arada "n" tane karakter olan, "ion" ile bitenler. // Notasyon : "co.*ion". // Cpp : "co.*ion". auto vec = file_to_vector("WordList.txt"); std::cout << "[" << vec.size() << "]"; // [1006] std::regex rx{ "co.*ion" }; std::ofstream ofs{ "out.txt" }; if (!ofs) { std::cerr << "out.txt could not be opened\n"; exit(EXIT_FAILURE); } for (const auto& word: vec) { if (std::regex_match(word, rx)) ofs << word << '\n'; } } * Örnek 2.1.0, "regex_match" fonksiyonu ile "capture-group" lar üzerinde de işlem yapabiliriz. #include #include #include #include #include std::vector file_to_vector(const std::string& file_name) { std::ifstream ifs{ file_name }; if (!ifs) { std::cerr << file_name << " could not be opened\n"; throw std::runtime_error{ file_name + " could not be opened" }; } // Mandotary Copy Ellision return std::vector{ std::istream_iterator{ifs}, {}}; } int main() { /* # OUTPUT # [1007] : 1234ahmo4321 # Capture Group Info # Size: 1 Length: 12 Pos: 0 */ auto vec = file_to_vector("WordList.txt"); std::cout << "[" << vec.size() << "] : "; std::regex rx{ "\\d{4}[a-z]{4}\\d{4}" }; // "std::smatch" aslında bir "container". // İlk öğesi ise bulunan yazının tamamı. // Diğer öğeleri ise varsa ilgili "capture-group" lara ilişkin. std::smatch the_capture_group; for (const auto& word: vec) if (std::regex_match(word, the_capture_group, rx)) { std::cout << word << '\n'; std::cout << "# Capture Group Info #\n"; std::cout << "Size: " << the_capture_group.size() << '\n'; std::cout << "Length: " << the_capture_group.length(0) << '\n'; std::cout << "Pos: " << the_capture_group.position(0) << '\n'; } } * Örnek 2.1.1, #include #include #include #include #include std::vector file_to_vector(const std::string& file_name) { std::ifstream ifs{ file_name }; if (!ifs) { std::cerr << file_name << " could not be opened\n"; throw std::runtime_error{ file_name + " could not be opened" }; } // Mandotary Copy Ellision return std::vector{ std::istream_iterator{ifs}, {}}; } int main() { /* # OUTPUT # [1007] : 1234ahmo4321 # Capture Group Info # [4] : 1234ahmo4321 1234 ahmo 4321 [4] : 1234ahmo4321 1234 ahmo 4321 */ auto vec = file_to_vector("WordList.txt"); std::cout << "[" << vec.size() << "] : "; std::regex rx{ "(\\d{4})([a-z]{4})(\\d{4})" }; std::smatch the_capture_group; for (const auto& word: vec) { if (std::regex_match(word, the_capture_group, rx)) { std::cout << word << '\n'; std::cout << "# Capture Group Info #\n"; // Prints Elements using "std::string": std::cout << "[" << the_capture_group.size() << "] : "; for (std::size_t i{}; i < the_capture_group.size(); ++i) std::cout << the_capture_group.str(i) << ' '; std::cout << '\n'; // Prints Elements using "std::submatch::operator<<()": std::cout << "[" << the_capture_group.size() << "] : "; for (std::size_t i{}; i < the_capture_group.size(); ++i) std::cout << the_capture_group[i] << ' '; std::cout << '\n'; } } } * Örnek 2.1.2, #include #include #include #include #include std::vector file_to_vector(const std::string& file_name) { std::ifstream ifs{ file_name }; if (!ifs) { std::cerr << file_name << " could not be opened\n"; throw std::runtime_error{ file_name + " could not be opened" }; } // Mandotary Copy Ellision return std::vector{ std::istream_iterator{ifs}, {}}; } int main() { /* # OUTPUT # [1007] : 1234ahmo4321 # Capture Group Info # [4] : 1234ahmo4321 1234 ahmo 4321 Length : 12 4 4 4 Start Index: 0 0 4 8 */ auto vec = file_to_vector("WordList.txt"); std::cout << "[" << vec.size() << "] : "; std::regex rx{ "(\\d{4})([a-z]{4})(\\d{4})" }; std::smatch the_capture_group; for (const auto& word: vec) { if (std::regex_match(word, the_capture_group, rx)) { std::cout << word << '\n'; std::cout << "# Capture Group Info #\n"; // Prints Elements: std::cout << "[" << the_capture_group.size() << "] : "; for (std::size_t i{}; i < the_capture_group.size(); ++i) std::cout << the_capture_group.str(i) << ' '; std::cout << '\n'; // Prints Length of Elemets: // "0" numaralı indiste bulunan yazının uzunluğu. // "1" numaralı indiste ilk "capture group" takinin uzunluğu. // ... std::cout << "Length: "; for (std::size_t i{}; i < the_capture_group.size(); ++i) std::cout << the_capture_group.length(i) << ' '; std::cout << '\n'; // Prints Index of Elemets: // "0" numaralı indiste, bulunan yazının kendisi. // "1" numaralı indiste, ilk "capture-group" taki ifadenin, // yazının kaçıncı indisinde başladığı bilgisi. // "2" numaralı indiste, ikinci "capture-group" taki ifadenin, // yazının kaçıncı indisinde başladığı bilgisi. // ... std::cout << "Start Index: "; for (std::size_t i{}; i < the_capture_group.size(); ++i) std::cout << the_capture_group.position(i) << ' '; std::cout << '\n'; } } } * Örnek 3.0.0, "regex_search" ile arama yapabiliriz. #include #include #include #include #include std::vector file_to_vector_by_sentence(const std::string& file_name) { std::ifstream ifs{ file_name }; if (!ifs) { std::cerr << file_name << " could not be opened\n"; throw std::runtime_error{ file_name + " could not be opened" }; } std::vector vec; std::string sline; while (getline(ifs, sline)) { vec.push_back(std::move(sline)); } // Named Return Value Opt. return vec; } int main() { /* # OUTPUT # [1003] : Yes!No!No!No!No!No!No!No!No!No!No!No!No!No!No!No!No!No!No!No!No!No! No!No!No!No!No!No!No!No!No!No!No!No!No!No!No!No!No!No!No!No!No!No!No!No!No! No!No!No!No!No!No!No!No!No!No!No!No!No!No!No!No!No!No!No!No!No!No!No!No!No! ... */ auto vec = file_to_vector_by_sentence("WordList.txt"); std::cout << "[" << vec.size() << "] : "; std::regex rx{ "(\\d{4})([a-z]{4})(\\d{4})" }; for (const auto& s: vec) { if (std::regex_search(s, rx)) { std::cout << "Yes!"; } else { std::cout << "No!"; } } } * Örnek 3.0.1, #include #include #include #include #include #include std::vector file_to_vec(const std::string& f_name) { std::ifstream ifs{ f_name }; if (!ifs) { throw std::runtime_error{ f_name + "file could not be opened!" }; } return std::vector{std::istream_iterator{ifs}, {}}; } int main() { std::regex rx{ "(\\d{4})\\.([A-F]{4})\\.(\\d{4})" }; auto vec = file_to_vec("WordList.txt"); std::smatch sm; for (const auto& s : vec) { if (std::regex_search(s, sm, rx)) { print( "{:<16}({})({})({}){:<16}\n", sm.prefix().str(), // Birinci "capture group" öncesindeki metin. sm.str(1), // Birinci "capture group" öncesindeki metin. sm.str(2), // İkinci "capture group" öncesindeki metin. sm.str(3), // Üçüncü "capture group" öncesindeki metin. sm.suffix().str() // Üçüncü "capture group" sonrasındaki metin. ); } } } * Örnek 3.0.2, #include #include #include #include #include #include std::vector file_to_vec(const std::string& f_name) { std::ifstream ifs{ f_name }; if (!ifs) { throw std::runtime_error{ f_name + "file could not be opened!" }; } return std::vector{std::istream_iterator{ifs}, {}}; } int main() { std::regex rx{ "(\\d{4})\\.([A-F]{4})\\.(\\d{4})" }; auto vec = file_to_vec("WordList.txt"); std::smatch sm; for (const auto& s : vec) { if (std::regex_search(s, sm, rx)) { if (sm.str(2)[0] == 'F' && sm[3].str()[0] == '1') // İkinci "capture group" un ilk harfi 'F', // Üçüncü "capture group" un ilk harfi '1' olmalı. print( "{:<16}({})({})({}){:<16}\n", sm.prefix().str(), sm.str(1), sm.str(2), sm.str(3), sm.suffix().str() ); } } } * Örnek 3.1.0, "std::sregex_iterator" kullanarak da arama yapabiliriz. #include #include #include #include #include #include std::string file_to_string(const std::string& f_name) { std::ifstream ifs{ f_name }; if (!ifs) { throw std::runtime_error{ f_name + "file could not be opened!" }; } return std::string{ std::istreambuf_iterator{ifs}, {} }; } int main() { std::regex rx{ "(\\d{4})\\.([A-F]{4})\\.(\\d{4})" }; auto str = file_to_string("WordList.txt"); for (std::sregex_iterator iter{ str.begin(), str.end(), rx }, end; iter != end; ++iter) { // Buradaki "end" iteratörü "default" olarak hayata gelirse, "end-of-sequence iterator" // konumunda olacaktır. if (iter->str(2)[0] == 'A') { // İkinci "capture group" un ilk harfi 'A' olmalı. print("{}\n", iter->str()); } } } * Örnek 3.1.1, #include #include #include #include #include #include std::string file_to_string(const std::string& f_name) { std::ifstream ifs{ f_name }; if (!ifs) { throw std::runtime_error{ f_name + "file could not be opened!" }; } return std::string{ std::istreambuf_iterator{ifs}, {} }; } int main() { /* # OUTPUT # 1 car 2 card 3 care 4 career 5 carry */ std::regex rx{ "\\bcar\\w*" }; // 'car' ile başlamalı, devamında 'n' tane karakter olmalı. auto str = file_to_string("WordList.txt"); int counter{}; for (std::sregex_iterator itr{ str.begin(), str.end(), rx }; itr != std::sregex_iterator{}; ++itr) { print("{:<4} {}\n", ++counter, itr->str()); } } * Örnek 3.2.0, "sregex_token_iterator" ile "tokenization" işlemi yapabiliriz. #include #include #include #include #include #include std::string file_to_string(const std::string& f_name) { std::ifstream ifs{ f_name }; if (!ifs) { throw std::runtime_error{ f_name + "file could not be opened!" }; } return std::string{ std::istreambuf_iterator{ifs}, {} }; } int main() { std::regex rx{ "(\\bcar)(\\w*)" }; // 'car' ile başlamalı, devamında 'n' tane karakter olmalı. auto str = file_to_string("WordList.txt"); for (std::sregex_token_iterator itr{ str.begin(), str.end(), rx, -1 }, end; itr != end; ++itr) { // '-1', uygun olmayanlar, // '0', bulunanlar, // '1', birinci "capture group", // '2', ikinci "capture group", // '{1,2}', birinci ve ikinci "capture group", print("{}\n", itr->str()); } } * Örnek 3.2.1, #include #include #include #include #include #include std::string file_to_string(const std::string& f_name) { std::ifstream ifs{ f_name }; if (!ifs) { throw std::runtime_error{ f_name + "file could not be opened!" }; } return std::string{ std::istreambuf_iterator{ifs}, {} }; } int main() { std::regex rx{ "[\\s,.!]+" }; auto str = file_to_string("WordList.txt"); for (std::sregex_token_iterator itr{ str.begin(), str.end(), rx, -1 }, end; itr != end; ++itr) { print("{}\n", itr->str()); } } * Örnek 4.0, "mark_count" ile "capture group" ların adedini öğrenebiliriz. #include #include int main() { std::regex rgx{ "(ahmet (kandemir) pehlivanli) (merve pehlivanli)" }; std::print("{}\n", rgx.mark_count()); // 3 } * Örnek 4.1.0, #include #include int main() { std::regex rgx{ "(ahmet \\(kandemir\\) pehlivanli) (merve pehlivanli)" }; std::print("{}\n", rgx.mark_count()); // 2 } * Örnek 4.1.1, #include #include int main() { std::regex rgx{ "(?:ahmet \\(kandemir\\) pehlivanli) (?:merve pehlivanli)" }; std::print("{}\n", rgx.mark_count()); // 0 } * Örnek 4.1.2, #include #include int main() { std::regex rgx{ "(ahmet (kandemir) pehlivanli) (merve pehlivanli)", std::regex_constants::nosubs }; std::print("{}\n", rgx.mark_count()); // 0 } * Örnek 5.0, "std::regex_replace" ile "replace" işlemi gerçekleştirebiliriz. #include #include #include #include #include #include std::string file_to_string(const std::string& f_name) { std::ifstream ifs{ f_name }; if (!ifs) { throw std::runtime_error{ f_name + "file could not be opened!" }; } return std::string{ std::istreambuf_iterator{ifs}, {} }; } int main() { /* # OUTPUT # interest () int (erest) interesting () int (eresting) international () int (ernational) interview () int (erview) into () int (o) //... */ std::regex rx{ "(\\w*)(int)(\\w*)" }; auto str = file_to_string("WordList.txt"); for (std::sregex_iterator iter{ str.begin(), str.end(), rx }, end; iter != end; ++iter) { print("{:<15} ({}) {} ({})\n", iter->str(), iter->str(1), iter->str(2), iter->str(3)); } std::cout << "\n=====================\n"; /* # OUTPUT # * * * * * //... */ // "match" olanların yerine "*" koyup, yeni halini // tek bir "std::string" olarak geri döndürür. auto result = std::regex_replace(str, rx, "*"); std::cout << "<" << result << ">\n"; std::cout << "\n=====================\n"; /* # OUTPUT # - int - erest - int - eresting - int - ernational - int - erview - int - o //... */ // "match" olanların yerine ilgili "capture-group" ları koyup, // yeni halini tek bir "std::string" olarak geri döndürür. result = std::regex_replace(str, rx, "$1-$2-$3"); std::cout << "<" << result << ">\n"; std::cout << "\n=====================\n"; /* # OUTPUT # //... */ // "match" olanları silip, // yeni halini tek bir "std::string" olarak geri döndürür. result = std::regex_replace(str, rx, "$'"); std::cout << "<" << result << ">\n"; std::cout << "\n=====================\n"; /* # OUTPUT # (interest) (interesting) (international) (interview) (into) //... */ // "match" olanları tekrar yazar, ancak "()" içerisinde. // Yeni halini tek bir "std::string" olarak geri döndürür. result = std::regex_replace(str, rx, "($&)"); std::cout << "<" << result << ">\n"; } * Örnek 5.1, #include #include #include #include #include #include int main() { /* # OUTPUT # // Enter a text : ben ben, bugun ne yazik ki ucaga yetismek zorundayim zorundayim arkadaslar, arkadaslar // ben, bugun ne yazik ki ucaga yetismek zorundayim arkadaslar, arkadaslar */ std::cout << "Enter a text: "; std::string str; std::getline(std::cin, str); std::regex rx{ "\\b(\\w+)\\s+\\1" }; // Peşpeşe iki tane aynı metin geldiğinde, // bir tanesini silecek. std::cout << std::regex_replace(str, rx, "$1"); } > "18. Cpp Idioms/Patterns > Nifty Counter": Anımsanacağı üzere farklı modül dosyalarındaki statik ömürlü nesnelerin, yani "global" isim alanındaki değişkenler ile sınıfların statik veri elemanlarının, hayata gelme sırası öngörülebilir değildi(bkz. "Static Init. Fiasco"). Aşağıdaki örneği inceleyelim; BU KISMI ARAŞTIR, DETAYLARINI ÖĞREN, TEKRAR İNCELE. * Örnek 1, Aşağıdaki örnekte her bir modül kendine has "g_mystream_init" değişkenine sahip olacaktır. Derleme sırasında "MyStream" başlık dosyasını "include" eden modüllerin birisinde ilk defa "g_mystream_init" nesnesi hayata gelecektir. Dolayısıyla "nifty_counter" değeri bir olacak, "MyStreamBuffer" adresinde "s_cout" nesnesi hayata gelecektir. "s_cout" nesnesi "inline" oldığından, tek bir tane olacaktır. // MyStream.h #ifndef MY_STREAM_H #define MY_STREAM_H // My Real Class struct MyStream { MyStream(); ~MyStream(); }; // "extern" bildirimi extern MyStream& s_cout; // An Init. Class for My Real Class struct MyStream_Init { MyStream_Init(); ~MyStream_Init(); }; // Internal Linkage: Bu başlık dosyasını "include" // eden her bir modül, kendine has bir "g_mystream_init" // değişkenine sahip olacak. static MyStream_Init g_mystream_init{}; #endif // MyStream.cpp #include "MyStream.h" #include // For 'placement new' #include // For 'aligned_storage' // Internal Linkage; Zero Init. static int nifty_counter; // Internal Linkage; Memory for the 's_cout' object. static typename std::aligned_storage_t MyStreamBuffer; MyStream& s_cout = reinterpret_cast(MyStreamBuffer); MyStream::MyStream() { // The Ctor. for My Real Class } MyStream::~MyStream() { // The Dtor. for My Real Class } MyStream_Init::MyStream_Init() { if (nifty_counter++ == 0) { new (&s_cout) MyStream(); // A call for 'placement new' } } MyStream_Init::~MyStream_Init() { if (--nifty_counter == 0) { (&s_cout)->~MyStream(); } } > "Conditionally Explicit Ctor." : Sınıfın kurucu işlevinin "excplicit" olmasının bir koşula bağlanması durumudur. Anımsanacağı üzere "Default Ctor.", tek parametreli "ctor.", çift parametreli "ctor." fonksiyonlarını "explicit" olarak betimleyebildiğimiz gibi tam tersi yönde de betimleyebiliyorduk. Artık C++20 ile birlikte bu fonksiyonların "explicit" olup olmamasını da bir "compile time" sabitine bağlayabiliriz. Kullanım biçimi "noexcept" fonksiyonu gibidir. * Örnek 1, class Myclass { public: explicit(true) Myclass(); }; * Örnek 2, #include template class Myclass { public: explicit(std::is_integral_v) Myclass(T) {} }; int main() { // Myclass m = 5; // error: conversion from ‘int’ to non-scalar type ‘Myclass’ requested Myclass m = 5.; // OK } * Örnek 3, En tipik kullanım yeri sarmalanan sınıflarda görülmektedir. Sarmalanan sınıfın ilgili "ctor." fonksiyonu "explicit" ise onu sarmalayan sınıfın o fonksiyonunun da "explicit" olmasını istiyoruz. #include class Neco { public: Neco() = default; explicit Neco(int) {} }; template class Wrapper { public: Wrapper() = default; template explicit(!std::is_convertible_v) Wrapper(U){ // -----> (1) // Burada; // "U" için "double", // "T" için "Neco" // gelecek. Böylelikle "double" to "Neco" // dönüşümünü sınamış olacağız. // "Neco" sınıfının "Conversion Ctor." // fonksiyonu "explicit" olduğundan, sınama // "false" döndürecektir. Bunun değilini // alırsak da "Neco" sınıfınınki ile uyumlu // hale gelmiş olur. } }; int main() { Wrapper mynec(4.); // -----> (1) } > Hatırlayıcı Notlar: >> "regex101.com" da "multi-line" modunda olması, her bir kelimenin ayrı bir yazı olarak ele alınması anlamına gelmektedir. >> "C++ Idioms" için kaynak: https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms /*================================================================================================================================*/ (36_18_11_2023) & (37_19_11_2023) & (38_25_11_2023) & (39_26_11_2023) & (40_02_12_2023) & (41_09_12_2023) & (42_16_12_2023) & (43_23_12_2023) & (44_06_01_2024) > "concurrency" : Birden fazla olayın aynı zaman dilimi içerisinde gerçekleşmesine "concurrency", birden fazla olayın tam olarak aynı anda gerçekleşiyor olmasına da "simultaneously" denir. Dolayısıyla "simultaneously" gerçekleşen olaylar için aynı zamanda "concurrency" tabirini kullanabiliriz fakat "concurrency" gerçekleşen olaylar için "simultaneously" tabirini kullanamayız. Buradan hareketle "simultaneously" için işlemci/çekirdek sayısının birden fazla olması bir zorunlulukken, "concurrency" için böyle bir zorunluluk bulunmamaktadır. Pekala bir sistemde bu iki mekanizmanın birleşmiş hali de bulunabilir. C++11 ile dile bu mekanizmalar eklenmiştir. Diğer yandan unutulmamalıdır ki C++ dilindeki iş bu standart fonksiyonlar, arka planda işletim sistemininin fonksiyonlarını çağırmaktadır. C++17 ile birlikte "STL" algoritma fonksiyonları paralel biçimde çalıştırabilir. Şimdi de örnekler üzerinden incelemelerde bulunalım; * Örnek 1.0, Bir iş yükü atanmamış "std::thread" nesneleri olabilir. Bunlar iş yapmayı bekleyenlerdir. #include #include int main() { std::thread tx; std::cout << tx.joinable() << '\n'; // true tx.join(); std::thread ty([]{}); std::cout << ty.joinable() << '\n'; // false ty.join(); } * Örnek 1.1, #include #include #include #include void foo() { } int main() { /* # OUTPUT # [I] : Full [II] : Empty ========== [I] : Empty [II] : Full */ std::thread t1{foo}; std::thread t2{}; std::cout << "[I] : "; std::cout << (t1.joinable() ? "Full" : "Empty")<< '\n'; std::cout << "[II] : "; std::cout << (t2.joinable() ? "Full" : "Empty") << '\n'; std::cout << "==========\n"; t2 = std::move(t1); std::cout << "[I] : "; std::cout << (t1.joinable() ? "Full" : "Empty")<< '\n'; std::cout << "[II] : "; std::cout << (t2.joinable() ? "Full" : "Empty") << '\n'; t2.join(); } * Örnek 1.2, #include #include int main() { auto fn = []{ std::cout << "My first thread\n"; }; std::thread tx{ fn }; // "std::thread" nesnesi, bir iş yüküyle hayata geldi. tx.join(); // "main-thread", "tx" in işi bitene kadar bloke olmuştur. // "tx" is in "non-joinable" state. } * Örnek 1.3, İş yükü olarak çeşitli fonksiyonları "std::thread" nesnelerine verebiliriz. #include #include void bar(int x, int y) { std::cout << "bar(" << x << ", " << y << ")\n"; } class Myclass { public: void foo(int x) { std::cout << "Myclass::foo(" << x << ")\n"; } static void s_foo(int x) { std::cout << "Myclass::s_foo(" << x << ")\n"; } void operator()(int x) { std::cout << "Myclass::operator()(" << x << ")\n"; } }; int main() { auto fn = [](int x){ std::cout << "Lambda::operator()(" << x << ")\n"; }; std::thread t1{ fn, 1 }; // Lambda::operator()(1) std::thread t2{ &Myclass::s_foo, 2 }; // Myclass::s_foo(2) Myclass mc; std::thread t3{ &Myclass::foo, &mc, 3 }; // Myclass::foo(3) std::thread t4{ bar, 4, 5 }; // bar(4, 5) t4.join(); t3.join(); t2.join(); t1.join(); } * Örnek 1.4.0, İş yükü olarak kullanılacak fonksiyonlar "non-const L-Value" referans alacaklarsa, o parametreleri "std::ref" ile sarmalama yapmamız gerekmektedir. Aksi halde sentaks hatası alırız. #include #include #include #include void func(std::list& local) { local.push_back(*local.begin()); local.push_back(*local.begin()); local.push_back(*local.begin()); } int main() { std::list my_list{ 2, 4, 6, 8, 10 }; for (const auto i : my_list) std::cout << i << ' '; // 2 4 6 8 10 std::cout << '\n'; std::thread t{ func, std::ref(my_list) }; t.join(); for (const auto i : my_list) std::cout << i << ' '; // 2 4 6 8 10 2 2 2 std::cout << '\n'; } * Örnek 1.4.1, #include #include #include class Myclass { public: Myclass() { std::cout << "Default Ctor.\n"; } Myclass(const Myclass&) { std::cout << "Copy Ctor.\n"; } Myclass(Myclass&&) noexcept { std::cout << "Move Ctor.\n"; } }; void bar(Myclass&) { } void foo(const Myclass&) { } void baz(Myclass&&) { } int main() { { // Default Ctor. Myclass m; // ERROR // std::thread t1{ bar, m }; t1.join(); // Copy Ctor. std::thread t2{ foo, m }; t2.join(); // Copy Ctor. std::thread t3{ baz, m }; t3.join(); } std::cout << "====================\n"; { // Default Ctor. Myclass m; // A reference for 'm'; no new instance of "Myclass"ç std::thread t1{ bar, std::ref(m) }; t1.join(); // Copy Ctor. std::thread t2{ foo, m }; t2.join(); // Move Ctor. std::thread t3{ baz, std::move(m) }; t3.join(); } } * Örnek 1.5, #include #include class Myclass { public: Myclass() = default; Myclass(const Myclass&) { std::cout << "Copy\n"; } Myclass(Myclass&&) noexcept { std::cout << "Move\n"; } }; void foo (Myclass) {} void bar (Myclass&&) {} void baz (Myclass&) {} void bat (const Myclass&) {} int main() { // "std::thread" sınıfının "Ctor." fonksiyonunun parametre paketindeki // öğeler için "decay" işlemi uygulanıyor; diziler göstericiye, fonksiyon // isimleri fonksiyon adreslerine, referans niteliğinin düşmesi vb. // Daha sonra elde edilen değer ilgili fonksiyona gönderiliyor. { Myclass m; // Copy // Move std::thread t1{ foo, m }; t1.join(); } std::cout << "\n==================\n"; { Myclass m; // Copy std::thread t1{ bar, m }; t1.join(); } std::cout << "\n==================\n"; { // Move std::thread t1{ bar, Myclass{} }; t1.join(); } std::cout << "\n==================\n"; { Myclass m; // Error: // std::thread t1{ baz, m }; t1.join(); } std::cout << "\n==================\n"; { Myclass m; // Copy std::thread t1{ bat, m }; t1.join(); } std::cout << "\n==================\n"; { // Move std::thread t1{ bat, Myclass{} }; t1.join(); } std::cout << "\n==================\n"; { Myclass m; // Default Ctor. std::thread t1{ baz, std::ref(m) }; t1.join(); } } * Örnek 2, "std::thread" türünden nesneler kopyalamaya karşı kapalıdır. #include #include #include #include int main() { /* # OUTPUT # thread-2 is running. thread-0 is running. thread-6 is running. thread-8 is running. thread-9 is running. thread-3 is running. thread-1 is running. thread-4 is running. thread-5 is running. thread-7 is running. main devam ediyor */ auto fn = [](int id){ // "thread-safe" version. std::osyncstream{std::cout} << "thread-" << id << " is running.\n"; }; std::vector tvec; for (int i = 0; i < 10; ++i) tvec.push_back(std::thread{fn, i}); // "std::thread" sınıfı "Move Only" bir sınıf. // "Copy Ctor." ve "Copy Assign" fonksiyonları "delete" edilmiş. for (auto& t : tvec) t.join(); std::cout << "main devam ediyor\n"; } * Örnek 3.0, Hayata gelen bir "thread" ya "join" edilmeli ya da "detach". #include #include #include #include void foo() { std::cout << "foo\n"; } void my_terminate() { std::cout << "my_terminate\n"; (void)getchar(); std::abort(); } int main() { /* // -----> (1) * İlgili "thread" nesnesi ".join()" veya ".detach()" * edilmediğinden bir hata nesnesi fırlatıldı. Yakalanmadığı * için de "std::terminate" çağrıldı fakat onu da konfigure * ettiğimizden, bizimki çağrıldı. */ std::set_terminate(my_terminate); { std::thread t1{foo}; // t1.join(); // -----> (1) } std::cout << "main devam ediyor\n"; } * Örnek 3.1, #include #include #include #include void foo() { std::cout << "foo\n"; } void my_terminate() { std::cout << "my_terminate\n"; (void)getchar(); std::abort(); } int main() { /* // -----> (1) * İlgili "thread" nesnesi ".join()" veya ".detach()" edildikten * sonra tekrardan ".join()" veya ".detach()" edilirse, yine bir * hata nesnesi fırlatır. Yakalanmadığı için de "std::terminate" * çağrılır fakat onu da konfigure ettiğimizden, bizimki çağrıldı. */ std::set_terminate(my_terminate); { std::thread t1{foo}; t1.join(); t1.detach(); // -----> (1) } std::cout << "main devam ediyor\n"; } * Örnek 3.2, #include #include #include #include void foo() { std::cout << "foo\n"; } void my_terminate() { std::cout << "my_terminate\n"; (void)getchar(); std::abort(); } int main() { /* // -----> (1) * İş yükü olmayan ilgili "thread" nesnesi * ".join()" veya ".detach()" edilirse yine bir * hata nesnesi fırlatacaktır. */ std::set_terminate(my_terminate); { std::thread t1{}; t1.join(); // t1.detach(); // -----> (1) } std::cout << "main devam ediyor\n"; } * Örnek 3.3, #include #include #include #include void foo() { std::cout << "foo\n"; } int main() { /* # OUTPUT # foo exception caught main devam ediyor. */ std::thread t1{ foo }; // 't1' is joinable here. std::thread t2{ std::move(t1) }; // 't1' is no longer joinable here, // but 't2' is. try { t1.join(); // Will cause throwing an error } catch (...) { std::cout << "exception caught\n"; } std::cout << "main devam ediyor.\n"; t2.join(); } * Örnek 3.4, #include #include #include #include #include #include std::thread make_one(int n) { return std::thread{ [n]() mutable { while (n--) std::cout << "Ulya Yuruk\n"; } }; } std::thread transfer_one(std::thread t) { //... return t; } int main() { /* # OUTPUT # Ulya Yuruk Ulya Yuruk Ulya Yuruk */ auto t1 = make_one(3); auto t2 = std::move(t1); t1 = transfer_one(std::move(t2)); t1.join(); } * Örnek 3.5.0, Tabii böyle yapmak yerine "RAII idiom" kullanan "std::jthread" sınıfını kullanabiliriz; #include #include #include void foo() { std::cout << "func called\n"; } int main() { /* # OUTPUT # main basladi func called Hata yakalandi main sonlanacak */ std::cout << "main basladi\n"; { try { std::jthread t{ foo }; throw std::runtime_error{ "error" }; // t.join(); } catch(...) { std::cout << "Hata yakalandi\n"; } } std::cout << "main sonlanacak\n"; } * Örnek 3.5.1, "std::jthread" kullanılmadığı ve hata nesnesinin fırlatılması durumunda ilgili "thread" için ".join()" çağrılmayacağından, "dead_lock" oluşacaktır. #include #include #include void foo() { std::cout << "func called\n"; } int main() { /* # OUTPUT # main basladi func called terminate called without an active exception */ std::cout << "main basladi\n"; { try { std::thread t{ foo }; throw std::runtime_error{ "error" }; t.join(); } catch(...) { std::cout << "Hata yakalandi\n"; } } std::cout << "main sonlanacak\n"; } * Örnek 3.5.2, // Concurrency in Action kitabında verilen basit bir jthread implementasyonu #include class joining_thread { public: joining_thread() noexcept = default; template explicit joining_thread(Callable&& func, Args&& ... args) : m_t(std::forward(func), std::forward(args)...) {} explicit joining_thread(std::thread t) noexcept : m_t(std::move(t)) {} joining_thread(joining_thread&& other) noexcept : m_t(std::move(other.m_t)) {} joining_thread& operator=(joining_thread&& other) noexcept { if (joinable()) join(); m_t = std::move(other.m_t); return *this; } joining_thread& operator=(std::thread other) noexcept { if (joinable()) join(); m_t = std::move(other); return *this; } ~joining_thread() noexcept { if (joinable()) join(); } void swap(joining_thread& other) noexcept { m_t.swap(other.m_t); } std::thread::id get_id() const noexcept { return m_t.get_id(); } bool joinable() const noexcept { return m_t.joinable(); } void join() { m_t.join(); } void detach() { m_t.detach(); } std::thread& as_thread() noexcept { return m_t; } const std::thread& as_thread() const noexcept { return m_t; } private: std::thread m_t; }; * Örnek 4, "n" tane "thread" ile eş zamanlı olarak bir iş yaptırtabiliriz. #include #include #include void foo(char c) { for (int i = 0; i < 1; ++i) std::cout << c; } int main() { /* # OUTPUT # ABEFGIJLKMNOQRSTUVWXYPZCDH */ std::vector tvec(26); for (int i = 0; i < 26; ++i) tvec[i] = std::thread{ foo, static_cast(i + 'A') }; for (auto& t: tvec) t.join(); } * Örnek 5.0, "this_thread" isim alanı içerisindeki "sleep_for" fonksiyonu, içinde bulunulan "thread" i belirtilen süre boyunca bekletir. #include #include #include #include using dsec = std::chrono::duration; void foo(char c) { for (int i = 0; i < 1; ++i) std::cout << c; // Çağıran "thread" i ilgili süre boyunca // bekletir, uyutur, akışını durdurur. std::this_thread::sleep_for(dsec{5}); } int main() { /* # OUTPUT # ABEFGIJLKMNOQRSTUVWXYPZCDH */ std::vector tvec(26); for (int i = 0; i < 26; ++i) tvec[i] = std::thread{ foo, static_cast(i + 'A') }; for (auto& t: tvec) t.join(); } * Örnek 5.1.0, "get_id()" isimli global fonksiyon ile içinde bulunulan "thread" in ID değerini, ".get_id()" isimli üye fonksiyon ile ilgili "std::thread" türünden nesnenin temsil ettiği "thread" in ID değerini "get" edebiliriz. Bu fonksiyonların geri dönüş türü ise "std::thread::id" türündendir. #include #include #include #include #include #include using dsec = std::chrono::duration; void foo(char c) { for (int i = 0; i < 1; ++i) std::osyncstream{ std::cout } << "[" << std::this_thread::get_id() << "] : " << c << '\n'; } int main() { /* # OUTPUT # [140511882524224] : A [140511848953408] : E [140511865738816] : C [140511840560704] : F [140511874131520] : B [140511823775296] : H [140511832168000] : G [140511857346112] : D [140511583131200] : M [140511599916608] : J [140511591523904] : L [140511574738496] : N [140511557953088] : P [140511566345792] : O [140511549560384] : Q [140511465698880] : R [140511448913472] : S [140511440520768] : T [140511432128064] : U [140511423735360] : V [140511415342656] : W [140511406949952] : X [140511398557248] : Y [140511390164544] : Z [140511457306176] : K [140511815382592] : I */ std::vector tvec(26); for (int i = 0; i < 26; ++i) tvec[i] = std::thread{ foo, static_cast(i + 'A') }; //for (auto& t: tvec) // std::cout << "<" << t.get_id() << ">\n"; // -----> (1) for (auto& t: tvec) t.join(); } * Örnek 5.1.1, #include #include #include #include #include #include std::thread::id main_thread_id; void foo() { if (std::this_thread::get_id() == main_thread_id) { std::cout << "Call from main thread\n"; } else { std::cout << "Call from ANOTHER thread\n"; } } int main() { /* # OUTPUT # Call from main thread Call from ANOTHER thread */ main_thread_id = std::this_thread::get_id(); foo(); std::thread tx{ foo }; tx.join(); } * Örnek 5.1.2, #include #include #include #include #include #include #include std::mutex foo_mtx; std::vector id_vec; void foo() { std::lock_guard g{ foo_mtx }; id_vec.push_back(std::this_thread::get_id()); } int main() { /* # OUTPUT # 140181196453440 140181188060736 140181179668032 140181171275328 140181088761408 140181080368704 140181071976000 140181063583296 140181055190592 140181046797888 140181038405184 140181030012480 140181021619776 140181013227072 140181004834368 140180996441664 140180988048960 140180979656256 140180971263552 */ std::vector t_vec; for(int i = 0; i < 20; ++i) { t_vec.emplace_back(foo); } for(const auto& id: id_vec) { std::cout << id << '\n'; } for(auto& t: t_vec) { t.join(); } } * Örnek 6.0, "thread" içerisinden bir hata nesnesi yakalandığında ya onu yakalayıp "handle" etmeli ya da ilgili "thread" i hayata getiren "thread" e ilgili hata nesnesi hakkında bilgi vermeliyiz. #include #include #include #include void foo(int x) { if (x % 5 == 0) throw std::runtime_error{ "foo: division error\n" }; } void my_terminate() { std::cout << "my_terminate: "; std::exit(EXIT_FAILURE); } int main() { std::set_terminate(&my_terminate); std::thread tr{ foo, 15 }; tr.join(); std::cout << "main devam ediyor\n"; } * Örnek 6.1, #include #include #include #include void foo(int x) { std::cout << "void foo(" << x << ") was called.\n"; // Handling the exception thrown within the thread: try { if (x % 5 == 0) throw std::runtime_error{ "foo: division error" }; } catch (const std::exception& ex) { std::cout << ex.what() << '\n'; } std::cout << "void foo(" << x << ") was completed.\n"; } void my_terminate() { std::cout << "my_terminate: "; std::exit(EXIT_FAILURE); } int main() { /* # OUTPUT # main basladi void foo(15) was called. foo: division error void foo(15) was completed. main devam ediyor main bitecek */ std::cout << "main basladi\n"; std::set_terminate(&my_terminate); std::thread tr{ foo, 15 }; tr.join(); std::cout << "main devam ediyor\n"; std::cout << "main bitecek\n"; } * Örnek 6.2, #include #include #include #include std::exception_ptr g_ex_ptr{ nullptr }; void foo(int x) { std::cout << "void foo(" << x << ") was called.\n"; try { if (x % 5 == 0) throw std::runtime_error{ "foo: division error" }; } catch (...) { g_ex_ptr = std::current_exception(); } std::cout << "void foo(" << x << ") was completed.\n"; } void my_terminate() { std::cout << "my_terminate: "; std::exit(EXIT_FAILURE); } int main() { /* # OUTPUT # main basladi void foo(15) was called. void foo(15) was completed. main devam ediyor foo: division error main bitecek */ std::cout << "main basladi\n"; std::set_terminate(&my_terminate); std::thread tr{ foo, 15 }; tr.join(); std::cout << "main devam ediyor\n"; // Handling the exception thrown from the thread: try { if (g_ex_ptr) { std::rethrow_exception(g_ex_ptr); } } catch (const std::exception& ex) { std::cout << ex.what() << '\n'; } std::cout << "main bitecek\n"; } * Örnek 6.3, #include #include #include #include std::vector g_ex_vec; std::mutex g_mutex; void f1() { throw std::runtime_error{ "runtime_error from f1" }; } //---------------------------------------------------------------------------------------------------- void f2() { throw std::out_of_range{ "out_of_range error from f2" }; } //---------------------------------------------------------------------------------------------------- void th_func1() { try { f1(); } catch (...) { std::lock_guard guard{ g_mutex }; g_ex_vec.push_back(std::current_exception()); } } //---------------------------------------------------------------------------------------------------- void th_func2() { try { f2(); } catch (...) { std::lock_guard guard{ g_mutex }; g_ex_vec.push_back(std::current_exception()); } } //---------------------------------------------------------------------------------------------------- int main() { std::thread t1(th_func1); std::thread t2(th_func2); t1.join(); t2.join(); for (auto const& ex : g_ex_vec) { try { if (ex != nullptr) std::rethrow_exception(ex); } catch (std::exception const& ex) { std::cout << "exception caught: " << ex.what() << '\n'; } } } * Örnek 7.0, Referans/gösterici semantiği ile bir nesneyi "thread" e iş yükü olarak vermişsek, referans olunan/gösterilen esas nesnenin hayatı bitmesi durumunda ilgili "thread" imiz bunun farkında olmayacaktır. Dolayısıyla "Tanımsız Davranış" oluşacak, "Dangling Reference/Pointer" durumu. #include #include #include #include #include #include void read_load(const std::vector* p) { auto n = std::accumulate(p->begin(), p->end(), 0); std::cout << n << '\n'; } void foo() { std::vector ivec{ 0, 2, 4, 6, 8, 1, 3, 5, 7, 9 }; std::thread t{ read_load, &ivec }; // Buradaki ".detach()" işleminden ötürü // iş bu "thread" arka planda bağımsız çalışacaktır. // Fakat "main-thread" akmaya devam edecektir. Bundan // dolayı "ivec" nesnesinin hayatı bitecektir. t.detach(); } int main() { /* # OUTPUT # 0 */ foo(); using namespace std::literals; std::this_thread::sleep_for(10s); } * Örnek 7.1, #include #include #include #include #include #include void read_load(const std::vector& p) { auto n = std::accumulate(begin(p), end(p), 0); std::cout << n << '\n'; } void foo() { std::vector ivec{ 0, 2, 4, 6, 8, 1, 3, 5, 7, 9 }; std::thread t{ read_load, std::ref(ivec) }; // Buradaki ".detach()" işleminden ötürü // iş bu "thread" arka planda bağımsız çalışacaktır. // Fakat "main-thread" akmaya devam edecektir. Bundan // dolayı "ivec" nesnesinin hayatı bitecektir. t.detach(); } int main() { /* # OUTPUT # 0 */ foo(); using namespace std::literals; std::this_thread::sleep_for(10s); } * Örnek 8, Yine bir "thread" i gelecekteki bir "time point" gelene kadar da bloke edebiliriz. #include #include #include auto now() { return std::chrono::steady_clock::now(); } auto awake_time() { using std::chrono::operator""ms; return now() + 2000ms; } int main() { std::cout << "Hello\n" << std::flush; const auto start{ now() }; std::this_thread::sleep_until(awake_time()); std::chrono::duration elapsed{ now() - start }; std::cout << "Waited " << elapsed.count() << " ms\n"; } * Örnek 9.0, "thread_local" anahtar sözcüğünü kullanarak bir değişkenin o "thread" e özgü olmasını sağlatabiliriz. "thread" in başlaması ile birlikte değişken de hayata gelecektir. "thread" in çalışması bittiğinde de değişkenimizin hayatı da sonlanacaktır. #include #include #include thread_local int t_val{ 0 }; void func(const std::string& id) { ++t_val; std::osyncstream{ std::cout } << "t_val in thread named [" << id << "] is [" << t_val << "]\n"; } int main() { /* # OUTPUT # t_val in thread named [main] is [1] t_val in thread named [t1] is [1] t_val in thread named [t2] is [1] t_val in thread named [t3] is [1] t_val in thread named [main] is [2] */ func("main"); std::thread t1{ func, "t1" }; std::thread t2{ func, "t2" }; std::thread t3{ func, "t3" }; //... t3.join(); t2.join(); t1.join(); func("main"); } * Örnek 9.1, #include #include thread_local int ival{ 0 }; void func(int* p) { *p = 777; std::cout << "*p: " << *p << ", ival: " << ival << '\n'; } int main() { /* # OUTPUT # ival: 0 ival: 333 *p: 777, ival: 0 */ std::cout << "ival: " << ival << '\n'; ival = 333; std::cout << "ival: " << ival << '\n'; std::thread t1{ func, &ival }; t1.join(); } * Örnek 9.2, #include #include #include #include #include thread_local std::string name{ "Ulya" }; void func(std::string const& surname) { name += '_' + surname + '@'; std::osyncstream ocout{ std::cout }; ocout << name << "[" << &name << "]\n"; } int main() { /* # OUTPUT # Ulya_Yuruk@[0x7f56394e6618] Ulya_Istanbul@[0x7f56384e4618] Ulya_Uskudar@[0x7f5638ce5618] Ulya_Ulya@[0x7f5639ce7618] Ulya_29.10.1995@[0x7f5637ce3618] */ const char* const pa[] = { "Ulya", "Yuruk", "Uskudar", "Istanbul", "29.10.1995" }; std::vector t_vec; for( auto i: pa ) { t_vec.emplace_back(func, i); } for( auto& i: t_vec ) { i.join(); } } * Örnek 9.3.0, #include #include #include #include class Myclass { public: Myclass() { std::osyncstream{ std::cout } << "Myclass constructor this : " << this << '\n'; } ~Myclass() { std::osyncstream{ std::cout } << "Myclass destructor this : " << this << '\n'; } }; void foo() { std::osyncstream{ std::cout } << "foo called\n"; thread_local Myclass m; std::osyncstream{ std::cout } << "foo ends\n"; } void bar() { using namespace std::chrono_literals; std::osyncstream{ std::cout } << "bar called\n"; foo(); std::this_thread::sleep_for(3s); std::osyncstream{ std::cout } << "bar ends\n"; } int main() { /* # OUTPUT # bar called foo called Myclass constructor this : 0x7fd352f73630 foo ends bar ends Myclass destructor this : 0x7fd352f73630 */ std::thread t{ bar }; t.join(); } * Örnek 9.3.1, #include #include #include #include class Myclass { public: Myclass() { std::osyncstream{ std::cout } << "Myclass constructor this : " << this << '\n'; } ~Myclass() { std::osyncstream{ std::cout } << "Myclass destructor this : " << this << '\n'; } }; void foo() { std::osyncstream{ std::cout } << "foo called\n"; thread_local Myclass m; std::osyncstream{ std::cout } << "foo ends\n"; } void bar() { using namespace std::chrono_literals; std::osyncstream{ std::cout } << "bar called\n"; foo(); std::this_thread::sleep_for(3s); std::osyncstream{ std::cout } << "bar ends\n"; } int main() { /* # OUTPUT # bar called foo called Myclass constructor this : 0x7f1ca9b7d630 foo ends bar called foo called Myclass constructor this : 0x7f1caa37e630 foo ends bar called foo called Myclass constructor this : 0x7f1ca937c630 foo ends bar ends Myclass destructor this : 0x7f1ca9b7d630 bar ends Myclass destructor this : 0x7f1caa37e630 bar ends Myclass destructor this : 0x7f1ca937c630 */ std::thread t1{ bar }; std::thread t2{ bar }; std::thread t3{ bar }; t3.join(); t2.join(); t1.join(); } * Örnek 9.4, #include #include #include #include thread_local int gt{}; void func(char c) { ++gt; std::osyncstream{ std::cout } << c << " " << gt << '\n'; } int main() { /* # OUTPUT # a 1 f 1 b 1 c 1 d 1 g 1 h 1 e 1 i 1 k 1 p 1 m 1 j 1 o 1 l 1 q 1 r 1 n 1 t 1 u 1 s 1 v 1 x 1 y 1 z 1 w 1 */ using namespace std; vector tvec; for (char c = 'a'; c <= 'z'; ++c) { tvec.emplace_back(func, c); } // for (auto& t : tvec) { t.join(); } } * Örnek 9.5, #include #include #include #include thread_local std::mt19937 eng{ 454255u }; void foo() { std::uniform_int_distribution dist{ 10, 99 }; std::osyncstream os{ std::cout }; for (int i = 0; i < 10; ++i) { os << dist(eng) << ' '; } os << '\n'; } int main() { /* # OUTPUT # 39 18 40 53 31 30 13 41 99 63 39 18 40 53 31 30 13 41 99 63 39 18 40 53 31 30 13 41 99 63 */ std::thread t1{ foo }; std::thread t2{ foo }; std::thread t3{ foo }; t3.join(); t2.join(); t1.join(); } * Örnek 10.0, "std::thread" nesnelerinin çalışmasını dışarıdan durdurabiliriz. Bunun için bizlerin "std::stop_source" türünden bir değişkene ihtiyacımız vardır. #include #include #include int main() { using namespace std::literals::chrono_literals; std::stop_source st_src; std::thread foo( [stoken = st_src.get_token()]() { // "stoken" is of type "std::stop_token". while (!stoken.stop_requested()) { std::cout.put('*'); std::this_thread::sleep_for(50ms); } } ); std::thread bar( // "Lambda Init. Capture" [stoken = st_src.get_token()]() { while (!stoken.stop_requested()) { std::cout.put('.'); std::this_thread::sleep_for(1s); } } ); std::this_thread::sleep_for(5s); // Aynı "std::stop_source" türden nesne kullanan "thread" ler için durdurulma talebi gönderildi. st_src.request_stop(); std::cout << "\nstopped\n"; foo.join(); bar.join(); } * Örnek 10.1, Fakat bunun yerine "std::jthread" sınıfını kullanabiliriz. Sadece iş yükü olarak kullanılacak fonksiyonun ilk parametresini "std::stop_token" türünden yapmalıyız. Gerisini "std::jthread" halledecektir. #include #include #include int main() { using namespace std::literals::chrono_literals; std::jthread foo( [](std::stop_token stoken) { while (!stoken.stop_requested()) { std::cout.put('*'); std::this_thread::sleep_for(50ms); } } ); std::jthread bar( [](std::stop_token stoken) { while (!stoken.stop_requested()) { std::cout.put('.'); std::this_thread::sleep_for(50ms); } } ); std::this_thread::sleep_for(5s); foo.request_stop(); bar.request_stop(); std::cout << "\nstopped\n"; } >> "thread" ler arası haberleşme: Eğer birden fazla "thread" nesnesi ortak nesne üzerinde çalışıyorsa ve en az birisi yazma işlevindeyse, "data racing" görülür. Çünkü "data racing" oluşması "Tanımsız Davranış". Ortak nesne üzerinde çalışan "thread" lerin mutlaka bir şekilde senkronize edilmesi gerekmektedir. * Örnek 1, #include #include void foo(long long) { } long long value; bool flag{}; void producer() { value = 76324; flag = true; } void consumer() { while (!flag) ; foo(value); } int main() { // Başlangıçta "flag" değişkeni "false" değerinde. // Ne zamanki "producer" fonksiyonu değişkenin değerini // "true" yaptı, "consumer" fonksiyonundaki döngüden çıkılacak // ve "value" değişkeni kullanılacak. // Eğer birden fazla "thread" nesnesi bu işlemleri yapıyorsa ve // senkronizasyon mekanizması da yoksa, tanımsız davranış oluşacaktır. // Örneğin, "flag" değişkeninin değerinin sınanması esnasında "value" // değişkeninin değeri değişebilir. Hatta "value" değişkeninin değeri // ne yeni değeri ne de eski değerinde de olabilir, yani ara bir değerde // de olabilir. } İşte bunu engellemek için bir takım kavramlar dile eklenmiştir. Bunlar, "mutex" nesneleri, "std::future" ve "std::promise", "std::async", "std::packaged_task", "Conditional Variables", "atomic", "semaphore" nesneleri. biçimindeki kavramlardır. >>> "mutex" nesneleri: C++ dilinde "mutex" birden fazla "mutex" nesnesi vardır. Bunlar, "std::mutex", "std::recursive_mutex", "std::timed_mutex", "std::shared_mutex" isimli sınıflardır. Bu sınıflar birim zamanda sadece tek bir "thread" nesnesinin çalışmasını sağlatabilir. >>>> "std::mutex" : Diğer "mutex" sınıfları arasında en minimal arayüzü sunan sınıftır. Bünyesinde ".lock()", ".unlock()" ve ".try_unlock" isimli fonksiyonları barındırır. Bu fonksiyonlar sırasıyla ilgili "mutex" nesnesini kilitler, kilidini kaldırır ve kilitlemeye çalışır. Dolayısıyla birim zamanda bir işin tek bir "thread" nesnesi tarafından yapılmasını istiyorsak, o işi bu iki fonksiyon çağrısı arasında yapmalıyız. * Örnek 1, #include #include #include #include #include std::string name{ "Ulya" }; std::mutex mtx; void func(std::string const& surname) { mtx.lock(); name += '_' + surname + '@'; std::cout << name << "[" << &name << "]\n"; mtx.unlock(); } int main() { /* # OUTPUT # Ulya_Ulya@[0x55cbd385a160] Ulya_Ulya@_Uskudar@[0x55cbd385a160] Ulya_Ulya@_Uskudar@_Yuruk@[0x55cbd385a160] Ulya_Ulya@_Uskudar@_Yuruk@_29.10.1995@[0x55cbd385a160] Ulya_Ulya@_Uskudar@_Yuruk@_29.10.1995@_Istanbul@[0x55cbd385a160] */ const char* const pa[] = { "Ulya", "Yuruk", "Uskudar", "Istanbul", "29.10.1995" }; std::vector t_vec; for( auto i: pa ) { t_vec.emplace_back(func, i); } for( auto& i: t_vec ) { i.join(); } } * Örnek 2, Eğer "thread" imizin halihazırda "lock" edilmiş bir "mutex" nesnesi gördüğünde bloke olmasını değil de başka işlevler yapmasını istiyorsak, ".try_lock()" fonksiyonunu kullanabiliriz. Bu fonksiyon ilgili "mutex" nesnesinin kilit durumunu sorgulamaktadır. #include #include #include int counter{}; std::mutex counter_mtx; void try_increase() { for (int i = 0; i < 100'000; ++i) { if (counter_mtx.try_lock()) { // Eğer "true" değer döndürürse, "std::mutex" nesnesi kilitlenecektir. ++counter; counter_mtx.unlock(); } } } int main() { std::thread ar_t[10]; for (int i = 0; i < 10; ++i) ar_t[i] = std::thread(try_increase); for (auto& t : ar_t) t.join(); std::cout << counter << " kez arttirma islemi yapilabildi.\n"; } * Örnek 3, Yine "std::jthread" ile birlikte "std::mutex" nesnesini de pekala kullanabiliriz. #include #include #include #include using namespace std; using namespace literals; std::mutex mtx; void foo() { cout << "foo is trying to lock the mutex\n"; mtx.lock(); std::cout << "foo has locked the mutex\n"; this_thread::sleep_for(800ms); cout << "foo is unlocking the mutex\n"; mtx.unlock(); } void bar() { this_thread::sleep_for(100ms); cout << "bar trying to lock the mutex\n"; while (!mtx.try_lock()) { cout << "bar could not lock the mutex\n"; this_thread::sleep_for(100ms); } cout << "bar has locked the mutex\n"; mtx.unlock(); } int main() { std::jthread thr1(foo); std::jthread thr2(bar); } Tabii böyle yapmak yerine "RAII idiom" u kullanan "std::lock_guard" sınıfını da pekala kullanabiliriz. >>>>> "std::lock_guard" : Bu sınıfın "ctor." fonksiyonu argüman olarak aldığı "mutex" nesnesinin ".lock()" fonksiyonunu, "dtor." fonksiyonu ise ilgili "mutex" nesnesinin ".unlock()" fonksiyonunu çağıracaktır. Bundan dolayıdır ki birim zamanda bir işin tek bir "thread" nesnesi tarafından yapılmasını istiyorsak, bu işi "std::lock_guard" nesnesinin hayata gelmesinden sonra yapmalıyız. Ayrıca bu sınıf, halihazırda ".lock()" fonksiyonu çağrılmış bir "mutex" nesnesinin kilidini almak suretiyle de hayata gelebilir. Bu işlevleri haricinde başka bir "interface" sunmamaktadır. * Örnek 1, "RAII idiom" kullanan "std::lock_guard" sınıfı; #include #include #include #include #include std::string name{ "Ulya" }; std::mutex mtx; void func(std::string const& surname) { std::lock_guard lg{ mtx }; name += '_' + surname + '@'; std::cout << name << "[" << &name << "]\n"; } int main() { /* # OUTPUT # Ulya_Ulya@[0x55cbd385a160] Ulya_Ulya@_Uskudar@[0x55cbd385a160] Ulya_Ulya@_Uskudar@_Yuruk@[0x55cbd385a160] Ulya_Ulya@_Uskudar@_Yuruk@_29.10.1995@[0x55cbd385a160] Ulya_Ulya@_Uskudar@_Yuruk@_29.10.1995@_Istanbul@[0x55cbd385a160] */ const char* const pa[] = { "Ulya", "Yuruk", "Uskudar", "Istanbul", "29.10.1995" }; std::vector t_vec; for( auto i: pa ) { t_vec.emplace_back(func, i); } for( auto& i: t_vec ) { i.join(); } } * Örnek 2.0, Halihazırda kilitlenmiş "mutex" nesnesi ile hayata gelebilir, hayatının bitmesi durumunda "dtor." fonksiyonu sayesinde ".unlock()" fonksiyonunu çağırtabilir. #include #include #include std::mutex m; int main() { //... m.lock(); { //... std::lock_guard lg(m, std::adopt_lock); //... } // Bu noktada "m" için ".unlock()" fonksiyonu çağrılacaktır. } * Örnek 2.1, #include #include #include unsigned long long counter = 0; std::mutex mtx; void func() { for (unsigned long long i = 0; i < 1'000'000ull; ++i) { mtx.lock(); std::lock_guard lg(mtx, std::adopt_lock); ++counter; } } int main() { { std::jthread t1(func); std::jthread t2(func); std::jthread t3(func); std::jthread t4(func); } std::cout << "counter = " << counter << '\n'; } * Örnek 3, #include #include #include #include #include int gt{}; std::mutex mtx; void func(char c) { std::lock_guard lg{ mtx }; ++gt; std::osyncstream{ std::cout } << c << " " << gt << '\n'; } int main() { /* # OUTPUT # z 1 y 2 x 3 s 4 t 5 u 6 v 7 r 8 b 9 h 10 j 11 l 12 k 13 o 14 e 15 d 16 f 17 n 18 m 19 i 20 c 21 p 22 q 23 g 24 a 25 w 26 */ using namespace std; vector tvec; for (char c = 'a'; c <= 'z'; ++c) { tvec.emplace_back(func, c); } // for (auto& t : tvec) { t.join(); } } * Örnek 4, #include #include #include #include #include std::mt19937 eng{ 454255u }; std::mutex mtx; void foo() { std::uniform_int_distribution dist{ 10, 99 }; std::lock_guard lg{ mtx }; for (int i = 0; i < 10; ++i) { std::cout << dist(eng) << ' '; } std::cout << '\n'; } int main() { /* # OUTPUT # 39 18 40 53 31 30 13 41 99 63 72 71 22 76 61 82 85 69 54 63 72 21 38 21 87 32 88 10 43 89 */ std::thread t1{ foo }; std::thread t2{ foo }; std::thread t3{ foo }; t3.join(); t2.join(); t1.join(); } * Örnek 5, #include #include #include std::mutex mtx; void foo(const std::string& s) { int x = 0; // "automatic lifetime". A "thread" has its own copy. static int y = 0; // "static lifetime". It is unique across the program. thread_local int z = 0; // "thread_local lifetime". A "thread" has its own copy. ++x; // OK ++z; // OK // Way - I: w/o using "RAII idiom" /* mtx.lock(); // Start of Critical Section ++y; std::cout << "thread-" << s << " has id of: [" << std::this_thread::get_id() << "]\n"; // End of Critical Section mtx.unlock(); */ // Way - II: w/ using "RAII idiom" // "Ctor." will call ".lock()", "Dtor." will call ".unlock()". std::lock_guard lg{ mtx }; // Start of Critical Section ++y; std::cout << "thread-" << s << " has id of [" << std::this_thread::get_id() << "] and value of [" << x << "," << y << "," << z << "]\n"; // End of Critical Section // Burada "RAII idiom" yöntemini seçmeliyiz. Eğer "Critical Section" // bölümünden bir hata fırlatırlırsa ve bunu "handle" edemezsek, // ilgili "mutex" nesnesi ".unlock()" edilemeyeceğinden "dead_lock" // oluşacaktır. Artık diğer "thread" lerin bu "Critical Section" // bölümüne erişmesi imkansız hale gelecektir. (bkz. "Stack Unwinding") } int main() { /* # OUTPUT # thread-b has id of [140651935209024] and value of [1,1,1] thread-a has id of [140651943601728] and value of [1,2,1] thread-d has id of [140651918423616] and value of [1,3,1] thread-c has id of [140651926816320] and value of [1,4,1] */ std::thread ta(foo, "a"); std::thread tb(foo, "b"); std::thread tc(foo, "c"); std::thread td(foo, "d"); td.join(); tc.join(); tb.join(); ta.join(); } Öte yandan hem "std::mutex" hem de "std::lock_guard" sınıfları "non-copyable" ve "non-moveable" bir sınıflardır. Tabii şunu da unutmamak gerekir ki birim zamanda tek bir "thread" nesnesi tarafından yapılmasını istediğimiz işin büyüklüğü de programın genel performansı üzerinde etkilidir. Dolayısıyla Kritik Bölge / Kritik Kod ismiyle nitelenen böyle işleri mümkün olduğunda minimal tutmalıyız. * Örnek 1, #include #include #include std::mutex mtx; int t_val{ 0 }; void foo(const std::string& s) { { // Burada "nested" bir blok daha oluşturduk. Böylece ilgili "std::mutex" // nesnesi "foo" fonksiyon çağrısının bitimine kadar değil, bu bloğun // sonuna kadar kilitli kalacaktır. std::lock_guard lg(mtx); ++t_val; std::cout << "thread-" << s << " has value of " << t_val << '\n'; } //... } int main() { /* # OUTPUT # thread-main has value of 0 thread-d has value of 1 thread-b has value of 2 thread-a has value of 3 thread-c has value of 4 */ std::thread ta(foo, "a"); std::thread tb(foo, "b"); std::thread tc(foo, "c"); std::thread td(foo, "d"); { std::lock_guard lg{mtx}; std::cout << "thread-" << "main" << " has value of " << t_val << '\n'; } ta.join(); tb.join(); tc.join(); td.join(); } * Örnek 2, Aşağıdaki örnekte bulunan fonksiyonlar arasında en verimlisi "foo_III" isimli fonksiyondur. //... std::mutex mtx; int shared_variable{}; void foo_I() { std::lock_guard lg{mtx}; // Döngü bitene kadar ilgili "mutex" kilitli tutulacaktır. for (int i = 0; i < 100; ++i) ++shared_variable; } void foo_II() { for (int i = 0; i < 100; ++i) { // Döngünün her turunda ilgili "mutex" önce kilitlenecek, sonra açılacaktır. std::lock_guard lg{mtx}; ++shared_variable; } } void foo_III() { int cnt = 0; for (int i = 0; i < 100; ++i) ++cnt; std::lock_guard lg{mtx}; // Sadece yerel değişkenimizin değerini kopyalarken ilgili "mutex" kilitli kalacaktır. shared_variable = cnt; } int main() { //... } Son olarak "std::mutex" gibi senkronize nesnelerini kullanırken de "dead_lock" oluşmamasına dikkat etmeliyiz. * Örnek 1.0, Aşağıdaki örnekte ilkin "a_mtx" ve "b_mtx" nesneleri kilitlenecektir. Daha sonra 500ms iki "thread" de bekletilecektir. Sonrasındada "b_mtx" ve "a_mtx" nesneleri kilitlenmek istenecektir fakat halihazırda her ikisi de kilitli olduğundan, bu aşamada iki "thread" de ilgili "mutex" nesnesinin açılmasını bekleyecektir. Fakat açılma aşaması diğer adımlarda olduğundan, "dead_lock" oluşacaktır. #include #include #include #include std::mutex a_mtx; std::mutex b_mtx; using namespace std::literals; namespace td = std::this_thread; void foo() { std::osyncstream{ std::cout } << td::get_id() << " is trying to lock a_mtx\n"; a_mtx.lock(); std::osyncstream{ std::cout } << td::get_id() << " has locked a_mtx\n"; std::this_thread::sleep_for(500ms); std::osyncstream{ std::cout } << td::get_id() << " is trying to lock b_mtx\n"; b_mtx.lock(); std::osyncstream{ std::cout } << td::get_id() << " has locked b_mtx\n"; a_mtx.unlock(); std::osyncstream{ std::cout } << td::get_id() << " has unlocked a_mtx\n"; b_mtx.unlock(); std::osyncstream{ std::cout } << td::get_id() << " has unlocked b_mtx\n"; } void bar() { std::osyncstream{ std::cout } << td::get_id() << " is trying to lock b_mtx\n"; b_mtx.lock(); std::osyncstream{ std::cout } << td::get_id() << " has locked b_mtx\n"; std::this_thread::sleep_for(500ms); std::osyncstream{ std::cout } << td::get_id() << " is trying to lock a_mtx\n"; a_mtx.lock(); std::osyncstream{ std::cout } << td::get_id() << " has locked a_mtx\n"; b_mtx.unlock(); std::osyncstream{ std::cout } << td::get_id() << " has unlocked b_mtx\n"; a_mtx.unlock(); std::osyncstream{ std::cout } << td::get_id() << " has unlocked a_mtx\n"; } int main() { /* # OUTPUT # 17100 is trying to lock a_mtx 17100 has locked a_mtx 17560 is trying to lock b_mtx 17560 has locked b_mtx 17100 is trying to lock b_mtx 17560 is trying to lock a_mtx :> */ std::jthread t1{ foo }; std::jthread t2{ bar }; } * Örnek 1.1, #include #include #include #include std::mutex a_mtx; std::mutex b_mtx; using namespace std::literals; namespace td = std::this_thread; void foo() { std::osyncstream{ std::cout } << td::get_id() << " is trying to lock a_mtx\n"; a_mtx.lock(); std::osyncstream{ std::cout } << td::get_id() << " has locked a_mtx\n"; std::this_thread::sleep_for(500ms); std::osyncstream{ std::cout } << td::get_id() << " is trying to lock b_mtx\n"; b_mtx.lock(); std::osyncstream{ std::cout } << td::get_id() << " has locked b_mtx\n"; a_mtx.unlock(); std::osyncstream{ std::cout } << td::get_id() << " has unlocked a_mtx\n"; b_mtx.unlock(); std::osyncstream{ std::cout } << td::get_id() << " has unlocked b_mtx\n"; } void bar() { std::osyncstream{ std::cout } << td::get_id() << " is trying to lock a_mtx\n"; a_mtx.lock(); std::osyncstream{ std::cout } << td::get_id() << " has locked a_mtx\n"; std::this_thread::sleep_for(500ms); std::osyncstream{ std::cout } << td::get_id() << " is trying to lock b_mtx\n"; b_mtx.lock(); std::osyncstream{ std::cout } << td::get_id() << " has locked b_mtx\n"; a_mtx.unlock(); std::osyncstream{ std::cout } << td::get_id() << " has unlocked a_mtx\n"; b_mtx.unlock(); std::osyncstream{ std::cout } << td::get_id() << " has unlocked b_mtx\n"; } int main() { /* # OUTPUT # 13908 is trying to lock a_mtx 14320 is trying to lock a_mtx 13908 has locked a_mtx 13908 is trying to lock b_mtx 13908 has locked b_mtx 13908 has unlocked a_mtx 13908 has unlocked b_mtx 14320 has locked a_mtx 14320 is trying to lock b_mtx 14320 has locked b_mtx 14320 has unlocked a_mtx 14320 has unlocked b_mtx */ std::jthread t1{ foo }; std::jthread t2{ bar }; } * Örnek 2.0, "dead_lock" oluşmasını yine kod bakarak rahat bir şekilde fark edemeyebiliriz; #include #include #include #include class Buffer { public: static constexpr auto size() { return 16u * 1024u * 1024u; } Buffer() : m_data(size()) {} Buffer(const Buffer&) = delete; Buffer & operator=(Buffer&&) = delete; void swap(Buffer & other) { if (this == &other) return; std::lock_guard lock1(mtx); std::lock_guard lock2(other.mtx); std::swap(m_data, other.m_data); } // ... private: std::vector m_data; mutable std::mutex mtx; }; Buffer buf_x; Buffer buf_y; int main() { std::jthread t1([]() { for (long i = 0; i < 1'000'000; ++i) buf_x.swap(buf_y); }); std::jthread t2([]() { for (long i = 0; i < 1'000'000; ++i) buf_y.swap(buf_x); //!!! }); } * Örnek 2.1, #include #include #include #include class Buffer { public: static constexpr auto size() { return 16u * 1024u * 1024u; } Buffer() : m_data(size()) {} Buffer(const Buffer&) = delete; Buffer & operator=(Buffer&&) = delete; void swap(Buffer & other) { if (this == &other) return; std::lock(mtx, other.mtx); std::swap(m_data, other.m_data); mtx.unlock(); other.mtx.unlock(); } // ... private: std::vector m_data; mutable std::mutex mtx; }; Buffer buf_x; Buffer buf_y; int main() { std::jthread t1([]() { for (long i = 0; i < 1'000'000; ++i) buf_x.swap(buf_y); }); std::jthread t2([]() { for (long i = 0; i < 1'000'000; ++i) buf_y.swap(buf_x); //!!! }); } * Örnek 3, Yine hata nesnesinin gönderilmesinden dolayı da bir "dead_lock" oluşabilir. #include #include #include #include using namespace std; using namespace std::literals; std::mutex gmtx; void func(int x) { gmtx.lock(); try { osyncstream{ std::cout } << this_thread::get_id() << " " << "locked the mutex\n"; osyncstream{ std::cout } << "x = " << x << '\n'; if (x % 2 == 0) throw invalid_argument{ "no even number" }; gmtx.unlock(); osyncstream{ std::cout } << this_thread::get_id() << " " << "unocked the mutex\n"; } catch (const exception& ex) { osyncstream{ std::cout } << this_thread::get_id() << " exception caught : " << ex.what() << '\n'; } } int main() { jthread t1{ func, 4 }; jthread t2{ func, 5 }; } * Örnek 4, #include #include #include #include using namespace std; using namespace std::literals; std::mutex gmtx; void func(int x) { std::lock_guard lg{ gmtx }; try { osyncstream{ std::cout } << this_thread::get_id() << " " << "locked the mutex\n"; osyncstream{ std::cout } << "x = " << x << '\n'; if (x % 2 == 0) throw invalid_argument{ "no even number" }; osyncstream{ std::cout } << this_thread::get_id() << " " << "unocked the mutex\n"; } catch (const exception& ex) { osyncstream{ std::cout } << this_thread::get_id() << " exception caught : " << ex.what() << '\n'; } } int main() { jthread t1{ func, 4 }; jthread t2{ func, 5 }; } Yine "dead_lock" oluşmasını engellemek için "std::lock" isimli fonksiyonu kullanabiliriz. >>>>> "std::lock" : Değişken sayıda argümanla bu fonksiyona çağrı yapabiliriz. Bu fonksiyon, kendisine gönderilen birden fazla "mutex" nesnelerinin hepsini kilitliyor. Fakat arka planda "dead_lock_avoidance" algoritması da kullanıyor. Dolayısıyla artık "dead_lock" olma ihtimali kalmıyor. * Örnek 1, #include #include #include std::mutex m1, m2; void foo() { std::lock(m1, m2); m1.unlock(); m2.unlock(); std::osyncstream{ std::cout } << "foo()\n"; } void bar() { std::lock(m2, m1); m1.unlock(); m2.unlock(); std::osyncstream{ std::cout } << "bar()\n"; } int main() { std::thread t1{ foo }; std::thread t2{ bar }; t1.join(); t2.join(); } >>>> "std::recursive_mutex" : Bu sınıf ise birden fazla "thread" tarafından kilitlenmeye olanak sağlar. Fakat maksimum kaç adet tarafından kilitlenmeye izin verileceği "Implementation Defined". Çalışma zamanına ilişkin maliyet doğurduğundan, kullanımına dikkat etmeliyiz. * Örnek 1.0, #include #include #include std::recursive_mutex rmtx; int gcount = 0; void rfunc(char c, int n) { if (n < 0) return; rmtx.lock(); std::cout << c << ' ' << gcount++ << '\n'; rfunc(c, n - 1); rmtx.unlock(); } int main() { std::thread tx{ rfunc, 'x', 10 }; std::thread ty{ rfunc, 'y', 10 }; tx.join(); ty.join(); } * Örnek 1.1, #include class DatabaseAccess { public: void create_table() { std::lock_guard lg{ db_mutex }; //... } void insert_data() { std::lock_guard lg{ db_mutex }; //.. } void create_table_and_insert_data() { std::lock_guard lg{ db_mutex }; create_table(); //... } private: std::recursive_mutex db_mutex; //... }; int main() { DatabaseAccess dx; dx.create_table_and_insert_data(); //deadlock } * Örnek 1.2, #include #include class Nec { public: void func() { std::lock_guard guard{ mtx }; std::cout << std::this_thread::get_id() << " func cagrildi\n"; foo(); std::cout << std::this_thread::get_id() << " func sona eriyor\n"; } void foo() { std::lock_guard guard{ mtx }; std::cout << std::this_thread::get_id() << " foo cagrildi\n"; std::cout << std::this_thread::get_id() << " foo sona eriyor\n"; } private: mutable std::recursive_mutex mtx; }; void gf() { Nec nec; nec.func(); } int main() { std::jthread t1{ gf }; std::jthread t2{ gf }; } >>>> "std::timed_mutex" : Bu sınıf bir müddet boyunca kilitlenmeye olanak tanır. * Örnek 1.0, #include #include #include #include int gcounter{}; std::timed_mutex mtx; void increment(int i) { using namespace std::literals; if (mtx.try_lock_for(50ms)) { ++gcounter; std::this_thread::sleep_for(10ms); std::cout << "thread : " << i << " kritik bolgeye girdi\n"; mtx.unlock(); } else std::cout << "thread " << i << " kritik bolgeye giremedi\n"; } int main() { std::thread t1{ increment, 1 }; std::thread t2{ increment, 2 }; t1.join(); t2.join(); std::cout << "gcounter = " << gcounter << '\n'; } * Örnek 1.1, #include #include #include #include #include #include #include std::timed_mutex mtx; void task(int id) { using namespace std::literals; int cnt{}; for (int i{}; i < 10'000; ++i) { if (mtx.try_lock_for(1us)) { ++cnt; mtx.unlock(); } } std::osyncstream{ std::cout } << id << " " << ++cnt << '\n'; } int main() { std::vector tvec; for (int i{}; i < 10; ++i) { tvec.emplace_back(task, i); } } >>>> "std::shared_mutex" : C++17 ile dile eklenmiştir. Amacı şudur; belli işlevi olan "thread" ler tarafından kilitlenebilsin, belli işlevi olanlar bloke edilsin. Örneğin, yazma işlemi yapmak isteyen bütün "thread" ler bloke edilirken, okuma yapmak isteyen "thread" ler ise okuma yapabilsin. "shared_mutex" isimli başlık dosyasındadır. Pekala bu "mutex" nesnesini de "RAII idiom" kullanan şu sınıflar ile birlikte kullanabiliriz; "std::lock_guard", "std::unique_lock", "scoped_lock", "std::shared_lock" Bu sınıflardan "std::lock_guard", "std::unique_lock" ve "scoped_lock" sınıfları Kritik Bölgeye sadece tek bir "thread" in girmesine olanak tanırken, "std::shared_lock" sınıfı birden fazla "thread" in girmesine olanak tanır. * Örnek 1, Bu "mutex" nesnesinin tipik kullanım alanı "read" amacı güden "thread" lerin adedinin "write" amacı güdenlerden fazla olduğu durumlardır. #include #include #include #include #include #include #include int cnt{}; std::shared_mutex mtx; using namespace std::literals; void Writer() { for(int i = 0; i < 3; ++i) { std::scoped_lock sl{ mtx }; // Bu noktaya sadece tek bir "thread" // girebilecek, diğerleri bloke edilecek. ++cnt; } std::this_thread::sleep_for(20ms); } void Reader() { for(int i = 0; i < 6; ++i) { int c; { std::shared_lock sl{ mtx }; // Birden fazla "thread" bu aşamaya gelebilir, // eş zamanlı okuma yapabilir. c = cnt; } std::osyncstream{ std::cout } << "thread-" << std::this_thread::get_id() << ": " << c << '\n'; std::this_thread::sleep_for(20ms); } } int main() { std::vector jvec; jvec.reserve(9); for(auto i{0}; i < 3; ++i) jvec.emplace_back(Writer); for(auto i{0}; i < 6; ++i) jvec.emplace_back(Reader); } Bu örnekte kullanılan "std::scoped_lock", "std::lock_guard" sınıfının daha verimli halidir. Bu sınıf, o sınıfın yaptıklarının neredeyse tamamını yapabilmekte ve ilave bir maliyet de sunmamaktadır. Örneğin, "dead_lock_avoidance" bir mekanizma ile birden fazla "mutex" nesnesini kilitleyebilmektedir. C++17 ile dile eklendi. Tek bir "mutex" nesnesi üzerinde çalışırken bile bu sınıfın kullanılmasının kötü bir etkisi bulunmamaktadır. Yine örnekteki "std::shared_lock" için; "shared_mutex" isimli başlık dosyasındadır. Kopyalamaya izin veren bir sınıftır. Detaylar için bkz. https://github.com/necatiergin/CONCURRENCY/blob/main/mutex/shared_mutex/notlar.md * Örnek 2.0, "std::lock_guard" ve "std::scoped_lock" a nazaran "std::unique_lock" nesnesini de kullanabiliriz. Bu sınıf taşımaya karşı açık ve diğerine göre daha fazla fonksiyonu sunmaktadır. Bir diğer deyişle bu yeni sınıfımız olabilecek en yetenekli "RAII" sınıfıdır. Diğer iki "_lock" sınıfları gibi "scope" sonuna kadar ilgili "mutex" nesnesini kilitli tutmazlar, ömürleri boyunca istenildiği kadar ilgili "mutex" nesnesini ".lock()" ve ".unlock()" edebilirler. Yine "dtor." çağrıları da ilgili "mutex" nesnesini ".unlock()" edecektir. Fakat bu sınıf kopyalamaya karşı kapalıdır. Eğer bu sınıfın hizmetlerinden fayda görmeyeceksek, "std::lock_guard" ve/veya "std::scoped_lock" sınıflarını kullanabiliriz. #include std::mutex mtx; void f1() { // "RAII" deyimini kullanabiliriz. //std::unique_lock lock(mtx); std::unique_lock lock(mtx); } void f2() { // Halihazırda kilitli olan "mutex" nesnesini alıp, // otomatikman ".unlock()" edilmesini sağlatabiliriz. mtx.lock(); std::unique_lock lock(mtx, std::adopt_lock); } void f3() { // Bir "mutex" nesnesini kilitsiz olarak alır, istediğimiz zaman // kilitleyedebiliriz. std::unique_lock ulock(mtx, std::defer_lock); ulock.lock(); } void f4() { // Aldığı "mutex" nesnesinin ".try_lock" fonksiyonunu // çağırtabilir, duruma göre farklı aksiyonlar alabilirz. std::unique_lock ulock(mtx, std::try_to_lock); // Fakat "ctor." fonkisyonunun geri dönüş değeri olmadığından, // ".try_lock()" çağrısının sonucunu aşağıdaki biçimde sorgulamalıyız. if (ulock.owns_lock()) { //... } } * Örnek 2.1, 500ms boyunca kilitlemeye çalışacaktır. #include #include std::timed_mutex mtx; void func() { using namespace std::literals; //std::unique_lock ulock(mtx, 500ms); std::unique_lock ulock(mtx, 500ms); //... } * Örnek 2.2, Kilitsiz bir şekilde ilgili "mutex" nesnesini alıyoruz. Sınıfın "dtor." fonksiyonu kilidi açacaktır. #include #include #include unsigned long gcount{}; std::mutex mtx; void foo() { for (unsigned long i{}; i < 1'000'000ul; ++i) { std::unique_lock lock(mtx, std::defer_lock); lock.lock(); //mutex'i ediniyor. ++gcount; lock.unlock(); // mutex'i serbest bırakıyor. // ... lock.lock(); //mutex'i ediniyor. ++gcount; lock.unlock(); // mutex'i serbest bırakıyor. // ... // mutex edinilmiş durumda ise burada destructor serbest bırakıyor } } int main() { { std::jthread t1(foo); std::jthread t2(foo); } std::cout << gcount << '\n'; //4000000 } >>> "std::future" ve "std::promise" : Bu sınıfları kullanılarak iki "thread" arasında haberleşme sağlanabilir. Her iki sınıfın başlık dosyası "future" ismindedir. Buradaki "std::promise" sınıfı değişikliği yapacak, "std::future" ise bu değişikliği görecek sınıftır. Fakat haberleşme sadece bir defalıktır. Bu iki sınıf kullanılarak hata nesnesi de paylaşılabilir, diğer değerler de. * Örnek 1.0, Bu iki sınıfı farklı "thread" ile çalıştırmak zorunda değiliz. Tek bir "thread" kullanarak bir değeri paylaşabiliriz. #include #include int main() { std::promise prom; std::future ftr = prom.get_future(); std::cout << "The value to be sent: " << 991 << '\n'; // The value to be sent: 991 prom.set_value(991); auto val = ftr.get(); std::cout << "The value to be read: " << val << '\n'; // The value to be read: 991 } * Örnek 1.1, Pekala bir hata nesnesini de aynı yöntemle paylaşabiliriz. #include #include #include int main() { std::promise prom; std::future ftr = prom.get_future(); prom.set_exception(std::make_exception_ptr(std::runtime_error{ "The value could not be set!" })); try { auto val = ftr.get(); } catch(const std::exception& ex) { std::cout << "Ex: [" << ex.what() << "]\n"; // Ex: [The value could not be set!] } } * Örnek 2.0.0, İki "thread" arasında haberleşmeyi bir fonksiyonun geri dönüş değeri ile sağlatmak istiyorsak ilgili fonksiyonun bir parametresi "std::promise" türünden olmalıdır. Bu parametre referans türünden olduğu gibi değer türünden de olabilir. "R-Value Reference" ve değer türünden olması durumunda "std::move()" ile sarmalamalı, "L-Value Reference" olması durumunda ise "std::ref" ile sarmalamalıyız, bu parametreye geçilecek argümanı. #include #include #include #include double square(double dval) { //... return dval * dval; } void calculate_square(std::promise prom, double dval) { //... prom.set_value(square(dval)); } int main() { std::promise prom; std::future ftr = prom.get_future(); std::jthread jt{ calculate_square, std::move(prom), 5.68 }; std::cout << "Calculated Value: " << ftr.get() << '\n'; // Calculated Value: 32.2624 } * Örnek 2.0.1, Pekala fonksiyon kullanmak yerine diğer "callable" nesneleri de pekala kullanabiliriz. #include #include #include #include void calculate_sum_square(std::promise prom, double dval_l, double dval_r) { //... prom.set_value(dval_l*dval_r + dval_l*dval_r); } struct CalculateSumSquare { void operator()(std::promise& prom, double dval_l, double dval_r) { //... prom.set_value(dval_l*dval_r + dval_l*dval_r); } }; int main() { double d1{ 341.143 }, d2{ 134.431 }; // Using Functions std::promise prom_func; std::future ftr_func = prom_func.get_future(); std::jthread jt_func{ calculate_sum_square, std::move(prom_func), d1, d2 }; std::cout << "Calculated Value: " << ftr_func.get() << '\n'; // Calculated Value: 91720.4 // Using Functor std::promise prom_Struct; std::future ftr_Struct = prom_Struct.get_future(); std::jthread jt_Struct{ CalculateSumSquare{}, std::ref(prom_Struct), d1, d2 }; std::cout << "Calculated Value: " << ftr_Struct.get() << '\n'; // Calculated Value: 91720.4 // Using Lambda std::promise prom_Lambda; std::future ftr_Lambda = prom_Lambda.get_future(); std::jthread jt_Lambda{ [](std::promise&& prom, double dval_l, double dval_r){ prom.set_value(dval_l*dval_r + dval_l*dval_r); }, std::move(prom_Lambda), d1, d2 }; std::cout << "Calculated Value: " << ftr_Lambda.get() << '\n'; // Calculated Value: 91720.4 } * Örnek 2.1.0, Yine hata nesnesi de kullanabiliriz. #include #include #include #include void calculate_sum_square(std::promise prom, double dval_l, double dval_r) { //... try { if (dval_l == 0 || dval_r == 0) throw std::invalid_argument("Arguments must be non-zero!"); } catch (...) { prom.set_exception(std::current_exception()); return; } prom.set_value(dval_l*dval_r + dval_l*dval_r); } struct CalculateSumSquare { void operator()(std::promise& prom, double dval_l, double dval_r) { //... try { if (dval_l == 0 || dval_r == 0) throw std::invalid_argument("Arguments must be non-zero!"); } catch (...) { prom.set_exception(std::current_exception()); return; } prom.set_value(dval_l*dval_r + dval_l*dval_r); } }; int main() { double d1{ 341.143 }, d2{ 0 }; // Using Functions std::promise prom_func; std::future ftr_func = prom_func.get_future(); std::jthread jt_func{ calculate_sum_square, std::move(prom_func), d1, d2 }; try { std::cout << "Calculated Value: " << ftr_func.get() << '\n'; // Calculated Value: Ex: [Arguments must be non-zero!] } catch(const std::exception& ex) { std::cout << "Ex: [" << ex.what() << "]\n"; } // Using Functor std::promise prom_Struct; std::future ftr_Struct = prom_Struct.get_future(); std::jthread jt_Struct{ CalculateSumSquare{}, std::ref(prom_Struct), d1, d2 }; try { std::cout << "Calculated Value: " << ftr_Struct.get() << '\n'; // Calculated Value: Ex: [Arguments must be non-zero!] } catch(const std::exception& ex) { std::cout << "Ex: [" << ex.what() << "]\n"; } // Using Lambda std::promise prom_Lambda; std::future ftr_Lambda = prom_Lambda.get_future(); std::jthread jt_Lambda{ [](std::promise&& prom, double dval_l, double dval_r){ try { if (dval_l == 0 || dval_r == 0) throw std::invalid_argument("Arguments must be non-zero!"); } catch (...) { prom.set_exception(std::current_exception()); return; } prom.set_value(dval_l*dval_r + dval_l*dval_r); }, std::move(prom_Lambda), d1, d2 }; try { std::cout << "Calculated Value: " << ftr_Lambda.get() << '\n'; // Calculated Value: Ex: [Arguments must be non-zero!] } catch(const std::exception& ex) { std::cout << "Ex: [" << ex.what() << "]\n"; } } * Örnek 2.1.1, Yine "std::make_exception_ptr" fonksiyonunu kullanarak içerideki "try-catch" bloğundan kurtulabiliriz. #include #include #include #include void calculate_sum_square(std::promise prom, double dval_l, double dval_r) { //... if (dval_l == 0 || dval_r == 0) { prom.set_exception(std::make_exception_ptr(std::invalid_argument("Arguments must be non-zero!"))); return; } prom.set_value(dval_l*dval_r + dval_l*dval_r); } struct CalculateSumSquare { void operator()(std::promise& prom, double dval_l, double dval_r) { //... if (dval_l == 0 || dval_r == 0) { prom.set_exception(std::make_exception_ptr(std::invalid_argument("Arguments must be non-zero!"))); return; } prom.set_value(dval_l*dval_r + dval_l*dval_r); } }; int main() { double d1{ 341.143 }, d2{ 0 }; // Using Functions std::promise prom_func; std::future ftr_func = prom_func.get_future(); std::jthread jt_func{ calculate_sum_square, std::move(prom_func), d1, d2 }; try { std::cout << "Calculated Value: " << ftr_func.get() << '\n'; // Calculated Value: Ex: [Arguments must be non-zero!] } catch(const std::exception& ex) { std::cout << "Ex: [" << ex.what() << "]\n"; } // Using Functor std::promise prom_Struct; std::future ftr_Struct = prom_Struct.get_future(); std::jthread jt_Struct{ CalculateSumSquare{}, std::ref(prom_Struct), d1, d2 }; try { std::cout << "Calculated Value: " << ftr_Struct.get() << '\n'; // Calculated Value: Ex: [Arguments must be non-zero!] } catch(const std::exception& ex) { std::cout << "Ex: [" << ex.what() << "]\n"; } // Using Lambda std::promise prom_Lambda; std::future ftr_Lambda = prom_Lambda.get_future(); std::jthread jt_Lambda{ [](std::promise&& prom, double dval_l, double dval_r){ if (dval_l == 0 || dval_r == 0) { prom.set_exception(std::make_exception_ptr(std::invalid_argument("Arguments must be non-zero!"))); return; } prom.set_value(dval_l*dval_r + dval_l*dval_r); }, std::move(prom_Lambda), d1, d2 }; try { std::cout << "Calculated Value: " << ftr_Lambda.get() << '\n'; // Calculated Value: Ex: [Arguments must be non-zero!] } catch(const std::exception& ex) { std::cout << "Ex: [" << ex.what() << "]\n"; } } * Örnek 3.0, "std::future" sınıfının ".get()" fonksiyonunu iki defa çağırmamız "std::future_error" sınıfı türünden hata nesnesi gönderilmesine neden olur ki bu sınıf da yine "std::exception" sınıfından kalıtım yoluyla elde edilmiştir. #include #include #include int main() { std::promise d_prom; auto ftr = d_prom.get_future(); d_prom.set_value(123.321); std::cout << "Read Value: " << ftr.get() << '\n'; // Read Value: 123.321 try { std::cout << "Read Value: " << ftr.get() << '\n'; } catch (const std::exception& ex) { std::cout << "Ex: [" << ex.what() << "]\n"; // Read Value: Ex: [std::future_error: No associated state] } } * Örnek 3.1, Benzer şekilde "std::promise" sınıfının ".get_future()" fonksiyonunu iki defa çağırmamız da "std::future_error" sınıfı türünden hata nesnesi gönderilmesine neden olur ki bu sınıf da yine "std::exception" sınıfından kalıtım yoluyla elde edilmiştir. #include #include #include #include int main() { std::promise d_prom; auto ftr1 = d_prom.get_future(); d_prom.set_value(123.321); std::cout << "Read Value: " << ftr1.get() << '\n'; // Read Value: 123.321 try { auto ftr2 = d_prom.get_future(); } catch (const std::exception& ex) { std::cout << "Ex: [" << ex.what() << "]\n"; // Ex: [std::future_error: Future already retrieved] } } * Örnek 4.0, "std::future" sınıfının ".wait" isimli fonksiyonları beklenen değer hesaplanıncaya dek, kendisini çağıran "thread" in bloke olmasını sağlamaktadır. ".wait", ".wait_for" ve ".wait_until" olmak üzere üç adettir. Bu üç fonksiyondan ".wait" fonksiyonu geri dönüş değerine sahip değilken, ".wait_for" ve ".wait_until" fonksiyonları "std::future_status" türünden bir numaralandırma sabiti döndürmektedir. Bu "std::future_status" türü ise üç değere sahiptir; "ready", "timeout" ve "deferred". Fonksiyonumuz bu değerlerden "ready" olanını döndürürse beklenen değerin hesaplandığını, "timeout" döndürürse sürecin zaman aşımına uğradığını, "deferred" döndürürse de hesaplama işleminin "std::future" sınıfının ".get()" fonksiyonu çağrıldığında yapılacağını belirtmektedir. #include #include #include using namespace std::literals; void func(std::promise x) { std::this_thread::sleep_for(3s); x.set_value(25); } int main() { /* # OUTPUT # Some work ... Some work ... Some work ... Some work ... Some work ... Some work ... Some work ... Some work ... Some work ... Some work ... Some work ... Some work ... Some work ... Some work ... Some work ... value is : 25 */ std::promise prom; auto ftr = prom.get_future(); std::jthread tx{ func, std::move(prom) }; std::future_status status{}; do { status = ftr.wait_for(200ms); std::cout << "Some work ...\n"; } while (status != std::future_status::ready); std::cout << "value is : " << ftr.get() << '\n'; } * Örnek 4.1, #include #include #include constexpr int x = 50; long long fib(int n) { return n < 3 ? 1 : fib(n - 1) + fib(n - 2); } int main() { /* # OUTPUT # bekle cevap gelecek ............................................................................... ............................................................................... ............................................................................... ............................................................................... ............................................................................... ib(50) is : 12586269025 */ using namespace std::literals; auto ftr = std::async(fib, x); std::cout << "bekle cevap gelecek\n"; while (ftr.wait_for(10ms) == std::future_status::timeout) std::cout << '.' << std::flush; auto result = ftr.get(); std::cout << "fib(" << x << ") is : " << result << '\n'; } * Örnek 5, Eğer kullanılan "std::future" nesnesini tekrar tekrar kullanmak istiyorsak "std::shared_future" sınıfını kullanmalıyız. Bu sınıf kopyalamaya müsait bir sınıftır. #include #include #include #include #include struct SumSquare { void operator()(std::promise&& prom, int a, int b) { prom.set_value(a * a + b * b); } }; void func(std::shared_future sftr) { std::osyncstream os{ std::cout }; os << "threadId (" << std::this_thread::get_id() << "): "; os << "result = " << sftr.get() << '\n'; } int main() { /* # OUTPUT # threadId (139986838124096): result = 106 threadId (139986821338688): result = 106 threadId (139986741491264): result = 106 threadId (139986829731392): result = 106 threadId (139986733098560): result = 106 */ std::promise prom; std::shared_future sftr = prom.get_future(); std::jthread th(SumSquare{}, std::move(prom), 5, 9); std::jthread t1(func, sftr); std::jthread t2(func, sftr); std::jthread t3(func, sftr); std::jthread t4(func, sftr); std::jthread t5(func, sftr); } >>> "std::async" : İşte "std::future" ve "std::promise" gibi nesneler ile uğraşmamak için kütüphanedeki "std::async" isimli fonksiyon şablonunu kullanabiliriz. Argüman olarak bizden çağrılacak "callable" nesne ile bu nesneye geçilecek argümanları istemektedir. Fonksiyonun bir diğer "overload" versiyonu için de argüman olarak senkron(geri dönüş değerinin ".get()" fonksiyonu çağrılırsa, kod çalıştırılmaya başlayacak) mu asenkron(ayrı bir "thread" oluşturulacak) mu çalıştırılma bilgisini geçiyoruz. Pekala bu bilgileri "|" operatörü ile birleştirmek suretyile de fonksiyona geçebiliriz. Bu durumda kararı derleyiciye bırakmış oluruz. Geri dönüş değeri olarak da bir "std::future" nesnesi döndürmektedir. Eğer ".get()" fonksiyonu çağrıldığında işlemler hala devam ediyorsa, o "thread" bloke edilecektir. * Örnek 1, #include #include #include using namespace std::literals; int func(int x, int y) { return x+y; } int main() { // Çalıştırma bilgisi derleyiciye bırakıldı. // auto ft = std::async(std::launch::deferred | std::launch::async, func, 13, 31); auto ft = std::async(func, 13, 31); std::cout << "Result: " << ft.get() << '\n'; // Result: 44 // ".get()" fonksiyon çağrısından sonra kod çalışmaya başlayacaktır. // Artık ".get()" çağrısını yapan "thread", kodu çalıştıracaktır. // Dolayısıyla "async" gibi "exception" gönderme ihtimali yoktur. ft = std::async(std::launch::deferred, func, 13, 31); std::cout << "Result: " << ft.get() << '\n'; // Result: 44 // Ayrı bir "thread" oluşturulacak. "exception" fırlatabilir. ft = std::async(std::launch::async, func, 13, 31); std::cout << "Result: " << ft.get() << '\n'; // Result: 44 // Eğer derleyiciye bırakırsak ve o da "deferred" seçerse // fakat biz de "async" varsayımında bulunarak ".get()" // yapmazsak, kod çalıştırılmayacaktır. Dolayısıyla kodun // çalıştırılmasını garanti altına almak istiyorsak "async" // olarak belirtmeliyiz eğer ".get()" çağrısının yapımında // şüphe varsa. } * Örnek 2.0, #include #include #include unsigned long long fibo(unsigned long long n) { return n < 3 ? 1 : fibo(n - 1) + fibo(n - 2); } using namespace std::literals; int main() { auto tp_start = std::chrono::steady_clock::now(); auto result = fibo(42) + fibo(44); auto tp_end = std::chrono::steady_clock::now(); // Duratiton: 4.80863 std::cout << "Duratiton: " << std::chrono::duration(tp_end - tp_start).count() << '\n'; } * Örnek 2.1, #include #include #include unsigned long long fibo(unsigned long long n) { return n < 3 ? 1 : fibo(n - 1) + fibo(n - 2); } using namespace std::literals; int main() { auto tp_start = std::chrono::steady_clock::now(); auto result_1 = std::async(fibo, 42); auto result_2 = fibo(44); auto result = result_1.get() + result_2; auto tp_end = std::chrono::steady_clock::now(); // Duratiton: 4.75029 std::cout << "Duratiton: " << std::chrono::duration(tp_end - tp_start).count() << '\n'; } * Örnek 3.0, "std::launch::async" ve "std::launch::deferred" arasındaki farkı gösteren güzel bir örnek. #include #include #include int main() { using namespace std::chrono_literals; std::chrono::time_point start = std::chrono::steady_clock::now(); auto eager = std::async(std::launch::async, [] {return std::chrono::steady_clock::now(); }); auto lazy = std::async(std::launch::deferred, [] {return std::chrono::steady_clock::now(); }); // "std::launch::async" ile oluşturulan "thread" DEĞİL, // "std::launch::deferred" ile oluşturulan bir saniye bekleyecektir. std::this_thread::sleep_for(1s); using dsec = std::chrono::duration; auto eager_sec = duration_cast(eager.get() - start).count(); auto deferred_sec = duration_cast(lazy.get() - start).count(); // duration for eager in sec : 0.00690772 std::cout << "duration for eager in sec : " << eager_sec << '\n'; // duration for deferred in sec : 1.00023 std::cout << "duration for deferred in sec : " << deferred_sec << '\n'; } * Örnek 3.1, #include #include void func() { std::cout << "func\n"; } int main() { std::cout << "main basladi\n"; std::async(std::launch::async, func); // Geri dönüş değerinin ömrü biteceğinden, // yine "dtor." üzerinden ".get()" çağrısı // otomatik olarak yapılacaktır. Dolayısıyla // bu çağrının aşağıdaki çağrıdan bir farkı // kalmamış olacaktır; // func(); std::cout << "main devam ediyor\n"; { auto ft = std::async(std::launch::async, func); // Arka planda "dtor." fonksiyonu "std::future" // sınıfının ".get()" fonksiyonunu çağıracaktır. } std::cout << "main hala devam ediyor\n"; { auto ft = std::async(std::launch::deferred, func); // "std::future" sınıfının ".get()" fonksiyonunu // çağırmadığımız müddetçe kod çalışmayacaktır. } std::cout << "main bitecek\n"; } * Örnek 4.0, "std::launch::async" ve "std::launch::deferred" birlikte kullanımına ilişkin örnek. #include #include #include #include #include #include using namespace std; using namespace std::chrono; int task(char ch) { mt19937 eng{ random_device{}() }; std::uniform_int_distribution dist{ 20, 500 }; int total_duration{}; for (int i = 0; i < 20; ++i) { auto dur = milliseconds(dist(eng)); this_thread::sleep_for(dur); cout << ch << flush; total_duration += static_cast(dur.count()); } return total_duration; } int foo() { return task('!'); } int bar() { return task('?'); } int main() { /* # OUTPUT # starting foo() in background and bar() in foreground: ??!!!?!!?!!???!??!!!?!?!?!!!!?!?!?!????? result = 8875 */ cout << "starting foo() in background and bar() in foreground:" << '\n'; // "launch policy", derleyiciye bırakıldı. future foo_result = async(foo); // "main-thread" or "another-thread" const auto bar_result = bar(); // "main-thread" const auto result = foo_result.get() + bar_result; cout << "\nresult = " << result << '\n'; } * Örnek 4.1, #include #include #include #include #include #include std::map histogram(const std::string& str) { std::map cmap{}; for (const auto c : str) { ++cmap[c]; } return cmap; } std::string get_sorted(std::string str) { sort(str.begin(), str.end()); erase_if(str, [](const char c) {return isspace(c); }); //str.erase(remove(str.begin(), str.end(), ' '),str.end()); //Remove erase idiom return str; } bool is_vowel(char c) { using namespace std::literals; return "aeiouAEIOU"s.contains(c); } size_t count_vowel(const std::string& str) { return count_if(str.begin(), str.end(), is_vowel); } int main() { /* # OUTPUT # Enter a string : Ahmet Kandemir Pehlivanli Ulya Yuruk A 1 K 1 P 1 U 1 Y 1 a 3 d 1 e 3 h 2 i 3 k 1 l 3 m 2 n 2 r 2 t 1 u 2 v 1 y 1 sorted string : "AKPUYaaadeeehhiiiklllmmnnrrtuuvy" total vowels : 13 */ std::string sline; std::cout << "Enter a string : "; getline(std::cin, sline); auto hist = std::async(histogram, sline); auto sorted_string = std::async(get_sorted, sline); auto vowel_cnt = std::async(count_vowel, sline); for (const auto& [c, count] : hist.get()) { std::cout << c << ' ' << count << '\n'; } std::cout << "sorted string : " << quoted(sorted_string.get()) << '\n' << "total vowels : " << vowel_cnt.get() << '\n'; } * Örnek 5, Hata gönderilmesi durumunda yine işi kendi halletmektedir. #include #include #include #include #include double square_root(double x) { if (x < 0.0) { throw std::domain_error("negative value to square root function"); } return std::sqrt(x); } int main() { /* # OUTPUT # 1.51658 1.84391 1.04881 error */ std::vector dvec{ 2.3, 3.4, 1.1, -1.9 }; std::vector> results; for (auto x : dvec) { results.push_back(std::async(square_root, x)); } for (auto& x : results) { try { std::cout << x.get() << '\n'; } catch (const std::domain_error&) { std::cout << "error\n"; } } } >>> "std::packaged_task" : Tıpkı "std::async" fonksiyonu gibi yüksek seviyeli bir başka aracımız daha vardır; "std::packaged_task" sınıfı. Bu sınıf türünden bir nesne, çoğunlukla asenkron bir çağrı yapmak için, bir "callable" nesneyi sarmalamaktadır. Bizler "std::async" ile doğrudan bir fonksiyonu çağırırken, "std::packaged_task" ise bir "Functor" görevi görmektedir. Geri dönüş değeri de yine "std::future" nesnesidir. "std::packaged_task" bünyesinde bir ".operator()()" barındırır, bu da sarmaladığı "callable" nesneyi çağırmaktadır. Yine bu sınıf da "move only" bir sınıftır. * Örnek 1, // synchronous usage of std::package_task #include #include #include int sum(int a, int b) { return std::pow(a, b) + std::pow(b, a); } int main() { { // Using Lambda std::packaged_task ptask( [](double a, double b){ return std::pow(a, b) + std::pow(b, a); } // returns "double" ); //std::future result = ptask.get_future(); auto result = ptask.get_future(); ptask(1.2, 3.4); // İşte şimdi kodlar çalışmaya başladı. std::cout << "result : " << result.get() << '\n'; // result : 6.20158 } std::cout << '\n'; { // Using a Function std::packaged_task ptask{ sum }; auto result = ptask.get_future(); ptask(2, 4); // İşte şimdi kodlar çalışmaya başladı. std::cout << "result : " << result.get() << '\n'; // result : 32 } } * Örnek 2, Yine "thread" lerle birlikte de kullanabiliriz. #include #include #include int fib(int n) { return (n < 3) ? 1 : fib(n - 1) + fib(n - 2); } int main() { std::packaged_task fib_task(&fib); auto result = fib_task.get_future(); std::thread th(std::move(fib_task), 40); std::cout << "task'in bitmesi bekleniyor...\n"; std::cout << result.get() << '\n'; std::cout << "task tamamlandi\n"; th.join(); } * Örnek 3, "Default Ctor." edilmiş bir "std::packaged_task" nesnesini argümanlarla kullanmaya kalkarsak hata nesnesi gönderilir. #include #include int main() { using ftype = int(int, int); std::packaged_task ptask; try { ptask(3, 6); } //catch (const std::future_error& ex) catch (const std::exception& ex) { std::cout << "exception caught: " << ex.what() << '\n'; // exception caught: std::future_error: No associated state } } * Örnek 4, Kopyalamaya karşı kapalı olmasına rağmen taşımaya müsaittir. #include #include #include using ftype = int(int, int); int main() { std::packaged_task pt_x; std::packaged_task pt_y([](int x, int y) {return x * x + y * y; }); // pt_x = pt_y; Syntax error pt_x = std::move(pt_y); std::future ftr = pt_x.get_future(); std::thread{ std::move(pt_x),4,6 }.detach(); std::cout << ftr.get(); } * Örnek 5, Farklı farklı "callable" nesnelerini sarmalayabiliriz. #include #include #include #include int sum_square(int x, int y) { return x * x + y * y; } void task_bind() { std::packaged_task task(std::bind(sum_square, 2, 11)); //std::future result = task.get_future(); auto result = task.get_future(); task(); std::cout << "task_bind\t\t: " << result.get() << '\n'; } void task_thread() { std::packaged_task task(sum_square); //std::future result = task.get_future(); auto result = task.get_future(); std::thread task_td(std::move(task), 2, 10); task_td.join(); std::cout << "task_thread\t\t: " << result.get() << '\n'; } void task_lambda() { std::packaged_task task( [](int a, int b) { return a * a + b * b; } ); //std::future result = task.get_future(); auto result = task.get_future(); task(2, 9); std::cout << "task_lambda\t\t: " << result.get() << '\n'; } int main() { /* # OUTPUT # task_bind : 125 task_thread : 104 task_lambda : 85 */ task_bind(); task_thread(); task_lambda(); } * Örnek 6, "std::future" sınıfındaki ".reset()" fonksiyonunu çağırarak da aynı "std::future" nesnesini tekrar tekrar kullanabiliriz. Bununla birlikte "std::packaged_task" nesnesini de bir fonksiyona göndererek, işlemleri o fonksiyon tarafından yapılmasını sağlatabiliriz. #include #include #include #include #include #include using ipair = std::pair; void func(std::packaged_task& ptask, const std::vector& pairs) { std::osyncstream os{ std::cout }; for (const auto [x, y] : pairs) { auto ftr = ptask.get_future(); ptask(x, y); os << x << " * " << x << " + " << y << " * " << y << " = " << ftr.get() << '\n'; ptask.reset(); // Bu çağrı sayesinde döngünün diğer turunda da kullanabileceğiz. } } int main() { /* # OUTPUT # 1 * 1 + 3 * 3 = 10 3 * 3 + 5 * 5 = 34 7 * 7 + 9 * 9 = 130 11 * 11 + 13 * 13 = 290 15 * 15 + 17 * 17 = 514 */ std::vector pvec; pvec.emplace_back(1, 3); pvec.emplace_back(3, 5); pvec.emplace_back(7, 9); pvec.emplace_back(11, 13); pvec.emplace_back(15, 17); std::packaged_task pt{ [](int x, int y) { return x * x + y * y; } }; std::jthread t(func, std::ref(pt), pvec); } * Örnek 7, Pekala "std::packaged_task" nesnelerini de bir "container" içerisinde tutabiliriz. #include #include #include #include #include class Summer { public: auto operator()(int from, int to) const { int sum{}; for (int i = from; i < to; ++i) sum += i; return sum; } }; int main() { using ftype = int(int, int); // Functor nesnelerimiz: Summer sum1, sum2, sum3, sum4; // Her bir Functor nesnemizi sarmalayan "std::packaged_task" nesnemiz: std::packaged_task pt1(sum1), pt2(sum2), pt3(sum3), pt4(sum4); // Her bir "std::packaged_task" için "std::future" nesnemiz: auto ft1(pt1.get_future()), ft2(pt2.get_future()), ft3(pt3.get_future()), ft4(pt4.get_future()); // "std::packaged_task" nesnelerinin tutulacağı kap: std::deque> td; td.push_back(std::move(pt1)); td.push_back(std::move(pt2)); td.push_back(std::move(pt3)); td.push_back(std::move(pt4)); int begin{ 1 }; int increment{ 5 }; int end = begin + increment; while (!td.empty()) { // Kabın en başındaki "std::packaged_task" nesnesini temin ediyoruz; auto task = std::move(td.front()); // Daha sonra onu kaptan çıkartıyor, kabı boşaltıyoruz. td.pop_front(); // Temin ettiğimiz "std::packaged_task" nesnesine ilişkin kodları koşturuyoruz; // Döngünün ilk turunda [1-6) arasındakiler toplanacaktır: begin: 1, end: 6 std::thread t(std::move(task), begin, end); // Daha sonra; begin = end; // begin: 6, end: 6 end += increment; // begin: 6, end: 11 // Döngünün ikinci turunda 6-11 arasındakiler toplanacaktır. // Döngünün üçüncü turunda 11-16 arasındakiler toplanacaktır. // Döngünün son turunda 16-21 arasındakiler toplanacaktır. // Böylelikle [1-21) arasındakileri toplamış oluyoruz. // "thread" nesnemizi "joinable" duruma getiriyoruz. t.detach(); } auto sum = ft1.get() + ft2.get() + ft3.get() + ft4.get(); std::cout << "result = " << sum << '\n'; } >>> "Conditional Variables" : Birden fazla "thread" çalıştığı zaman "thread" lerden birisinin işini yapmaya devam etmesi için diğer "thread" lerin bir değer üretmesi durumunda kullanılan bir kavramdır. Öyle bir kavramdır ki o değerin üretildiği bilgisini dinler ve bilgi geldiğinde de bu değeri bekleyen diğer "thread" lere bilgi geçer. "condition_variable" isimli başlık dosyasında bildirilmiştir bu nesnemiz. * Örnek 1.0, "pooling" yöntemi ile "Conditional Variables" kullanmadan iki "thread" arasında haberleşme sağlanabilir. #include #include #include #include int shared_variable{}; std::mutex mtx; using namespace std::literals; void producer() { std::lock_guard lg{ mtx }; // ... production code std::this_thread::sleep_for(1000ms); shared_variable = 999; } void consumer() { std::unique_lock ulock{ mtx }; // Burada "std::unique_lock" kullanmak zorundayız // çünkü diğer "lock" sınıflarını ".lock()" ve // ".unlock()" fonksiyonlarını çağırmamıza izin // VERMEMEKTEDİR. while (shared_variable == 0) { // Programın akışı bu kısma girmişse, // beklenen değer üretilmemiş demektir. // Dolayısıyla kilidi açıyor ve varsa // yapacak başka işler yapıyoruz. ulock.unlock(); std::this_thread::yield(); std::this_thread::sleep_for(1000ms); // Daha sonra tekrar kilitliyoruz. // Böylelikle "while" döngüsünden çıkılırsa, // ilgili "mutex" nesnesi kilitli olacak ve // devamında yapacağımız işlemler senkronize // edilmiş olacaktır. ulock.lock(); } // Programın akışı buraya girmişse beklenen değer // üretilmiş demektir. İlgili "mutex" nesnesinin // kilidi hala bizde olmalı ki senkronize bir şekilde // işlemleri gerçekleştirelim. std::cout << "the value is : " << shared_variable << '\n'; } int main() { // the value is : 999 std::jthread t1{ producer }; std::jthread t2{ consumer }; } * Örnek 1.1, "pooling" yöntemi ile "Conditional Variables" kullanmadan birden fazla "thread" arasında haberleşme sağlanabilir. #include #include #include #include #include // #include using namespace std; using namespace literals; // "thread" ler tarafından ortak kullanılacak nesnemiz. string shared_data{}; // Anlık veri alımı gerçekleştiğinde kullanılacak: bool update_flag{ false }; // Veri alımı tamamlandığında kullanılacak: bool completed_flag{ false }; mutex data_mutex; mutex completed_mutex; void receive_data() { for (int i = 0; i < 4; ++i) { cout << "<<< receive_data_thread is waiting for data... >>>\n"; // Veri alındığını belirtmesi için ilgili "thread" uyutuldu. this_thread::sleep_for(1s); // Daha sonra alınan verinin ortak değişkene aktarılması süreci; scoped_lock shared_data_lock(data_mutex); shared_data += std::string{"[chunk_"} + std::to_string(i) + std::string{"]"}; cout << shared_data << '\n'; update_flag = true; // Anlık veri alımının gerçekleştiğini belirttik. } cout << "<<< receiving_data_operation has ended >>>\n"; scoped_lock completed_lock(completed_mutex); completed_flag = true; // Veri alımının tamamlandığını belirttik. } void display_progress() { while (true) { cout << "<<< display_progress_thread waiting for data... >>>\n"; unique_lock shared_data_lock(data_mutex); // "unique_lock" is must here. while (!update_flag) { // Anlık veri alımı boyunca bizler uyku modunda olacağız. shared_data_lock.unlock(); this_thread::sleep_for(20ms); shared_data_lock.lock(); } // Akışın buraya gelmesi demek anlık veri alımının tamamlandığının // göstergesidir. Dolayısıyla aşağıdaki atamaları yapıyoruz ki tekrardan // anlık veri akışı başlasın. Aynı zamanda ilgili "mutex" nesnesinin // kilidi hala bizdedir. update_flag = false; cout << "<<< received [" << shared_data.length() << "] bytes so far >>>\n"; shared_data_lock.unlock(); // Veri alımının sonlanıp sonlanmadığının kontrolünü yapıyoruz: lock_guard completed_lock(completed_mutex); if (completed_flag) { cout << "<<< display_progress_thread has ended >>>\n"; break; } } } void process_data() { cout << "<<< process_data_thread waiting for data... >>>\n"; unique_lock completed_lock(completed_mutex); while (!completed_flag) { // Veri alımı tamamlanmadıysa, bizler uyku modunda olacağız. completed_lock.unlock(); this_thread::sleep_for(10ms); completed_lock.lock(); } // Akış buraya gelmişse veri alımının tamamlandığının // göstergesidir. completed_lock.unlock(); lock_guard shared_data_lock(data_mutex); cout << "<<< display_progress_thread ended. shared_data: [" << shared_data << "] >>>\n"; // ... } int main() { /* # OUTPUT # <<< receive_data_thread is waiting for data... >>> <<< process_data_thread waiting for data... >>> <<< display_progress_thread waiting for data... >>> [chunk_0] <<< receive_data_thread is waiting for data... >>> <<< received [9] bytes so far >>> <<< display_progress_thread waiting for data... >>> [chunk_0][chunk_1] <<< receive_data_thread is waiting for data... >>> <<< received [18] bytes so far >>> <<< display_progress_thread waiting for data... >>> [chunk_0][chunk_1][chunk_2] <<< receive_data_thread is waiting for data... >>> <<< received [27] bytes so far >>> <<< display_progress_thread waiting for data... >>> [chunk_0][chunk_1][chunk_2][chunk_3] <<< receiving_data_operation has ended >>> <<< display_progress_thread ended. shared_data: [[chunk_0][chunk_1][chunk_2][chunk_3]] >>> <<< received [36] bytes so far >>> <<< display_progress_thread has ended >>> */ jthread receiver(receive_data); // Veri alımından sorumlu jthread progress(display_progress); // Alınan verinin toplam veriye oranını ekrana yazdıcarak. jthread processor(process_data); // Veri alımı tamamlandığında, veri üzerinde işlem yapacak. } * Örnek 2.0.0, "Conditional Variable" kullanarak iki "thread" arasında haberleşme sağlanabilir. #include #include #include #include int gdata; bool ready_flag{}; std::mutex mtx; std::condition_variable cv; void producer() { { std::lock_guard lg{ mtx }; gdata = 2345; ready_flag = true; } // Aşağıdaki çağrı ile birlikte değer gelmesini bekleyen "thread" // lerden bir tanesine bilgi gönderiyoruz. Yani bir nevi yayın // yapıyoruz. cv.notify_one(); } void consumer() { { std::unique_lock lock{ mtx }; // ".wait()" fonksiyonu şöyle çalışmaktadır; // i. Kilidi ediniyor ve "predicate" olarak gönderilen // argümana bir çağrı yapıyor. Eğer "true" değer // elde ederse, programın akışı oradan yoluna devam // ediyor. Eğer "false" ise, kilidi bırakıyor ve // tekrar uyumaya geçiyor. Ta ki "condition_variable" // tarafından bir bildirim gelene ya da "fake notifying" // oluşana kadar. Bu ikisi oluşursa uyanacak ve tekrar // "predicate" çağrılacak. İş bu ikinci çağrıdan "false" // elde derse, "fake notifying" deyip tekrar uyumaya; // "true" elde ederse, programın akışı oradan yoluna // devam edecek. cv.wait(lock, [] {return ready_flag; }); // Bu noktada şu iki şeyden eminiz; // i. Beklenen veri hazırdır. // ii. Kilidin hayla bizde olduğundan. } std::cout << "gdata : " << gdata; } int main() { // gdata : 2345 std::jthread t1(producer); std::jthread t2(consumer); } * Örnek 2.0.1, #include #include #include #include #include #include #include class IStack { public: IStack() {}; IStack(const IStack&) = delete; IStack& operator=(const IStack&) = delete; int pop() { std::unique_lock lock(mtx); m_cv.wait(lock, [this]() {return !m_vec.empty(); }); int val = m_vec.back(); m_vec.pop_back(); return val; } void push(int x) { std::scoped_lock lock(mtx); m_vec.push_back(x); m_cv.notify_one(); } private: std::vector m_vec; mutable std::mutex mtx; mutable std::condition_variable m_cv; }; constexpr int n{ 1'000 }; IStack gstack; void producer(std::ofstream& ofs) { for (int i = 0; i < n; ++i) { gstack.push(2 * i + 1); std::osyncstream{ ofs} << 2 * i + 1 << " pushed\n"; } } void consumer(std::ofstream& ofs) { for (int i = 0; i < n; ++i) { std::osyncstream{ ofs } << gstack.pop() << " popped\n"; } } int main() { /* # log.txt # 1 pushed 3 pushed 5 pushed 7 pushed 9 pushed 1 popped //... 49 popped 51 popped 51 pushed //... 553 pushed 555 popped 555 pushed //... 609 popped 611 pushed 611 popped //... 1227 pushed 1227 popped 1229 pushed 1229 popped 1231 popped 1231 pushed //... 1347 pushed 1341 popped 1349 popped //... 1373 pushed 1373 popped 1375 pushed 1375 popped //... 1991 popped 1991 pushed 1993 popped 1993 pushed 1995 popped 1995 pushed 1997 popped 1997 pushed 1999 pushed 1999 popped */ std::ofstream ofs{ "log.txt" }; if (!ofs) { std::cerr << "cannot create log.txt\n"; exit(EXIT_FAILURE); } std::jthread th1(producer, std::ref(ofs)); std::jthread th2(consumer, std::ref(ofs)); } * Örnek 2.0.2, #include #include #include #include #include #include using namespace std; using namespace chrono; string shared_data; mutex mtx; condition_variable cv; bool cflag{ false }; void reader() { cout << "READER thread is locking the mutex\n"; unique_lock ulock(mtx); cout << "READER thread has locked the mutex\n"; cout << "READER thread is going to sleep...\n"; cv.wait(ulock, [] {return cflag; }); cout << "READER thread wakes up\n"; cout << quoted(shared_data) << '\n'; cout << "Reader thread unlocks the mutex\n"; } void writer() { { cout << "WRITER thread is locking the mutex\n"; scoped_lock slock(mtx); cout << "WRITER thread has locked the mutex\n"; this_thread::sleep_for(2s); // Modify the string cout << "WRITER thread modifying data...\n"; shared_data = "shared data is ready now"; cflag = true; cout << "WRITER thread unlocks the mutex\n"; } cout << "WRITER thread is sending a notification\n"; cv.notify_one(); } int main() { /* # OUTPUT # shared_data value is : "not ready yet." WRITER thread is locking the mutex WRITER thread has locked the mutex READER thread is locking the mutex WRITER thread modifying data... WRITER thread unlocks the mutex WRITER thread is sending a notification READER thread has locked the mutex READER thread is going to sleep... READER thread wakes up "shared data is ready now" Reader thread unlocks the mutex */ shared_data = "not ready yet."; cout << "shared_data value is : " << quoted(shared_data) << '\n'; jthread twriter(writer); this_thread::sleep_for(500ms); jthread treader(reader); } * Örnek 2.1, "Conditional Variable" birden fazla "thread" arasında haberleşme sağlanabilir. #include #include #include #include #include #include std::condition_variable cv; std::mutex mtx; int gval = 0; using namespace std::literals; void waits(const std::string& id) { std::unique_lock lk(mtx); std::osyncstream{ std::cout } << id << " is waiting\n"; cv.wait(lk, [] {return gval == 1; }); std::osyncstream{ std::cout } << id << " finished waiting. gval == 1\n"; } void signals(const std::string& id) { std::this_thread::sleep_for(1s); std::osyncstream{ std::cout } << id << " will notify\n"; std::this_thread::sleep_for(std::chrono::seconds(1s)); // İlgili "condition_variable" nesnesini kullanan bütün // "thread" ler uyandırılacaktır. Fakat beklenen değer // henüz hazır olmadığından tekrar uyku moduna geçeceklerdir. cv.notify_all(); { std::lock_guard lk(mtx); gval = 1; std::osyncstream{ std::cout } << id << " is notifying\n"; } // İlgili "condition_variable" nesnesini kullanan bütün // "thread" ler uyandırılacaktır. cv.notify_all(); } int main() { /* # OUTPUT # t2 is waiting t1 is waiting t3 is waiting t5 will notify t4 will notify t5 is notifying t4 is notifying t3 finished waiting. gval == 1 t2 finished waiting. gval == 1 t1 finished waiting. gval == 1 */ std::jthread t1(waits, "t1"), t2(waits, "t2"), t3(waits, "t3"), t4(signals, "t4"), t5(signals, "t5"); } >>> "atomic" : Anımsanacağı üzere paylaşımlı kullanılan bir nesneye en az bir "thread" yazma amacı ile, diğer "thread" ler ise okuma amacıyla erişirse "data racing" oluşur ki bu da bir "Tanımsız Davranış" olur. Dolayısıyla böyle nesneleri ya senkronize etmeli ya hiç paylaşmamalı ya da sadece salt okuma amacı ile erişmemiz gerekir. Çünkü yazma işlemi üç kademeli bir işlemdir; -> İlgili değer bellekten kendi "register" bölgemize "cache" edilir. -> Kendi İlgili değer "modify" edilir. -> Yeni değer tekrar belleğe aktarılır, "publish" edilir. İşte bu üç kademeli işlem sırasında başka "thread" lerin araya girmemesi, "interleave" gerçekleştirmemesi, gerekmektedir. Dolayısıyla bir işlemin "atomic" olması demek o işlem gerçekleşirken araya başkalarının girmeyeceği GARANTİ ALTINDADIR demek. Başlık dosyası "atomic" biçimindedir. "Atomic" türler başlıca şunlardır; "std::atomic_flag" ve "std::atomic" Bunlardan, >>>> "std::atomic_flag" : "primitive" seviyesinde olan bir türdür ve "lock-free" olma garantisini altındadır, yani senkronizasyon için arka planda "mutex" nesnesinin kullanılmadığı KESİNDİR. C++11 ile dile eklenmiştir. C++20 ile arayüzü genişletilmiştir. "true" veya "false" değerlerinden birisini tutabilir. Bünyesindeki ".clear()" ile değerini "false" a; ".test_and_set()" ile değerini "true" ya çekeriz ve eski durumunu geri döndürür. Bu türü hayata getirirken "ATOMIC_FLAG_INIT" makrosunu da kullanabiliriz, "Default Ctor." da çağırabiliriz. C++20'ye kadar "Default Ctor." çağrısı sonucu nesnemiz çöp değer ile hayata gelirken, C++20 ile birlikte "false" ile hayata gelir. Diğer yandan "ATOMIC_FLAG_INIT" kullanımı sonucunda "false" değeri ile hayata gelir. "lock_free" garantisi VERMEKTEDİR. Yani herhangi bir senkronizasyon mekanizması kullanılmadan, "mutex" nesneleri gibi, "concurrency" uygulamalarında kullanılabilirler. Bir diğer deyişle "lock_free" olması demek donanım seviyesinde bir senkronizasyon, olmaması demek arka planda "mutex" benzeri nesnelerin kullanıldığı anlamındadır. * Örnek 1, #include #include int main() { using namespace std; cout << boolalpha; // atomic_flag flag_x{ false }; //gecersiz // atomic_flag flag_y{ true }; //gecersiz atomic_flag flag_z; // C++ 17'de belirsiz deger, C++20'de false değeri cout << "flag_z = " << flag_z.test() << '\n'; // C++20: flag_z = false atomic_flag flag = ATOMIC_FLAG_INIT; // gecerli cout << "flag = " << flag.test() << '\n'; // C++20: flag = false auto b = flag.test_and_set(); cout << "b = " << b << '\n'; // b = false cout << "flag = " << flag.test() << '\n'; // flag = true flag.clear(); cout << "flag = " << flag.test() << '\n'; // flag = false b = flag.test_and_set(); cout << "b = " << b << '\n'; // b = false cout << "flag = " << flag.test() << '\n'; // flag = true } * Örnek 2, #include #include #include #include class SpinLockMutex { public: SpinLockMutex() { m_af.clear(); } void lock() { // İlk baştaki durum "false" olduğundan, bu "lock()" // ilk çağrıldığında "true" olacak ve "while" döngüsünden // çıkacak. Artık bu aşamada yapılan diğer "lock()" // çağrılarında değerimiz zaten "true" olduğundan, ".test_and_set()" // çağrısının geri döndürdüğü değer yine "true" olacak. Dolayısıyla // diğerleri beklemede kalacak. while (m_af.test_and_set()) ; //NULL STATEMENT } void unlock() { // Bu çağrıdan dolayı da değerimiz "false" olacaktır. Artık ".lock()" // çağrısında bekleyen diğer "thread" ler kilitleyebilir. m_af.clear(); } private: std::atomic_flag m_af; // true if thread holds mutex }; SpinLockMutex sm; unsigned long long gcount{}; void worker() { for (unsigned long long i = 0; i < 100'000ULL; ++i) { std::scoped_lock lock(sm); ++gcount; } } int main() { { std::jthread th1(worker); std::jthread th2(worker); } std::cout << "gcount = " << gcount << '\n'; } >>>> "std::atomic" : Bir sınıf şablonudur. Bu sınıfın, -> "bool" ve "User Defined Type" açılımı "Primary Template" dir. Sadece "User Defined Type" türlerin bazı şartları sunması gerekmektedir. -> Tam sayı türlerinin her biri ayrı birer "Explicit/Full Specialization". -> Gösterici türleri için "Partial Specialization". * Örnek 1.0, Bu sınıf türü ile yapılan bütün işlemlerin "atomic" işlemler olduğu garanti DEĞİLDİR. #include #include #include int main() { using namespace std; int x = 0; auto f = [&x]{ for (int i = 0; i < 10'000; ++i) { ++x; } }; { std::jthread j1{ f }; std::jthread j2{ f }; std::jthread j3{ f }; std::jthread j4{ f }; } std::cout << x << '\n'; // 35213 } * Örnek 1.1, #include #include #include int main() { using namespace std; atomic x = 0; auto f = [&x]{ for (int i = 0; i < 10'000; ++i) { x++; // Atomik bir işlemdir. // ++x; // Atomik bir işlemdir. // x += 1; // Atomik bir işlemdir. // Atomik bir işlem DEĞİLDİR. // Burada ilk önce "int" türüne dönüşüm, // sonrasında toplama, // sonunda da atama işlemi gerçekleşecektir. // x = x + 1; } }; { std::jthread j1{ f }; std::jthread j2{ f }; std::jthread j3{ f }; std::jthread j4{ f }; } std::cout << x << '\n'; // 40000 } * Örnek 1.2, #include #include #include #include using namespace std; atomic x = 5; void bar() { for (int i = 0; i < 10'000; ++i) { // The operation is "non-atomic". x.exchange(x + 1); // x : 25427 } } int main() { { std::vector vec; for (int i = 0; i < 10; ++i) { vec.emplace_back(bar); } } std::cout << "x : " << x << '\n'; } * Örnek 2, Yine bu sınıf "non-assignable" ve "non-copyable" dır. Sadece "primitive" türlere ve/veya "primitive" türlerden atama/kopyalama yapılabilir. #include #include int main() { using namespace std; cout << boolalpha; atomic flag_1; atomic flag_2; //indetermined value before before C++20. false value since C++20 cout << flag_1 << '\n'; // false cout << flag_2 << '\n'; // false //atomic flag_3{flag_2}; //invalid //flag_1 = flag_2; //invalid flag_1 = true; // ".store()" fonksiyonunun aldığı argüman, yeni değeri olacak. // flag_1.store(false); cout << "flag_1 = " << flag_1 << '\n'; // operator T: flag_1 = true flag_2 = false; // flag_2.store(true); cout << "flag_2 = " << flag_2 << '\n'; // operator T: flag_2 = false // ".exchange()" fonksiyonunun geri dönüş değeri eski değeri, // aldığı argüman ise yeni değeri olacak. auto b = flag_1.exchange(true); cout << "b = " << b << '\n'; // b = true cout << "flag_1 = " << flag_1 << '\n'; // operator T: flag_1 = true // ".load()" fonksiyonu ise değeri "get" etmektedir. cout << "flag_1.load() = " << flag_1.load() << '\n'; // flag_1.load() = true cout << "flag_2.load() = " << flag_2.load() << '\n'; // flag_2.load() = false } * Örnek 3, Şimdi de kapsayıcı bir örnek yapalım; #include #include #include class AtomicCounter { public: AtomicCounter() : m_c(0) {} AtomicCounter(int val) : m_c{ val } {} // Operatör fonksiyonları artık referans döndürmemektedir, // "atomic" türlerde. int operator++() { return ++m_c; } int operator++(int) { return m_c++; } int operator--() { return --m_c; } int operator--(int) { return m_c--; } int get() const { return m_c.load(); } operator int()const { return m_c.load(); } private: std::atomic m_c; }; AtomicCounter cnt; void foo() { for (int i = 0; i < 1'000'000; ++i) { ++cnt; } } int main() { { std::jthread ta[10]; for (auto& th : ta) th = std::jthread{ foo }; } std::cout << "cnt = " << cnt.get() << '\n'; // cnt = 10000000 std::cout << "cnt = " << cnt << '\n'; // cnt = 10000000 } * Örnek 4.0, Sınıfın ".lock_free()" fonksiyonu, "lock_free" sorgulamasını çalışma zamanında yapar; Sınıfın statik veri elemanı "is_always_lock_free" ise derleme zamanında "lock_free" sorgulaması yapar. #include #include #include struct Neco {}; int main() { /* # OUTPUT # a1.lock_free() : true a2.lock_free() : true a3.lock_free() : false a4.lock_free() : true a5.lock_free() : true */ using namespace std; boolalpha(cout); atomic a1; cout << "a1.lock_free() : " << a1.is_lock_free() << '\n'; atomic a2; cout << "a2.lock_free() : " << a2.is_lock_free() << '\n'; atomic> a3; //C++20 cout << "a3.lock_free() : " << a3.is_lock_free() << '\n'; atomic a4; cout << "a4.lock_free() : " << a4.is_lock_free() << '\n'; atomic a5; cout << "a5.lock_free() : " << a5.is_lock_free() << '\n'; } * Örnek 4.1, #include #include #include struct Nec_4 { short x, y, z, t; }; struct Nec_8 { short x, y, a, b, c, d, e, f; }; int main() { using namespace std; constexpr auto b1 = atomic::is_always_lock_free; // true constexpr auto b2 = atomic::is_always_lock_free; // true constexpr auto b3 = atomic::is_always_lock_free; // true constexpr auto b4 = atomic::is_always_lock_free; // false } * Örnek 5, İsminde "fatch" geçen fonksiyonlar hem "atomic" hem de bu fonksiyonların parametrelerine "Memory Order" argümanı geçebiliriz. Bu fonksiyonlar "+=" gibi operatör fonksiyonlarının "Memory Order" argümanı alan versiyonlarıdır. Fakat iş bu "fatch" fonksiyonlarının geri dönüş değeri, eski değerdir. #include #include int main() { using namespace std; atomic x = 5; atomic y = 5; auto r1 = x += 2; auto r2 = y.fetch_add(2); cout << "x = " << x << '\n'; //7 cout << "y = " << y << '\n'; //7 cout << "r1 = " << r1 << '\n'; //7 cout << "r2 = " << r2 << '\n'; //5 } * Örnek 6.0, "Compare-Exchange-Swap" fonksiyonunun işlevi "multi-thread" çalışma esnasında yapılan bazı işlemler arasında, bizim "atomic" değişkenimizin değerinin değişmesi söz konusudur ve programımızın lojik yapısı gereği bu değişikliği yakalamamız gerekmektedir. Yani herhangi iki "atomic" işlem arasında "atomic" değerimizin değeri değiştiğinde, bu değişikliği yakalamamız gerekiyor. İşte tipik olarak döngüsel bir yapıda bu fonksiyonu çağırarak işimizi görebiliriz. "Compare-Exchange-Swap" fonksiyonu, temelde aşağıdaki gibi işlev görür: // from Fedor Picus CppCon talk //CAS compare-exchange-swap conceptually bool compare_exchange_swap_strong(T &expected, T desired) { // Assuma that; // expected is 5 // desired is 95 //Lock is not a real mutex but // some form of exclusive access implemented in hardware Lock lock; //get exclusive access //"value" is the value of atomic "this->value" T temp = value; // Assume that it is "5". if (temp != expected) { expected = temp; return false; } value = desired; return true; } * Örnek 6.1.0, #include #include #include int main() { /* # OUTPUT # b = true x = 95 expected = 5 desired = 95 */ std::atomic x; x.store(5); // "this->value" is "5". int expected{ 5 }; int desired{ 95 }; auto b = x.compare_exchange_strong(expected, desired); // ^ ^ ^ ^ // | | | | New value of "this->value". // | | | Old value of "this->value" must be equal to the this value so that its value will be equal to the "desired". // | | Old value of "this->value" // | The result std::cout << std::boolalpha; std::cout << "b = " << b << '\n'; std::cout << "x = " << x << '\n'; std::cout << "expected = " << expected << '\n'; std::cout << "desired = " << desired << '\n'; } * Örnek 6.1.1, #include #include #include int main() { /* # OUTPUT # b = false x = 5 expected = 5 desired = 95 */ std::atomic x; x.store(5); // "this->value" is "5". int expected{ 7 }; int desired{ 95 }; auto b = x.compare_exchange_strong(expected, desired); std::cout << std::boolalpha; std::cout << "b = " << b << '\n'; std::cout << "x = " << x << '\n'; std::cout << "expected = " << expected << '\n'; std::cout << "desired = " << desired << '\n'; } * Örnek 6.2.0, #include #include template void atomic_inc(std::atomic&x) { T val{ x }; // "val" in değeri "50" olsun. // <---- // Bu aşamada başka bir "thread" ile "interleave" oluşmadığı // varsayılırsa; // "expired" isimli parametrenin değeri "50" olduğundan, // ".compare_exchange_weak()" fonksiyonu "true" döndürecek. // "val" değişkeninin değeri artık "51" olacak. while (!x.compare_exchange_weak(val, val + 1)) { } } * Örnek 6.2.1, #include #include template void atomic_inc(std::atomic&x) { T val{ x }; // "val" in değeri "50" olsun. // <---- // Bu araya başka bir "thread" in girdiği varsayılırsa; // "expired" isimli parametrenin değeri "20" olduğundan, // ".compare_exchange_weak()" fonksiyonu "false" döndürecek. // Döngüye girecek. "val" değişkeninin değeri artık "20" olacak. // Döngünün bir sonraki turunda eğer araya başka "thread" girmezse, // "* Örnek 6.2.0" numaralı örnekteki senaryo gerçekleşecek. while (!x.compare_exchange_weak(val, val + 1)) { } } * Örnek 6.3, "atomic multiply" işlemi: #include #include template T fetch_multiply(std::atomic&a, T factor) { T old_value = a.load(); while (!a.compare_exchange_strong(old_value, old_value * factor)) ; return old_value; } int main() { using namespace std; atomic ax(6); cout << "ax = " << ax << '\n'; auto pval = fetch_multiply(ax, 9); cout << "ax = " << ax << '\n'; cout << "pval = " << pval << '\n'; } * Örnek 7.0, "ABA Problem" : The scenario consists of you sitting in a car and waiting that the traffic light becomes green. Green stands in our case for B, and red for A. What’s happening? -> You look at the traffic light and it is red (A). -> Because you are bored, you begin to check the news on your smartphone and forget the time. -> You look once more at the traffic light. Damn, is still red (A). Of course, the traffic light became green (B) between your two checks. Therefore, what seems to be one red phase was two. What does this mean for threads (processes)? Now once more formal. -> Thread 1 reads a variable var with value A. -> Thread 1 is preempted, and thread 2 runs. -> Thread 2 changes the variable var from A to B to A. -> Thread 1 starts to execute and checks the value of variable var; because the value of variable var is the same, thread 1 continues with its work, * Örnek 7.1, #include #include #include #include #include #include std::atomic is_ready{ false }; //CTAD std::atomic is_done{ false }; std::atomic go_count{ 0 }; void lottery(std::string name) { ++go_count; while (!is_ready) ; for (volatile int i = 0; i < 20000; ++i) { } bool expected{ false }; if (is_done.compare_exchange_strong(expected, true)) { std::cout << "kazanan: " << name << '\n'; } } int main() { using namespace std; const char* pnames[] = { "necati", "akif", "ahmet", "harun", "mehmet", "dogukan" }; mt19937 eng{ random_device{}() }; std::shuffle(begin(pnames), end(pnames), eng); vector tvec; for (auto p : pnames) { tvec.emplace_back(lottery, p); } while (go_count != std::size(pnames)) ; is_ready = true; for (auto& t : tvec) { t.join(); } } * Örnek 8, Diğer yandan "std::atomic" sınıfının "primitive" tür açılımları için tür eş isimleri de dile eklenmiştir. #include #include #include int main() { using namespace std; constexpr auto b1 = same_as, atomic_int>; constexpr auto b2 = same_as, atomic_char>; constexpr auto b3 = same_as, atomic_long>; constexpr auto result = b1 && b2 && b3; //true } * Örnek 9, C diline eklenen "atomic" ile uyum sağlanması için bazı global fonksiyonlar da mevcuttur. #include #include int main() { using namespace std; atomic x{}; x.store(98); cout << x.load() << '\n'; // 98 atomic_store(&x, 30); cout << atomic_load(&x) << '\n'; // 30 // } * Örnek 10, "volatile" is nothing to do with "atomic". Yani bir değişkeni "volatile" olarak nitelememiz, o işlemin "interleave" OLUŞTURACAĞINI GARANTİ ETMEZ. // volatile is not atomic #include #include #include // volatile int x = 0; // -----> (1) // std::atomic x = 0; // -----> (2) int main() { using namespace std; const auto fn_inc = [] { for (int i = 0; i < 100'000; ++i) ++x; }; const auto fn_dec = [] { for (int i = 0; i < 100'000; ++i) --x; }; { jthread t1{ fn_inc }; jthread t2{ fn_dec }; } // -----> (1) : x = -20619 // -----> (2) : x = 0 cout << "x = " << x << '\n'; } * Örnek 11.0, Pekiyi yukarıdaki örnekler bahsi geçen "Memory Order" konusu nedir? "Memory Order" C++20'ye kadar bir "unscoped enum" yapıdır. C++20 ile birlikte "scoped enum" hale getirildi ve bu türden yeni "inline" değişkenler tanımlandı. (For more info, see https://en.cppreference.com/w/cpp/atomic/memory_order) Aşağıdaki örnekte "assert" "fail" olmayacaktır. #include #include #include #include std::atomic_bool x_flag, y_flag; std::atomic ival; void set_x() { x_flag.store(true); } void set_y() { y_flag.store(true); } void read_x_then_y() { while (!x_flag.load()) ; if (y_flag.load()) ++ival; } void read_y_then_x() { while (!y_flag.load()) ; if (x_flag.load()) ++ival; } void func() { x_flag = false; y_flag = false; ival = 0; { std::jthread t1{ set_x }; std::jthread t2{ set_y }; std::jthread t3{ read_x_then_y}; std::jthread t4{ read_y_then_x}; } assert(ival != 0); } int main() { for (int i = 0; i < 10'000'000; ++i) { func(); } } * Örnek 11.1, #include #include #include #include std::atomic ptr; int data; void producer() { std::string* p = new std::string("Hello"); data = 42; ptr.store(p, std::memory_order_release); } void consumer() { std::string* p2; while (!(p2 = ptr.load(std::memory_order_acquire))) ; assert(*p2 == "Hello"); // never fires assert(data == 42); // never fires } int main() { std::thread t1(producer); std::thread t2(consumer); t1.join(); t2.join(); } * Örnek 11.2, #include #include #include #include std::atomic_bool flag1 = false; std::atomic_bool flag2 = false; std::string name{}; void foo() { name = "necati"; flag1.store(true, std::memory_order_release); // Derleyici optimizasyon yaparken, bu çağrının üstünde kalan kodları, bu çağrının altına kaydıramaz. } void bar() { while (!flag1.load(std::memory_order_acquire)) // Derleyici optimizasyon yaparken, bu çağrının altında kalan kodları, bu çağrının üzerine kaydıramaz. ; name += " ergin"; flag2.store(true, std::memory_order_release); } void baz() { while (!flag2.load(std::memory_order_acquire)) // Derleyici optimizasyon yaparken, bu çağrının altında kalan kodları, bu çağrının üzerine kaydıramaz. ; name += " can"; } void test() { flag1 = false; flag2 = false; std::thread t1{ foo }; std::thread t2{ bar }; std::thread t3{ baz }; t1.join(); t2.join(); t3.join(); assert(name == "necati ergin can"); } int main() { for (int i = 0; i < 1000; ++i) { test(); } } * Örnek 11.3, #include #include #include #include using namespace std; std::atomic x, y, z; void func_1() { x.store(true, std::memory_order_relaxed); } void func_2() { y.store(true, std::memory_order_relaxed); } void func_3() { while (!y.load(std::memory_order_relaxed)) ; if (x.load(std::memory_order_relaxed)) { z = true; } assert(z); // It fails. } int main() { for (int i = 0; i < 100000; ++i) { x = false; y = false; z = false; std::jthread thread_1(func_1); std::jthread thread_2(func_2); std::jthread thread_3(func_3); } } * Örnek 11.4, #include #include #include std::atomic g_count = 0; void foo() { for (int i = 0; i < 10'000; ++i) g_count.fetch_add(1, std::memory_order_relaxed); } void bar() { for (int i = 0; i < 10'000; ++i) g_count.fetch_add(1, std::memory_order_relaxed); } void baz() { for (int i = 0; i < 10'000; ++i) g_count.fetch_add(1, std::memory_order_relaxed); } int main() { { std::jthread t1{ foo }; std::jthread t2{ bar }; std::jthread t3{ baz }; } std::cout << "g_count: " << g_count << '\n'; // g_count: 30000 } * Örnek 11.5, #include #include #include #include #include #include class SpinLockMutex { public: SpinLockMutex() { m_f.clear(); } void lock() { while (m_f.test_and_set(std::memory_order::acquire)) ; //null statement } void unlock() { m_f.clear(std::memory_order::release); } private: std::atomic_flag m_f; }; SpinLockMutex mtx; unsigned long long counter{}; void func() { for (int i{ 0 }; i < 100'000; ++i) { mtx.lock(); ++counter; mtx.unlock(); } } int main() { std::vector tvec; for (int i = 0; i < 10; ++i) { tvec.emplace_back(func); } for (auto &th : tvec) { th.join(); } std::cout << "counter = " << counter << "\n"; } * Örnek 11.6, #include #include #include #include std::atomic data{45}; void do_work(int id) { std::osyncstream{ std::cout } << "thread id : " << id << " " << data.fetch_add(1, std::memory_order_relaxed) << '\n'; } int main() { std::jthread jt1{ do_work, 0 }; std::jthread jt2{ do_work, 1 }; std::jthread jt3{ do_work, 2 }; std::jthread jt4{ do_work, 3 }; std::jthread jt5{ do_work, 4 }; std::jthread jt6{ do_work, 5 }; std::jthread jt7{ do_work, 6 }; std::jthread jt8{ do_work, 7 }; } Tabii diğer yandan şöyle de bir gelişme yaşanmıştır; Modern C++ ile birlikte "static" ömürlü yerel değişkenlerin "initialization" süreci "thread-safe" hale gelmiştir. Dolayısıyla böylesi nesneler için herhangi bir senkronizasyon mekanizması kullanmaya lüzum yoktur. * Örnek 1, #include #include #include class Myclass { public: Myclass() { std::cout << "Ctor. @" << this << '\n'; } ~Myclass() { std::cout << "Dtor. @" << this << '\n'; } }; void foo() { static Myclass m; } int main() { // Ctor. @0x561ef08b6199 // Dtor. @0x561ef08b6199 std::vector jvec; for (int i = 0; i < 100; ++i) jvec.emplace_back(foo); } * Örnek 2, İşte bu garantiye güvenerek bir fonksiyonun sadece bir kez çağrılmasını sağlatabiliriz. #include #include #include void foo() { std::cout << "Once!\n"; } void foo_call() { static auto f = [](){ foo(); return 0; }(); } int main() { std::vector tvec; for (int i{}; i < 100; ++i) { tvec.emplace_back(foo_call); // Once! } } * Örnek 3, Fakat bu yönteme alternatif olarak "std::once_flag" sınıfını ve "std::call_once" fonksiyonunu kullanabiliriz. Bunlar sırasıyla bir sınıf ve fonksiyon şablonudur ve birlikte kullanıldığında bir fonksiyonun sadece bir defa çağrılmasını sağlarlar. Bu iki kavram da "std::mutex" başlık dosyasındadır. #include #include #include #include #include std::unique_ptr uptr; std::once_flag init_flag; void f_init() { uptr = std::make_unique(656); } const int& get_value() { std::call_once(init_flag, f_init); // Birinci parametresi "once_flag" türünden nesnemiz, // ikinci parametresi çağrılacak fonksiyon, // diğer parametreler ise iş bu fonksiyonun alacağı argümanlar. return *uptr; } void do_work() { const int& v = get_value(); assert(v == 656); // Holds } int main() { std::vector tvec; tvec.reserve(16); for (int i{}; i < 16; ++i) { tvec.emplace_back(do_work); } for (auto& th : tvec) { th.join(); } } * Örnek 4.0, Aşağıdaki örnekte ya bir kez "foo" ya da bir kez "bar" çağrılacak. Çünkü ortak "std::once_flag" türünden nesne kullandığımız için. #include #include #include std::once_flag once_flag; using namespace std::literals; void foo() { std::this_thread::sleep_for(100ms); std::call_once(once_flag, []() { std::cout << "registered in foo\n"}); } void bar() { std::this_thread::sleep_for(100ms); std::call_once(once_flag, []() { std::cout << "register in bar\n"; }); } int main() { std::thread ta[10]; for (int i = 0; i < 10; ++i) { ta[i] = i % 2 ? std::thread{ foo } : std::thread{ bar }; } for (auto& t : ta) t.join(); } * Örnek 4.1, Bir diğer "Singleton" implementasonu. #include #include #include #include #include class Singleton { public: Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; // Non moveable also. static Singleton* get_instance() { call_once(m_init_flag, Singleton::init); return m_instance; } static void init() { m_instance = new Singleton(); } private: static std::once_flag m_init_flag; static Singleton* m_instance; Singleton() = default; }; Singleton* Singleton::m_instance{}; std::once_flag Singleton::m_init_flag; void func() { std::osyncstream{ std::cout } << Singleton::get_instance() << '\n'; } int main() { std::vector tvec; for (int i = 0; i < 100; ++i) { tvec.emplace_back(func); } for (auto& th : tvec) th.join(); } >> "std::syncstream" sınıfı gelmeden evvel "std::cout" nesnesini senkronize ederken aşağıdaki gibi bir yöntem kullanılmaktaydı; * Örnek 1, #include #include #include #include #include struct pcout : std::stringstream { public: ~pcout() { std::lock_guard locker{ cmutex }; std::cout << rdbuf(); std::cout.flush(); } static inline std::mutex cmutex; }; void my_print(int x) { pcout{} << "print islevi x = " << x << "\n"; } int main() { std::vector tvec(20); for (int i = 0; i < 20; ++i) tvec[i] = std::jthread{ my_print, i }; } >> "std::algorithm" başlık dosyasındaki bazı fonksiyonlara eklenen "overload" versiyonlar: Bu "overload" versiyonları sayesinde ilgili fonksiyonları; -> Sıralı biçimde: "std::execution::sequenced_policy" sınıfı kullanılır. C++17 ile dile eklenmiştir. İlave bir "thread" kullanmadan, sıralı biçimde çalıştırır. Kaldı ki halihazırdaki diğer "overload" versiyonlar da sıralı biçimde çalıştırmaktadırlar ancak gönderilen hata nesnelerini yakalayabiliyoruz. Fakat "std::execution::sequenced_policy" sınıfı kullanılarak çalıştırılanlar hata nesnesi gönderildiğinde doğrudan "std::terminate" fonksiyonu çağrılmaktadır. İki versiyon arasındaki en büyük farklılık budur. -> Vektörize edilmiş biçimde: "std::execution::unsequenced_policy" sınıfı kullanılır. C++20 ile dile eklenmiştir. Ayrı "thread" kullanmadan, sadece vektörize ederek kullan anlamındadır. Çoğu derleyici nezdinde bu "std::execution::unsequenced_policy" sınıfı "std::execution::sequenced_policy" olarak işlenir. -> Paralel biçimde: "std::execution::parallel_policy" sınıfı kullanılır. C++17 ile dile eklenmiştir. İş yükünü "thread" lere dağıtarak halleder. Paylaşılan nesnelerin senkronize edilmesinden BİZ SORUMLUYUZ. Yine bu sınıf kullanıldığında arka plandaki "thread" lerin öğeleri sırayla ele almaları da kesin değildir. -> Hem Paralel hem Vektörize edilmiş biçimde: "std::execution::parallel_unsequenced_policy" sınıfı kullanılır ve C++17 dile eklenmiştir. Hem vektörize et hem de ayrı "thread" ler kullan anlamındadır. Paylaşılan nesnelerin senkronize edilmesinden BİZ SORUMLUYUZ. Çoğu derleyici nezdinde bu "std::execution::parallel_unsequenced_policy" sınıfı "std::execution::parallel_policy" olarak işlenir. çalıştırabiliriz. Artık yapılacak işi birden fazla "thread" e yükleme vb. kısımları ilgili fonksiyonun kendisi halledecektir. Başlık dosyası "execution" ismindedir. İşte ilgili fonksiyonlara yukarıdaki sınıf türünden nesneler geçerek, o fonksiyonun çalışma biçimini belirleyebiliriz. Yine bu kütüphane bu sınıflar türünden "tag" nesneleri de sunmaktadır. Böylelikle sınıf türünden nesne oluşturmaktansa, kütüphanedeki bu "tag" nesnelerini de kullanabiliriz. Ayrıca bu sınıfların içi boştur, sadece tür belirtmek için kullanılırlar.Diğer yandan yukarıdaki belirtilen "execution_policy" ler birer RİCADIR, EMİR DEĞİLDİR. Tabii şunu da hatırlatmakta fayda vardır; "std::algorithm" başlık dosyasındaki bazı fonksiyonlar için "overload" versiyonu oluşturulamamış, yeni fonksiyonlar eklenmiştir. * Örnek 1.0, Aşağıdaki örnekte bir iş küçük iş parçacıklarına bölünerek bir paralelleştirilme gerçekleştirilmiştir. #include #include #include #include #include #include #include using uint64 = unsigned long long; uint64 accummulate_s(const uint64* first, const uint64* last) { return std::accumulate(first, last, 0ull); } uint64 acc_parrallel(const std::vector& vec) { const auto data = vec.data(); const auto size = vec.size(); // "data" : Dizinin başlangıç noktası. // "data + size / 4" : Dizinin ilk dörtte birlik kısmının bittiği yer. auto ft1 = std::async(std::launch::async, accummulate_s, data, data + size / 4); // "data + size / 4" : Dizinin ilk dörtte birlik kısmının bittiği yer. // "data + 2 * (size / 4)" : Dizinin ikinci dörtte birlik kısmının bittiği yer. auto ft2 = std::async(std::launch::async, accummulate_s, data + size / 4, data + 2 * (size / 4)); // "data + 2 * (size / 4)" : Dizinin ikinci dörtte birlik kısmının bittiği yer. // "data + 3 * (size / 4)" : Dizinin üçüncü dörtte birlik kısmının bittiği yer. auto ft3 = std::async(std::launch::async, accummulate_s, data + 2 * (size / 4), data + 3 * (size / 4)); // "data + 3 * (size / 4)" : Dizinin üçüncü dörtte birlik kısmının bittiği yer. // "data + size" : Dizinin bittiği yer. auto ft4 = std::async(std::launch::async, accummulate_s, data + 3 * (size / 4), data + size); // Dizinin her dörtte birlik kısmının toplanması: return ft1.get() + ft2.get() + ft3.get() + ft4.get(); } int main() { using namespace std; using namespace chrono; vector uvec(50'000'000u); mt19937 eng; uniform_int_distribution dist{ 0ull, 100ull }; generate(uvec.begin(), uvec.end(), [&] {return dist(eng); }); { auto tp_start = steady_clock::now(); auto sum = accumulate(uvec.begin(), uvec.end(), 0ull); auto tp_end = steady_clock::now(); cout << duration(tp_end - tp_start).count() << '\n'; // 0.714942 seconds cout << "sum = " << sum << '\n'; // sum = 2499803776 } std::cout << '\n'; { auto tp_start = steady_clock::now(); auto sum = acc_parrallel(uvec); auto tp_end = steady_clock::now(); cout << duration(tp_end - tp_start).count() << '\n'; // 0.173961 cout << "sum = " << sum << '\n'; // sum = 2499803776 } } * Örnek 1.1, Aşağıda ise "std::packaged_task" lar kullanılmıştır. #include #include #include #include #include #include #include #include using uint64 = unsigned long long; uint64 accummulate_s(const uint64* first, const uint64* last) { return std::accumulate(first, last, 0ull); } uint64 acc_parrallel(const std::vector& vec) { using task_t = uint64(const uint64*, const uint64*); const auto data = vec.data(); const auto size = vec.size(); std::packaged_task task1(accummulate_s); std::packaged_task task2(accummulate_s); std::packaged_task task3(accummulate_s); std::packaged_task task4(accummulate_s); auto ft1 = task1.get_future(); auto ft2 = task2.get_future(); auto ft3 = task3.get_future(); auto ft4 = task4.get_future(); std::jthread t1(std::move(task1), data, data + size / 4); std::jthread t2(std::move(task2), data + size / 4, data + 2 * (size / 4)); std::jthread t3(std::move(task3), data + 2 * (size / 4), data + 3 * (size / 4)); std::jthread t4(std::move(task4), data + 3 * (size / 4), data + size); return ft1.get() + ft2.get() + ft3.get() + ft4.get(); } int main() { using namespace std; using namespace chrono; vector uvec(25'000'000u); mt19937 eng; uniform_int_distribution dist{ 0ull, 100ull }; generate(uvec.begin(), uvec.end(), [&] {return dist(eng); }); { auto tp_start = steady_clock::now(); auto sum = acc_parrallel(uvec); auto tp_end = steady_clock::now(); cout << duration(tp_end - tp_start).count() << '\n'; // 0.0786106 cout << "sum = " << sum << '\n'; // sum = 1249894138 } std::cout << '\n'; { auto tp_start = steady_clock::now(); auto sum = accumulate(uvec.begin(), uvec.end(), 0ull); auto tp_end = steady_clock::now(); cout << duration(tp_end - tp_start).count() << '\n'; // 0.161134 cout << "sum = " << sum << '\n'; // sum = 1249894138 } } * Örnek 2.0, Bu yeni "overload" versiyonlar ile eski "overload" versiyonların arasındaki fark; #include #include #include #include #include #include void my_terminate() { std::cout << "my_terminate\n"; abort(); } int main() { // OUTPUT => exception caught: 8 is not allowed std::set_terminate(my_terminate); std::vector ivec{ 5, 7, 9, 2, 4, 6, 8, 10, 3, 1}; try { for_each(ivec.begin(), ivec.end(), [](int x) { if (x == 8) throw std::runtime_error{ "8 is not allowed" }; }); } catch (const std::exception& ex) { std::cout << "exception caught: " << ex.what() << '\n'; } } * Örnek 2.1, #include #include #include #include #include #include void my_terminate() { std::cout << "my_terminate\n"; abort(); } int main() { // OUTPUT => my_terminate std::set_terminate(my_terminate); std::vector ivec{ 5, 7, 9, 2, 4, 6, 8, 10, 3, 1}; try { for_each(std::execution::seq, ivec.begin(), ivec.end(), [](int x) { if (x == 8) throw std::runtime_error{ "8 is not allowed" }; }); } catch (const std::exception& ex) { std::cout << "exception caught: " << ex.what() << '\n'; } } * Örnek 3.0, "std::execution::parallel_policy" kullanırken ortak nesnenin senkronizasyonundan bizler sorumluyuz. #include #include #include #include #include #include namespace ex = std::execution; int main() { using namespace std; vector ivec(100'000); int cnt{}; for_each(ex::par, ivec.begin(), ivec.end(), [&cnt](int& x) { x = ++cnt; }); cout << "cnt = " << cnt; // 25545 } * Örnek 3.1, "std::atomic" sınıfını kullanarak da senkronizasyon sağlayabiliriz. #include #include #include #include #include #include #include namespace ex = std::execution; int main() { using namespace std; vector ivec(100'000'000); std::atomic cnt{}; for_each(ex::par, ivec.begin(), ivec.end(), [&cnt](int& x) { x = ++cnt; }); cout << "cnt = " << cnt; // 100000 } * Örnek 4, "execution_policy" kullanımları arasındaki süre bazlı fark; #include #include #include #include #include #include using dsec = std::chrono::duration; constexpr std::size_t n{ 20'000'000 }; int main() { std::vector vec(n); std::mt19937 eng{ std::random_device{}() }; std::uniform_real_distribution dist{ -20., 20.}; std::generate(vec.begin(), vec.end(), [&] {return dist(eng); }); auto tp_start = std::chrono::steady_clock::now(); //sort(std::execution::seq, vec.begin(), vec.end()); // 13.9295 saniye //sort(std::execution::unseq, vec.begin(), vec.end()); // 9.78678 saniye //sort(std::execution::par, vec.begin(), vec.end()); // 9.83721 saniye //sort(std::execution::par_unseq, vec.begin(), vec.end()); // 9.92502 saniye auto tp_end = std::chrono::steady_clock::now(); std::cout << dsec{ tp_end - tp_start }.count() << " saniye\n"; } * Örnek 5.0, "std::accumulate" fonksiyonunun "execution_policy" argümanına sahip versiyonu bir "overload" değil, "std::reduce" isimli fonksiyondur. #include #include #include int main() { using namespace std; vector ivec{ 1, 3, 5, 7, 9 }; auto x = reduce(ivec.begin(), ivec.end(), 0); auto y = reduce(ivec.begin(), ivec.end()); auto z = reduce(ivec.begin(), ivec.end(), int{}); cout << "x = " << x << '\n'; // x = 25 cout << "y = " << y << '\n'; // y = 25 cout << "z = " << z << '\n'; // z = 25 } * Örnek 5.1.0, #include #include #include #include #include #include using namespace std; using namespace chrono; namespace ex = std::execution; int main() { vector ivec(100'000'000); mt19937 eng; uniform_int_distribution dist{ 0u, 100u }; generate(ex::par, ivec.begin(), ivec.end(), [&]{ return dist(eng); }); auto tp_start = steady_clock::now(); auto result = reduce( ex::par, ivec.begin(), ivec.end(), 0u, [](auto a, auto b){ return a*a + b*b; } ); auto tp_end = steady_clock::now(); // 1153778770 in 767ms std::cout << result << " in " << duration_cast(tp_end - tp_start).count() << "ms\n"; } * Örnek 5.1.1, #include #include #include #include #include #include using namespace std; using namespace chrono; namespace ex = std::execution; int main() { vector ivec(100'000'000); mt19937 eng; uniform_int_distribution dist{ 0u, 100u }; generate(ex::par, ivec.begin(), ivec.end(), [&]{ return dist(eng); }); auto tp_start = steady_clock::now(); auto result = reduce( ex::seq, ivec.begin(), ivec.end(), 0u, [](auto a, auto b){ return a*a + b*b; } ); auto tp_end = steady_clock::now(); // 1153778770 in 730ms std::cout << result << " in " << duration_cast(tp_end - tp_start).count() << "ms\n"; } * Örnek 6.0.0, "std::inner_product" fonksiyonunun "execution_policy" argümanına sahip versiyonu bir "overload" değil, "std::transform_reduce" isimli fonksiyondur. #include #include #include #include int main() { std::vector x{ 2, 3, 1, 5, 6 }; std::vector y{ 1, 2, 4, 3, 5 }; // 2 6 4 15 30 //auto result = std::inner_product(x.begin(), x.end(), y.begin(), 0); auto result = std::inner_product(x.begin(), x.end(), y.begin(), 0, std::plus<>{}, std::multiplies<>{}); std::cout << "Inner product of x and y: " << result << '\n'; // Inner product of x and y: 57 } * Örnek 6.0.1, #include #include #include #include #include int main() { std::vector x{ 2, 3, 1, 5, 6 }; std::vector y{ 1, 2, 4, 3, 5 }; // 2 6 4 15 30 auto result = std::transform_reduce(std::execution::par, x.begin(), x.end(), y.begin(), 0); std::cout << "Inner product of x and y: " << result << '\n'; // Inner product of x and y: 57 } * Örnek 6.1.0, #include #include #include #include int main() { using namespace std; std::vector v1{ "ali", "can", "ece", "ata", "gul", "tan", "eda", "naz" }; std::vector v2{ "nur", "tan", "ece", "ece", "gul", "tan", "naz", "eda" }; // 1 2 3 int result = std::inner_product(v1.begin(), v1.end(), v2.begin(), 0, plus{}, equal_to{}); cout << "result = " << result << '\n'; // result = 3 } * Örnek 6.1.1, #include #include #include #include int main() { using namespace std; vector target { 0.12, 0.17, 0.25, 0.39, 0.43, 0.70 }; vector source { 0.08, 0.11, 0.23, 0.36, 0.42, 0.74 }; auto max_dev = transform_reduce(execution::par, target.begin(), target.end(), source.begin(), 0.0, [](auto x, auto y) { return max(x, y); }, [](auto trg, auto src) { return std::abs(src - trg); } ); cout << "Max devistion is: " << max_dev << '\n'; // Max devistion is: 0.06 } * Örnek 7, Sonuçtan da görüleceği üzere, paralel çalıştırıldığında öğelerin "thread" lere aktarılması sırayla değildir. Dolayısıyla sadece değerlerlerin işlem sırası farklıdır, değerlerin kendisi aynıdır. //from cppreference.com #include #include #include #include #include #include std::mutex mtx; int main() { using namespace std; constexpr size_t size = 1'000'000u; mt19937 eng; vector vec1(size); vector vec2(size); eng.seed(134); for_each( vec1.begin(), vec1.end(), [&eng](unsigned int& x) {x = eng(); } ); // Sequence; Birinci rastgele sayı, dizinin ilk öğesine atanacak. eng.seed(134); for_each( execution::par, vec2.begin(), vec2.end(), [&eng](unsigned int& x) { scoped_lock lock(mtx); x = eng(); } ); // Parallel; Birinci rastgele sayı, dizinin ilk öğesine atanmayabilir. cout << boolalpha << (vec1 == vec2); // false sort(vec1.begin(), vec1.end()); sort(vec2.begin(), vec2.end()); cout << boolalpha << (vec1 == vec2); // true } * Örnek 8.0, "std::partial_sum" fonksiyonunun "execution_policy" argümanına sahip versiyonu bir "overload" değil, "inclusive_scan" ve "exclusive_scan" isimli fonksiyonlardır. #include #include #include int main() { using namespace std; vector svec{ 1, 3, 5, 7, 9, 11, 13, 15 }; // 1 = 1 // 1 + 3 = 4 // 4 + 5 = 9 // 9 + 7 = 16 // 16 + 9 = 25 // 25 + 11 = 36 // 36 + 13 = 49 // 49 + 15 = 64 vector dvec(svec.size()); // Bu fonksiyon varsayılan olarak "std::plus" almaktadır. Fakat diğer "overload" // versiyonuna bir "callable" geçerek istediğimiz operasyonu yaptırtabiliriz. partial_sum(svec.begin(), svec.end(), dvec.begin()); for (auto i : dvec) cout << i << ' '; // 1 4 9 16 25 36 49 64 } * Örnek 8.1, #include #include #include #include int main() { using namespace std; vector svec{ 1, 3, 5, 7, 9, 11, 13, 15 }; vector dvec(svec.size()); inclusive_scan(std::execution::par, svec.begin(), svec.end(), dvec.begin()); for (auto i : dvec) cout << i << ' '; // 1 4 9 16 25 36 49 64 cout << '\n'; // Başlangıç değerini "exclusive_scan" için belirleyebiliyoruz. exclusive_scan(std::execution::par, svec.begin(), svec.end(), dvec.begin(), 10); for (auto i : dvec) cout << i << ' '; // 10 11 14 19 26 35 46 59 cout << '\n'; } >> "thread pooling" : "thread" oluşturmak çok maaliyetli bir iştir. Dolayısıyla her seferinde bir "thread" oluşturmaktansa, işin başında belli bir miktar oluştururuz. Yapılacak iş(ler) ile oluşturulan bu "thread" (ler) karşılıklı eşleşecekler. Dolayısıyla iş yükünün ağırlığı ile oluşturulacak "thread" adedinin doğrusal olması önemlidir. * Örnek 1, // ThreadPool.h #pragma once #include "CQueue.h" #include #include #include using Task = std::function; class ThreadPool { public: ThreadPool() { const auto thread_count = std::thread::hardware_concurrency(); for (std::size_t i{}; i < thread_count; ++i) { m_tvec.emplace_back( &ThreadPool::work_load, this ); } } ~ThreadPool() { for (auto& i : m_tvec) i.join(); } void push(Task f) { m_task_queue.push(f); } //... private: CQueue m_task_queue; std::vector m_tvec; void work_load() { for (;;) { Task task; m_task_queue.pop(task); task(); } } }; // CQueue.h #pragma once #include #include #include template class CQueue { public: //... void push(const T& tval) { std::scoped_lock sl{ mtx }; mq.push(tval); mcv.notify_one(); } void pop(T& tval) { std::unique_lock ul{ mtx }; // "std::unique_lock" is must here. mcv.wait( ul, [this]{ return !mq.empty(); } ); tval = mq.front(); mq.pop(); } //... private: std::mutex mtx; std::queue mq; std::condition_variable mcv; }; // main.cpp #include "CQueue.h" #include "ThreadPool.h" #include #include #include using namespace std::chrono; void task() { std::osyncstream{ std::cout } << "thread-" << std::this_thread::get_id() << " : started.\n"; std::this_thread::sleep_for(300ms); std::osyncstream{ std::cout } << "thread-" << std::this_thread::get_id() << " : stopped.\n"; } int main() { /* # OUTPUT # Threads in the pool: 8 thread-140255750960704 : started. thread-140255708997184 : started. thread-140255717389888 : started. thread-140255725782592 : started. thread-140255767746112 : started. thread-140255759353408 : started. thread-140255742568000 : started. thread-140255734175296 : started. thread-140255750960704 : stopped. thread-140255708997184 : stopped. thread-140255725782592 : stopped. thread-140255717389888 : stopped. thread-140255767746112 : stopped. thread-140255742568000 : stopped. thread-140255759353408 : stopped. thread-140255734175296 : stopped. Done! */ // Max. # of thread supported by the implementation: const auto thread_count = std::thread::hardware_concurrency(); std::cout << "Threads in the pool: " << thread_count << '\n'; ThreadPool the_pool; // Bu aşamada "the_pool" içerisindeki vektör dizisinin her bir // elemanı ayrı bir "thread" ve bu "thread" ler de sınıfın // "work_load" fonksiyonlarını çalıştırmaya başlıyorlar. for (std::size_t i{}; i < 1 * thread_count; ++i) { // Her bir "thread" için "1" adet iş yükü tayin // edilmiştir. the_pool.push(task); } std::this_thread::sleep_for(5s); std::cout << "Done!\n"; } >>> "semaphore" nesnesi: "n" tane "thread" i bir koda ulaşmasını sağlamaktır. "std::recursive_mutex" yapısını andırmaktadır fakat "mutex" nesnelerinden farkı şudur; "mutex" nesnelerinde ".lock()" fonksiyonunu çağıran "thread" in ".unlock()" fonksiyonunu çağırması lazım gelir ki diğer "thread" lerin akışları devam edebilsin. Fakat "semaphore" nesnesinde durum farklıdır. Kritik Bölge'ye girmek isteyenler, nesnenin ".release()" fonksiyonuna çağrı yaparlar. İzin verilirse giriş yaparlar. Kritik Bölge'den çıkanlar da yine nesnenin ".acquire()" isimli fonksiyonunu çağırır ki sayaç bilgisi güncellensin. "semaphore" nesnesi, tipik olarak bir değişkeni ve bir kuyruk yapısını sarmalamaktadır. Sarmalanan bu tam sayı değişken, kaç tane "thread" in Kritik Bölgeye girebileceği ile ilgili. Eğer bu tam sayı değişkenin değeri "0" olursa, artık giriş kapatılacak ve girmeye çalışan "thread" ler bloke edilecektir. Ta ki giriş hakkı tekrardan doğana kadar. Başlık dosyasının ismi "semaphore" dir. Kullanılan sınıf ise "counting_semaphore" isimli bir sınıf şablonudur. C++20 ile dile eklenmiştir, bu sınıf. Yine sınıfın "try_" fonksiyonları "bool" değer döndürür, müsaitlik durumunu sorgular ve izin verilmediği taktirde blokeye yol açmaz ve içinde "_for" ve "_until" geçen fonksiyonlar sırasıyla "o kadar süre boyunca" ve "o vakte kadar" anlamları içerir. * Örnek 1, #include int main() { // "10" adet "thread" in girişine // izin verilir. std::counting_semaphore sem(10); // Hiç bir "thread" in girişine izin // verilmez. Herhangi bir "thread" in // "sem_zero" nun ".release()" fonksiyonunu // çağırması gerekiyor ki bekleyenlerden // bir tanesi girebilsin. std::counting_semaphore sem_zero(0); // En fazla bir adet "thread" in geçişine // izin verilir demektir. İşte böyle yapmak // yerine tür eş ismi eklenmiştir; -----> (1) std::counting_semaphore<1> sem_binary(0); // -----> (1) std::binary_semaphore binary_semaphore(1); } * Örnek 2, #include int main() { std::counting_semaphore smp(5); // En fazla "5" adede izin verildi. // "main-thread" tarafından yapılan bu çağrı sonucunda // sayaç değişkeni "4" oldu. Eğer sayaç değeri "0" olursa, // bu çağrı o "thread" i bloke edecektir. smp.acquire(); //... // "main-thread" tarafından yapılan bu çağrı sonucunda // sayaç değişkeni tekrar "5" oldu. smp.release(); // "main-thread" tarafından yapılan bu çağrı sonucunda // sayaç değişkeni tekrar "7" oldu. // smp.release(3); } * Örnek 3, #include #include #include #include #include #include using namespace std; counting_semaphore smp{ 3 }; void func() { using namespace literals; smp.acquire(); osyncstream{ cout } << "thread id : " << this_thread::get_id() << '\n'; this_thread::sleep_for(5000ms); smp.release(); } int main() { /* # OUTPUT # thread id : 140130699408960 thread id : 140130682623552 thread id : 140130691016256 :> thread id : 140130665838144 thread id : 140130649052736 thread id : 140130371212864 :> thread id : 140130404783680 thread id : 140130329249344 thread id : 140130387998272 :> thread id : 140130640660032 thread id : 140130346034752 thread id : 140130337642048 :> thread id : 140130396390976 thread id : 140130657445440 thread id : 140130379605568 :> thread id : 140130354427456 thread id : 140130304071232 thread id : 140130362820160 :> thread id : 140130674230848 thread id : 140130320856640 thread id : 140130421569088 :> thread id : 140130632267328 thread id : 140130413176384 thread id : 140130312463936 :> */ vector tvec; for (int i = 0; i < 24; ++i) { tvec.emplace_back(func); } } * Örnek 4, #include #include #include #include #include #include using namespace std; using namespace literals; counting_semaphore smp{ 3 }; void foo() { smp.acquire(); osyncstream{ cout } << "thread id in foo: " << this_thread::get_id() << '\n'; this_thread::sleep_for(3000ms); smp.release(); } void bar() { smp.acquire(); osyncstream{ cout } << "thread id in bar : " << this_thread::get_id() << '\n'; this_thread::sleep_for(3000ms); smp.release(); } int main() { /* # OUTPUT # thread id in bar : 140670448510528 thread id in foo: 140670440117824 thread id in bar : 140670431725120 :> thread id in foo: 140670423332416 thread id in foo: 140670287201856 thread id in foo: 140670389761600 :> thread id in bar : 140670398154304 thread id in foo: 140670178162240 thread id in bar : 140670262023744 :> thread id in foo: 140670127806016 thread id in foo: 140670270416448 thread id in bar : 140670152984128 :> thread id in bar : 140670278809152 thread id in bar : 140670043944512 thread id in bar : 140670414939712 :> thread id in bar : 140670136198720 thread id in foo: 140670406547008 thread id in bar : 140670312379968 :> thread id in foo: 140670144591424 thread id in foo: 140670303987264 thread id in foo: 140670035551808 :> thread id in bar : 140670295594560 thread id in bar : 140670169769536 thread id in foo: 140670161376832 */ vector tvec; for (int i = 0; i < 24; ++i) { if (i % 2) tvec.emplace_back(foo); else tvec.emplace_back(bar); } } * Örnek 5, #include #include #include #include std::string name{}; std::binary_semaphore smp(0); void prepare() { name = "tamer dundar"; std::cout << "data is ready now\n"; smp.release(); } void use() { std::cout << "use is waiting the data\n"; smp.acquire(); std::cout << "name is " << name << '\n'; } int main() { // use is waiting the data // data is ready now // name is tamer dundar std::jthread t1(prepare); std::jthread t2(use); } * Örnek 6, // adapted from Reiner Grimm (modified) #include #include #include std::binary_semaphore sm_ping{0}; std::binary_semaphore sm_pong{0}; std::atomic counter{ 0 }; constexpr int n = 100; void ping() { while (counter <= n) { sm_ping.acquire(); ++counter; std::cout << "ping\n"; sm_pong.release(); } } void pong() { while (counter < n) { sm_pong.acquire(); ++counter; std::cout<< "pong\n"; sm_ping.release(); } } int main() { /* # OUTPUT # ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping pong ping */ sm_ping.release(); std::jthread t1(ping); std::jthread t2(pong); } > "19. Cpp Idioms/Patterns > Strategy Pattern" : Müşteri kodlara, strateji seçimi imkanı verilebilmektedir. * Örnek 1, #include #include #include /** * The Strategy interface declares operations common to all supported versions * of some algorithm. * * The Context uses this interface to call the algorithm defined by Concrete * Strategies. */ class Strategy { public: virtual ~Strategy() = default; virtual std::string doAlgorithm(std::string_view data) const = 0; }; /** * The Context defines the interface of interest to clients. */ class Context { /** * @var Strategy The Context maintains a reference to one of the Strategy * objects. The Context does not know the concrete class of a strategy. It * should work with all strategies via the Strategy interface. */ private: std::unique_ptr strategy_; public: /** * Usually, the Context accepts a strategy through the constructor, but also * provides a setter to change it at runtime. */ explicit Context(std::unique_ptr &&strategy = {}) : strategy_(std::move(strategy)){} /** * Usually, the Context allows replacing a Strategy object at runtime. */ void set_strategy(std::unique_ptr &&strategy) { strategy_ = std::move(strategy); } /** * The Context delegates some work to the Strategy object instead of * implementing +multiple versions of the algorithm on its own. */ void doSomeBusinessLogic() const { if (strategy_) { std::cout << "Context: Sorting data using the strategy (not sure how it'll do it)\n"; std::string result = strategy_->doAlgorithm("aecbd"); std::cout << result << "\n"; } else { std::cout << "Context: Strategy isn't set\n"; } } }; /** * Concrete Strategies implement the algorithm while following the base Strategy * interface. The interface makes them interchangeable in the Context. */ class ConcreteStrategyA : public Strategy { public: std::string doAlgorithm(std::string_view data) const override { std::string result(data); std::sort(std::begin(result), std::end(result)); return result; } }; class ConcreteStrategyB : public Strategy { std::string doAlgorithm(std::string_view data) const override { std::string result(data); std::sort(std::begin(result), std::end(result), std::greater<>()); return result; } }; /** * The client code picks a concrete strategy and passes it to the context. The * client should be aware of the differences between strategies in order to make * the right choice. */ void clientCode() { Context context(std::make_unique()); std::cout << "Client: Strategy is set to normal sorting.\n"; context.doSomeBusinessLogic(); std::cout << "\n"; std::cout << "Client: Strategy is set to reverse sorting.\n"; context.set_strategy(std::make_unique()); context.doSomeBusinessLogic(); } int main() { /* # OUTPUT # Client: Strategy is set to normal sorting. Context: Sorting data using the strategy (not sure how it'll do it) abcde Client: Strategy is set to reverse sorting. Context: Sorting data using the strategy (not sure how it'll do it) edcba */ clientCode(); return 0; } > "20. Cpp Idioms/Patterns > Thread-Safe Interface" : Sınıf türünden nesnelerin "Internal Synchronization" ile "thread-safe" hale getirilmesidir. Yani "public interface" de bulunan bütün fonksiyonlar bir kilit kullanacak ama bu fonksiyonların kullandığı implementasyon ("private" ve "protected" bölümdekiler) fonksiyonları kilit kullanmayacak. Dolayısıyla "public" fonksiyonlar sadece "protected" ve "private" bölümündekileri çağırmalı, "public" bölümdeki diğer fonksiyonları ÇAĞIRMAMALIDIR. Böylece "dead_lock" oluşmayacak, "recursive_mutex" kullanmaya gerek kalmayacak vs. Tabii sınıfın "non-const static" veri elemanları da varsa, yine bunları da senkronize etmek gerekmektedir. Yine "virtual-dispatch" varsa, "override" edilen fonksiyonların da kilit altında çalıştırılması gerekmektedir. Son olarak referans döndüren fonksiyon çağrıları sonucunda sınıfın veri elemanına doğrudan erişim mümkün olduğu için, "mutex" kapsamından çıkılmış olmaktadır. * Örnek 1, #include #include #include class Myclass_TSI { public: void foo() { std::scoped_lock sl{ mtx }; fx_impl(); } void bar() { std::scoped_lock sl{ mtx }; fx_impl(); fy_impl(); } void baz() { std::scoped_lock sl{ mtx }; fx_impl(); fy_impl(); fz_impl(); } private: void fx_impl() { std::cout << "fx_impl: [" << std::this_thread::get_id() << "]\n"; } void fy_impl() { std::cout << "fy_impl: [" << std::this_thread::get_id() << "]\n"; } void fz_impl() { std::cout << "fz_impl: [" << std::this_thread::get_id() << "]\n"; } private: mutable std::mutex mtx; }; int main() { /* # OUTPUT # fx_impl: [139770356966976] fx_impl: [139770348574272] fy_impl: [139770348574272] fx_impl: [139770340181568] fy_impl: [139770340181568] fz_impl: [139770340181568] */ Myclass_TSI test_object; std::jthread t_main{ [&]{ test_object.foo(); } }; { std::jthread t1{ [&]{ test_object.bar(); } }; std::jthread t2{ [&]{ test_object.baz(); } }; } return 0; } > "21. Cpp Idioms/Patterns > Type Erasure" : Birden çok formu vardır. Örneğin, C dilinde fonksiyonların parametrelerinin "void*" yapılmaları, C++ dilinde kalıtım hiyerarşisi kullanmak suretiyle fonksiyonların parametrelerinin taban sınıf türünden gösterici/referans olmaları vb. Dolayısıyla buradaki kritik nokta ortak bir takım özelliklere sahip farklı türlerin bir arada kullanılmasıdır. Yani bir kap içerisinde "printable" özellikte fakat farklı türden değişkenleri tutmak isteyelim. Bunun için kalıtım kullanamam çünkü "primitive" türler için kalıtım uygulanamaz. "std::variant" da kullanamam çünkü kapalı bir hiyerarşi oluşturmuş olurum, ileride genişletemem. İşte bunu gerçekleştiren örnek aşağıdaki gibi olacaktır: * Örnek 1, #include #include #include #include /* * Aşağıdaki örnekte "printable" özelliğinde olan sınıfları * tek bir "container" içerisinde saklandı. Dolayısıyla * ilgili sınıfların "get_name()" isminde bir sınıfı bünyelerinde * barındırması gerekmektedir. */ struct Foo { std::string get_name() const { return "Foo"; } }; struct Bar { std::string get_name() const { return "Bar"; } }; struct Baz { std::string get_name() const { return "Baz"; } }; class Object { public: template Object(T&& val) : m_concept_ptr(std::make_shared>(std::forward(val))) {} std::string get_name()const { return m_concept_ptr->get_name(); } struct Concept { virtual ~Concept() = default; virtual std::string get_name()const = 0; }; template struct Model : Concept { Model(const T& tval) : m_object(tval) {} virtual std::string get_name()const override { return m_object.get_name(); } T m_object; }; private: std::shared_ptr m_concept_ptr; }; int main() { /* # OUTPUT # Foo Bar Baz Foo Bar Baz */ std::vector vec{ Object(Foo()), Object(Bar()), Object(Baz()) }; Foo foo; Bar bar; Baz baz; vec.push_back(foo); vec.push_back(bar); vec.push_back(baz); for (auto& x : vec) std::cout << x.get_name() << '\n'; } * Örnek 2, #include class Animal { public: template Animal(T&& tval) : mptr(std::make_unique>(std::forward(tval))) {} Animal(const Animal& other) : mptr(other.mptr->clone()) {} Animal& operator=(const Animal& other) { return *this = Animal(other); } Animal(Animal&&)noexcept = default; Animal& operator=(Animal&&)noexcept = default; void cry() { mptr->cry(); } private: struct AnimalConcept { virtual ~AnimalConcept() = default; virtual std::unique_ptr clone()const = 0; virtual void cry() = 0; }; template struct AnimalModel : public AnimalConcept { AnimalModel(const T& t) : m_object(t) {} std::unique_ptr clone()const override { return std::make_unique(*this); } void cry() override { m_object.cry(); } T m_object; }; std::unique_ptr mptr; }; #include #include class Cat { public: void cry() { std::cout << "miyav miyav miyav\n"; } }; class Dog { public: void cry() { std::cout << "hav hav hav\n"; } }; class Bird { public: void cry() { std::cout << "cik cik cik\n"; } }; int main() { /* # OUTPUT # miyav miyav miyav hav hav hav cik cik cik hav hav hav miyav miyav miyav cik cik cik */ Animal a1{ Cat{} }; Animal a2{ Dog{} }; Animal a3{ Bird{} }; a1.cry(); a2.cry(); a3.cry(); std::vector avec; avec.emplace_back(Dog{}); avec.emplace_back(Cat{}); avec.emplace_back(Bird{}); for (auto& a : avec) a.cry(); } > Hatırlatıcı Notlar: >> Concurrency in Action kitabı tavsiye edilir. >> "STL" kütüphanesindeki fonksiyonlar, farklı indislerden yazma işlemi için senkronizasyon garantisi verirken aksi halde garanti vermemektedir. >> Unutmamalıyız ki bu "std::mutex" sınıfı ne taşınabilir ne de kopyalanabilir bir sınıftır. >> 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. >> "std::lock_guard" yerine "std::scoped_lock" kullanmalıyız, C++17 itibariyle. >> "std::accumulate" : * Örnek 1, #include #include #include #include #include int main() { using namespace std; vector ivec{ 1, 3, 5, 7, 9 }; cout << accumulate( ivec.begin(), ivec.end(), 0 ) << '\n'; // 25 cout << accumulate( ivec.begin(), ivec.end(), 0, [](int prev_result, int val) { return prev_result + val; } ) << '\n'; // 25 cout << accumulate( ivec.begin(), ivec.end(), ""s, [](string prev_result, int val) { return prev_result + '-' + to_string(val); } ) << '\n'; // -1-3-5-7-9 /* // ERROR cout << reduce( execution::par, ivec.begin(), ivec.end(), "0"s, [](string prev_result, int val) { return prev_result + to_string(val); } ) << '\n'; */ } >> "Sequenced Before" : Tek bir "thread" ile ilgilidir. C dilindeki "sequence point" kavramının daha gelişmiş halidir. >>> "sequence point": Kaynak kodda öyle bir noktaki o noktadan önce yazılmış kodların yan etkileri, iş bu noktadan sonra artık gözlemlenebilir olmak zorundadır. >> "Happens Before" : Tek bir "thread" ile de ilgili olabilir, birden fazla "thread" ile de. "Sequenced Before" kavramının daha gelişmiş halidir. Burada işlemlerin önceliği değil, işlemlerin sonucunun gözlemlenebilirliği kastetilir. Örneğin, iki ayrı "thread" de koşan "t_opt1" ve "t_opt2" isimli işlemlerimiz olsun ve bu ikisi arasında da "Happens Before" ilişkisi olsun. Dolayısıyla "t_opt1" işlemi "t_opt2" işleminden önce yapılır manasına değil, "t_opt1" işleminin oluşturduğu sonuç "t_opt2" tarafından GÖZLEMLENEBİLİR manasına GELMEKTEDİR. Çünkü zamansal olarak "t_opt1" işlemi önce gerçekleşmiş olabilir fakat sonuç henüz belleğe aktarılmadığı için "t_opt2" işlemi tarafından görülmeyebilir. Yani yapılan değişiklik henüz içsel durumdadır. İşte "std::mutex", "std::condition_variable" ve/veya "atomic" nitelikteki nesneler ile "Happens Before" ilişkisi kurabiliriz. >> "Sequential Consistency" : "thread" lerin kendi içerisindeki "atomic" işlemlerin sırasının korunduğunun garantisidir. Örneğin, iki adet "thread" imiz olsun. Bu iki "thread" ise kendi içinde önce ".store()" daha sonra ".load" işlemi yapıyor olsun. Kendi içlerindeki bu ".store()" ve ".load()" işlemleri arasında "Sequential Consistency" varsa, program genelinde önce "store", sonra "load" işlemi yapılır. Ama bu işlemler ard arda yapılacağı garanti DEĞİLDİR. Uzaktan bakıldığında "store" işlemi önce, sonra "load" işlemi. /*================================================================================================================================*/ (45_07_02_2024_coroutine_01) & (46_19_02_2024_coroutine_02) & (47_20_02_2024_coroutine_03) & (48_21_02_2024_coroutine_04) (49_23_02_2024_coroutine_05) & (50_23_02_2024_coroutine_06) & (51_27_02_2024_coroutine_07) > "coroutines" : >> "coroutins" dediğimiz şeyler aslında birer fonksiyonlardır. Fakat öyle fonksiyonlardır ki belirli bir noktada çalışmasını istediğimiz zaman durdurabilir, yine istediğimiz zaman devam ettirebiliriz. Tabii durdurulma esnasında programın akışı, çağıran fonksiyona geri dönmektedir. Durdurma-tekrar çalıştırma mekanizması defalarca tekrar edebilir. >> C++20 ile dile eklenen bir araçtır. >> Karşımıza üç adet anahtar sözcük çıkmaktadır: co_await, co_yield, co_return Şimdi bu operatörleri detaylı incelemeye başlayalım: >>> "co_await" : >>>> C++20 ile birlikte "operator co_await" isimli bir operatör fonksiyonu daha tanımlar olduk. Bu operatör iki farklı yerde kullanılır. Ya bizler bu operatöre kendimiz bir operand geçeriz ya da derleyicinin "coroutine" gördüğü yerde, o ifadeden yola çıkarak bir takım kodlar yazması halinde. Burada derleyicinin "co_await" operatörünü görmesinden hareketle yazılacak kod temsili olarak aşağıdaki gibidir: //... { promise_type promise /* { Ctor. Arguments } */; try { co_await promise.initial_suspend(); //... ---> COROUTINE FUNCTION'S BODY } catch(...) { if (/* !initial-await-resume-called */) throw; promise.unhandled_exception(); } final-suspend: co_await promise.final_suspend(); } >>>> Diğer yandan "co_await" operatörün operandı bir "awaitable" olmak zorundadır. Dolayısıyla bu operatörü sadece "coroutine" için kullanabiliriz. >>>>> "awaitable" türü: Bir sınıf türüdür. Bu sınıfı kendimiz de yazabileceğimiz gibi standart kütüphanede hazır olarak da mevcuttur. Örneğin, "std::suspend_never" ve "std::suspend_always" isimli sınıflar, birer "awaitable" türündendir. >>>> Derleyici bu operatörü gördüğünde "awaiter" türünden bir nesne elde etmek isteyecektir. Bunun için, >>>>> İlk olarak derleyici "promise_type" sınıfının "await_transform" isimli bir üye fonksiyona sahip olup olmadığını sınar. Eğer böyle bir fonksiyona sahipse, direkt o fonksiyona çağrı yapar ve elde ettiği geri dönüş değerini de iş bu "co_await" operatörüne operand olarak geçer. Fonksiyona da argüman olarak, bizim "co_await" için kullandığımız ifadeyi geçer. Eğer böyle bir fonksiyon mevcut değilse, "co_await" operatörünün operand olarak kullandığımız ifade dikkate alınacaktır. Yani fonksiyon varsa bizim "co_await" için kullandığımız operandı "await_transform" fonksiyonuna gönderiyor ve elde ettiği değeri ise "co_await" için kullanıyor. Aksi halde bizim "co_await" için kullandığımızı kullanıyor. Böylelikle bir "awaitable" türünden bir nesne elde etmiş olacağız. Şimdi bu nesneden hareketle "awaiter" türünden bir başka nesne elde etmek isteyelim. >>>>> İşte gerek "await_transform" çağrısı ile bir "awaiter" elde edilmesi gerek bizim direkt olarak bir "awaitable" türünden ifade kullandıktan sonra derleyici, bir "awaiter" türünden nesne elde edebilmek adına, -> İlgili "awaitable" türünün "operator co_await" isimli fonksiyonuna sahip olup olmadığını bakar. Eğer o sınıfta bu isimde bir üye fonksiyon varsa, ona çağrı yapılacak. -> Eğer öyle bir üye fonksiyon yoksa, aynı isimde global isimde bir fonksiyon olup olmadığına bakacak. Eğer varsa, ona çağrı yapacak. Bu fonksiyonların çağrılması sonucunda "awaiter" türünden bir nesne elde edilmeye çalışılır. Eğer o isimde bir fonksiyon hiç yoksa ilgili "awaitable" türünden nesne zaten "awaiter" türündendir denilir. Dolayısıyla şu tespit doğrudur: Her "awaiter" aslında bir "awaitable" dır. FAKAT HER "awaitable" IN BİR "await" OLMASI GEREKMİYOR. Dolayısıyla yukarıdaki "std::suspend_never" ve "std::suspend_always" isimli sınıflar aslında birer "awaiter" sınıflardır. >>>>>> "awaiter" : Niyahet böyle bir sınıf türü elde ettik. Pekiyi bu nasıl bir sınıf olmalıdır? İşte bu sınıf "await_ready", "await_suspend" ve "await_resume" isimli fonksiyonlara sahip olması ve çağrılabilir olması GEREKMEKTEDİR. Bu fonksiyonlardan, >>>>>>> "await_ready" : "bool" türünden değer döndürür. Ekseriyetle "false" değerini döndürdüğünü görürüz. "false" olması halinde "suspend" teklifi kabul edilecektir. Yani "suspend" edilebilmesi için bu fonksiyonun "false" değer döndürmesi gerekmektedir. "IT IS NOT READY YET, SO SUSPEND IT". Eğer "true" döndürürse, "suspend" edilme maaliyetinden kaçınılacağı için, "suspend" edilemeyecektir. Bir diğer deyişle "suspend" HİÇ VUKU BULMAYACAKTIR. YANİ "std::suspend_never" sınıfının "await_ready" fonksiyonu "true" değer döndürmektedir. Bu fonksiyon durdurulmadan HEMEN ÖNCE ÇAĞRILMAKTADIR. >>>>>>> "await_suspend" : Bu fonksiyonun parametresi "std::coroutine_handle" türündendir. Bu fonksiyon derleyici tarafından "suspend" vuku bulması halinde çağrılır. >>>>>>>>> "coroutine_handle" : Aslında bu kavram, aşağıda kabaca değineceğimiz, "Return Object" nesnesini tutan kavramdır. Bünyesinde ilgili "coroutine" nin devam ettirilmesini, akıbetinin sorgulanması vb. işleri yapmamızı sağlayan fonksiyonları barındırır. Standart bir kavramdır. Bu sınıf bünyesinde bir takım fonksiyonlar barındırır. En önemlileri ise "resume", "done" ve "destroy" isimli fonksiyonlardır. Bu fonksiyonlardan, -> "resume" : -> "done" : -> "destroy" : Fonksiyonun geri dönüş değeri "void", "bool" ya da "std::coroutine_handle" türünden birisi olabilir. Eğer, -> "void" olması halinde "suspend" vuku bulacak ve programın akışı çağıran koda dönecektir. -> "bool" olması halinde "suspend" işlemi bir şarta bağlanır. "true" değer dönerse "suspend" gerçekleşecek, "false" dönerse gerçekleşmeyecektir. Bir diğer deyişle "bool" olması halinde "await_ready" fonksiyonundaki işleyişin tam tersi bir mekanizma işletilecektir. >>>>>>> "await_resume" : Bu fonksiyon ise "std::suspend_never" vuku bulması halinde çağrılır. Programın akışı "suspend" edildiği noktadan tekrar başlamasından hemen evvel çağrılır. C++20 standardı ile elimizde iki adet standart bir "awaiter" sınıfı vardır. Bunlar "std::suspend_never" ve "std::suspend_always" isimli sınıflardır. Bu sınıflardan "std::suspend_never" kullanılması durumunda "suspend" EDİLMEYECEK, "std::suspend_always" kullanılması durumunda ise "suspend" EDİLECEKTİR. Bu fonksiyonların temsili tanımlaması ise, struct suspend_always { constexpr bool await_ready() const noexcept { return false; } constexpr void await_suspend(std::coroutine_handle<>) const noexcept { } constexpr void await_ready() const noexcept { } }; struct suspend_never { constexpr bool await_ready() const noexcept { return true; } constexpr void await_suspend(std::coroutine_handle<>) const noexcept { } constexpr void await_ready() const noexcept { } }; şeklindedir. >>>> Bu operatör "coroutine" in akışının "suspend" edilmesini sağlar. >>> "co_yield : >>>> Bu operatör "coroutine" in akışının "suspend" edilmesini sağlar. "co_await" operatöründen farkı "suspend" oluştuğunda kendisini çağıran koda bir değer de döndürmesini sağlar. Bunu da "promise_type" sınıfının "yield_value" isimli fonksiyonu çağırır ve onun geri dönüş değerini argüman olarak alır. Böylelikle "co_yield" karşılığı olan ifade için "coroutine" çağıran koda bir değer döndürmüş olur. >> "coroutines" tip fonksiyonların geri dönüş değeri kavramı da pekala vardır fakat esasında "coroutines" tip fonksiyonların bize sunduğu "interface" kavramıdır. İşte böylesi fonksiyonların, kendisine sunduğu "interface" yi kullanabilmek için, fonksiyonun geri döndürdüğü "Return Object" i kullanmamız gerekmektedir. İş bu "Return Object" hem "coroutines" tip fonksiyon ile haberleşmemizi hem de onun döndürdüğü değerin ne olduğunu anlamamıza olanak sağlamaktadır. >>> İş bu geri dönüş değeri olan sınıf(lar)ın tanımları henüz standart kütüphaneye ekli olmadığı için (C++20 itibariyle), bu sınıfları ya bizler yazmalı ya da üçüncü parti kütüphanelerden faydalanmalıyız. >> "coroutines" tip fonksiyonlar, içsel mekanizmaları için pekala bellek yönetimi kullanmaktadırlar. Çünkü kaldıkları yerden devam edebilmeleri için "Resume Point" dediğimiz verinin saklanması gerekmektedir. Buna ilave olarak "coroutines" tip fonksiyonlarda kullanılan yerel değişkenlerin, iş bu fonksiyonların parametre değişkenlerinin vb. değerlerin de pekala saklanmış olması gerekmektedir. İşte bu mekanizmaya ise "Coroutine State" veya "Coroutine Frame" denmektedir. Genel olarak burada kullanılan mekanizma için Dinamik Bellek Yöntemleri kullanılır fakat özel durumlarda derleyiciler optimizasyon yaparlar ve Dinamik Bellek Yöntemlerinin maliyetini elimine ederler. >> "coroutines" tip fonksiyonlar aslında birer "opaque" biçimindedir. Yani arka planda işlenen şeyler, kendisini çağırandan direkt gizlenmiştir. Sadece bir takım "Coroutine Handle" araçları kullanarak dış dünyaya açılmaktadır. >> Aynı "coroutine" fonksiyonu farklı "thread" ler ile kullanabiliriz. Yani bir "thread" ile durdurulan, başka bir "thread" ile tekrar çalışmaya devam ettirilebilir. >> "coroutine" başlık dosyasının eklenmesi gerekmektedir. >> Derleyicinin bir fonksiyonu "coroutine" olarak ele alması için, >>> Fonksiyonun tanımında yukarıdaki üç anahtar sözcükten("co_await", "co_yield" yada "co_return") birisinin olması gerekmektedir. >>> Geri dönüş değerinin bir sınıf türünden olması gerekmektedir. Öyle bir sınıf ki bu sınıf aşağıdaki şartları da sağlamalıdır: >>>> "promise_type" türünden bir "nested_type" ı bünyesinde barındırması gerekmektedir. Bu türü sınıfımız içerisinde tanımlamak zorunda değiliz. Sadece bildirimini sınıf içerisinde bulundurup, tanımını başka yerde de yapabiliriz. * Örnek 1, //... #include #include struct CoroFace{ //... struct promise_type { }; }; CoroFace foo() { std::cout << "Hello Coroutine!\n"; co_return; } >>>> İş bu "promise_type" türünden sınıf sırasıyla "initial_suspend", "get_return_object", "final_suspend", "return_void"/"return_value" ve "unhandled_exception" isimli fonksiyonlarına sahip olması gerekmektedir. Bu fonksiyonlardan, >>>>> "initial_suspend" : "coroutine" çalışmaya başlar başlamaz "suspend" edilsin mi edilmesin mi diye belirlemede bulunmak için kullanılır. Eğer "suspend" edilsin şeklinde bir belirleme yapılırsa, "coroutine" çağrılır fakat "suspend" edilir ve programın akışı geri döner. Ta ki "resume" edilene kadar. Aksi halde, yani "suspend" edilmesin şeklinde bir belirleme yapılırsa, programın akışı yukarıdaki üç anahtar sözcükten birine gelene kadar akar. O noktada "suspend" edilir. Bu fonksiyonun geri dönüş değerinin türü ise "awaiter" türündendir. Dolayısıyla geri dönüş değeri ya "std::suspend_always" ya da "std::suspend_never" türünden birisi olur. * Örnek 1, Fonksiyonun tipik kullanımı: //... #include #include struct CoroFace{ //... struct promise_type { auto initial_suspend() { return std::suspend_never{}; // Buradaki "suspend_never" in türü "awaiter" biçiminde olup, bu geri dönüş değeri ise // "co_await" operatörüne operand olacaktır. } }; }; CoroFace foo() { std::cout << "Hello Coroutine!\n"; co_return; } * Örnek 1.1, //... #include #include struct CoroFace{ //... struct promise_type { std::suspend_never initial_suspend() { return {}; } }; }; CoroFace foo() { std::cout << "Hello Coroutine!\n"; co_return; } İleride detaylarına gireceğimiz üzere; bu fonksiyonun geri dönüş değerinin türü için "awaiter" türü demiştik. İşte bu tür ise aslında "co_await" operatörüne operand olmaktadır. >>>>> "get_return_object" : Bu fonksiyon işte "coroutine" ile onu çağıran kod arasında bir "interface" olarak kullanılmak gereken "Return Object" isimli nesneyi döndürmektedir. Dolayısıyla bu fonksiyonun geri dönüş değerinin türü, "coroutine" fonksiyonunun geri dönüş değeri olan sınıf türünden olmalıdır. * Örnek 1, Fonksiyonun tipik kullanımı: //... #include #include struct CoroFace{ //... struct promise_type { auto initial_suspend() { return std::suspend_never{}; } CoroFace get_return_object() { return {}; // This will return a default constructed CoroFace object. } }; }; CoroFace foo() { std::cout << "Hello Coroutine!\n"; co_return; } >>>>> "final_suspend" : Tıpkı "initial_suspend" gibi "suspend" oluşsun mu oluşmasın mı diye belirleme yapmak için kullanılır. Tek farkı "coroutine" fonksiyonunun çalışmasının bitmesinden bu belirlemenin kullanılacak olmasıdır. Yani yapılacak belirlemeye göre ya fonksiyon işini bitirdikten sonra "suspend" olacak ya da olmadan sonlanacaktır. Ek olarak bu fonksiyonun "noexcept" olma zorunluluğu vardır. * Örnek 1, Fonksiyonun tipik kullanımı: //... #include #include struct CoroFace{ //... struct promise_type { auto initial_suspend() { return std::suspend_never{}; } CoroFace get_return_object() { return {}; } auto final_suspend() noexcept { return std::suspend_never{}; } }; }; CoroFace foo() { std::cout << "Hello Coroutine!\n"; co_return; } >>>>> "return_void" / "return_value" : Bu fonksiyonlardan yalnızca bir tanesini tanımlayabiliriz. Programın akışı "co_return" çağrısına geldiğinde yalın bir şekilde dönüş yapmak istiyorsak, "co_return" operatörünün bir operandı yoksa, "return_void" fonksiyonunu tanımlamalıyız. Eğer "co_return" operatörüne bir operand vermişsek, "return_value" fonksiyonunu tanımlamalıyız. * Örnek 1, //... #include #include struct CoroFace{ //... struct promise_type { auto initial_suspend() { return std::suspend_never{}; } CoroFace get_return_object() { return {}; } auto final_suspend() noexcept { return std::suspend_never{}; } void return_void () { } }; }; CoroFace foo() { std::cout << "Hello Coroutine!\n"; co_return; } >>>>> "unhandled_exception" : Aslında bizim "coroutine" fonksiyonumuz derleyici tarafından "try-catch" bloğu içine alınır. İşte "catch-all" fonksiyonunda da bu fonksiyon çağrılır. İşte "Exception" mekanizmasının işletilebilmesi için bu fonksiyon da gereklidir. * Örnek 1, //... #include #include struct CoroFace{ //... struct promise_type { auto initial_suspend() { return std::suspend_never{}; } CoroFace get_return_object() { return {}; } auto final_suspend() noexcept { return std::suspend_never{}; } void return_void () { } void unhandled_exception() { } }; }; CoroFace foo() { std::cout << "Hello Coroutine!\n"; co_return; } >> Şu fonksiyonları "coroutine" fonksiyon olamazlar: >>> "main" fonksiyonu, >>> C dilindeki "variadic" fonksiyonlar, >>> Sınıfların "ctor" ve "dtor" fonksiyonları, >>> "constexpr" ve C++20 ile dile eklenen "consteval" anahtar sözcüğü ile nitelenen fonksiyonlar, >>> Geri dönüş değeri türü için çıkarım mekanizması kullanılamayacağından, o fonksiyonların geri dönüş değeri türü "auto" olamaz. >>> Bünyelerinde "regular return statemen" bulunduramazlar. Şimdi de bütün bunları kullanarak ilk "coroutine" örneğimizi yapalım: * Örnek 1.0, #include #include struct CoroFace{ //... struct promise_type { auto initial_suspend() { return std::suspend_never{}; } CoroFace get_return_object() { return {}; } auto final_suspend() noexcept { return std::suspend_never{}; } void return_void () { } void unhandled_exception() { } }; }; CoroFace foo() { std::cout << "Hello Coroutine!\n"; co_return; } int main() { foo(); // Hello Coroutine! } * Örnek 1.1, #include #include struct CoroFace{ //... struct promise_type { auto initial_suspend() { std::cout << "CoroFace::promise_type::initial_suspend()\n"; return std::suspend_never{}; } CoroFace get_return_object() { return {}; } auto final_suspend() noexcept { std::cout << "CoroFace::promise_type::final_suspend()\n"; return std::suspend_never{}; } void return_void () { } void unhandled_exception() { } }; }; CoroFace foo() { std::cout << "Hello Coroutine!\n"; co_return; } int main() { /* # OUTPUT # CoroFace::promise_type::initial_suspend() Hello Coroutine! CoroFace::promise_type::final_suspend() */ foo(); } * Örnek 2, #include #include struct CoroFace{ //... struct promise_type { auto initial_suspend() { std::cout << "CoroFace::promise_type::initial_suspend()\n"; return std::suspend_always{}; } CoroFace get_return_object() { return {}; } auto final_suspend() noexcept { std::cout << "CoroFace::promise_type::final_suspend()\n"; return std::suspend_never{}; } void return_void () { } void unhandled_exception() { } }; }; CoroFace foo() { std::cout << "Hello Coroutine!\n"; co_return; } int main() { foo(); // CoroFace::promise_type::initial_suspend() std::cout << "Main continues!\n"; // Main continues } * Örnek 3, #include #include struct CoroFace{ //... struct promise_type { auto initial_suspend() { std::cout << "CoroFace::promise_type::initial_suspend()\n"; return std::suspend_never{}; } CoroFace get_return_object() { return {}; } auto final_suspend() noexcept { std::cout << "CoroFace::promise_type::final_suspend()\n"; return std::suspend_never{}; } void return_void () { std::cout << "CoroFace::promise_type::return_void()\n"; } void unhandled_exception() { } }; }; CoroFace foo() { std::cout << "Hello Coroutine!\n"; co_return; } int main() { /* # OUTPUT # CoroFace::promise_type::initial_suspend() Hello Coroutine! CoroFace::promise_type::return_void() CoroFace::promise_type::final_suspend() */ foo(); } * Örnek 4, #include #include struct CoroFace{ //... struct promise_type { promise_type() { std::cout << "CoroFace::promise_type default ctor. \n"; } auto initial_suspend() { std::cout << "CoroFace::promise_type::initial_suspend()\n"; return std::suspend_never{}; } CoroFace get_return_object() { return {}; } auto final_suspend() noexcept { std::cout << "CoroFace::promise_type::final_suspend()\n"; return std::suspend_never{}; } void return_void () { std::cout << "CoroFace::promise_type::return_void()\n"; } void unhandled_exception() { } ~promise_type() { std::cout << "CoroFace::promise_type dtor\n"; } }; }; CoroFace foo() { std::cout << "Hello Coroutine!\n"; co_return; } int main() { /* # OUTPUT # CoroFace::promise_type default ctor. CoroFace::promise_type::initial_suspend() Hello Coroutine! CoroFace::promise_type::return_void() CoroFace::promise_type::final_suspend() CoroFace::promise_type dtor */ foo(); } * Örnek 5, #include #include void* operator new(std::size_t sz) { std::cout << "operator new called. size : " << sz << "\n"; if (sz == 0) ++sz; if (void* ptr = std::malloc(sz)) return ptr; throw std::bad_alloc{}; } void operator delete(void* ptr) noexcept { std::cout << "operator delete called. addresss : " << ptr << "\n"; std::free(ptr); } struct CoroFace{ //... struct promise_type { promise_type() { std::cout << "CoroFace::promise_type default ctor. \n"; } auto initial_suspend() { std::cout << "CoroFace::promise_type::initial_suspend()\n"; return std::suspend_never{}; } CoroFace get_return_object() { return {}; } auto final_suspend() noexcept { std::cout << "CoroFace::promise_type::final_suspend()\n"; return std::suspend_never{}; } void return_void () { std::cout << "CoroFace::promise_type::return_void()\n"; } void unhandled_exception() { } ~promise_type() { std::cout << "CoroFace::promise_type dtor\n"; } }; }; CoroFace foo() { std::cout << "Hello Coroutine!\n"; co_return; } int main() { /* # OUTPUT # operator new called. size : 40 CoroFace::promise_type default ctor. CoroFace::promise_type::initial_suspend() Hello Coroutine! CoroFace::promise_type::return_void() CoroFace::promise_type::final_suspend() CoroFace::promise_type dtor operator delete called. addresss : 0x5b3eb05826c0 */ foo(); } * Örnek 6, #include #include void* operator new(std::size_t sz) { std::cout << "operator new called. size : " << sz << "\n"; if (sz == 0) ++sz; if (void* ptr = std::malloc(sz)) return ptr; throw std::bad_alloc{}; } void operator delete(void* ptr) noexcept { std::cout << "operator delete called. addresss : " << ptr << "\n"; std::free(ptr); } struct CoroFace{ //... struct promise_type { promise_type() { std::cout << "CoroFace::promise_type default ctor. \n"; } auto initial_suspend() { std::cout << "CoroFace::promise_type::initial_suspend()\n"; return std::suspend_never{}; } CoroFace get_return_object() { return {}; } auto await_transform(int x) { std::cout << "CoroFace::promise_type::await_transform(" << x << ")\n"; return std::suspend_never{}; } auto final_suspend() noexcept { std::cout << "CoroFace::promise_type::final_suspend()\n"; return std::suspend_never{}; } void return_void () { std::cout << "CoroFace::promise_type::return_void()\n"; } void unhandled_exception() { } ~promise_type() { std::cout << "CoroFace::promise_type dtor\n"; } }; }; CoroFace foo() { std::cout << "Hello Coroutine!\n"; co_await 31; } int main() { /* # OUTPUT # operator new called. size : 40 CoroFace::promise_type default ctor. CoroFace::promise_type::initial_suspend() Hello Coroutine! CoroFace::promise_type::await_transform(31) CoroFace::promise_type::return_void() CoroFace::promise_type::final_suspend() CoroFace::promise_type dtor operator delete called. addresss : 0x624beff6f6c0 */ foo(); } * Örnek 7.1, #include #include struct SuspendNever{ bool await_ready() const noexcept { std::cout << "SuspendNever::await_ready()\n"; return true; } void await_suspend(std::coroutine_handle) const noexcept { std::cout << "SuspendNever::await_suspend()\n"; } void await_resume() const noexcept { std::cout << "SuspendNever::await_resume()\n"; } }; void* operator new(std::size_t sz) { std::cout << "operator new called. size : " << sz << "\n"; if (sz == 0) ++sz; if (void* ptr = std::malloc(sz)) return ptr; throw std::bad_alloc{}; } void operator delete(void* ptr) noexcept { std::cout << "operator delete called. addresss : " << ptr << "\n"; std::free(ptr); } struct CoroFace{ //... struct promise_type { promise_type() { std::cout << "CoroFace::promise_type default ctor. \n"; } auto initial_suspend() { std::cout << "CoroFace::promise_type::initial_suspend()\n"; return std::suspend_never{}; } CoroFace get_return_object() { return {}; } auto final_suspend() noexcept { std::cout << "CoroFace::promise_type::final_suspend()\n"; return std::suspend_never{}; } void return_void () { std::cout << "CoroFace::promise_type::return_void()\n"; } void unhandled_exception() { } ~promise_type() { std::cout << "CoroFace::promise_type dtor\n"; } }; }; CoroFace foo() { std::cout << "Hello Coroutine!\n"; co_await std::suspend_never{}; } int main() { /* # OUTPUT # operator new called. size : 40 CoroFace::promise_type default ctor. CoroFace::promise_type::initial_suspend() Hello Coroutine! SuspendNever::await_ready() SuspendNever::await_resume() CoroFace::promise_type::return_void() CoroFace::promise_type::final_suspend() CoroFace::promise_type dtor operator delete called. addresss : 0x61ded9f3c6c0 */ foo(); } * Örnek 7.2, #include #include struct SuspendAlways{ bool await_ready() const noexcept { std::cout << "SuspendAlways::await_ready()\n"; return false; } void await_suspend(std::coroutine_handle) const noexcept { std::cout << "SuspendAlways::await_suspend()\n"; } void await_resume() const noexcept { std::cout << "SuspendAlways::await_resume()\n"; } }; void* operator new(std::size_t sz) { std::cout << "operator new called. size : " << sz << "\n"; if (sz == 0) ++sz; if (void* ptr = std::malloc(sz)) return ptr; throw std::bad_alloc{}; } void operator delete(void* ptr) noexcept { std::cout << "operator delete called. addresss : " << ptr << "\n"; std::free(ptr); } struct CoroFace{ //... struct promise_type { promise_type() { std::cout << "CoroFace::promise_type default ctor. \n"; } auto initial_suspend() { std::cout << "CoroFace::promise_type::initial_suspend()\n"; return SuspendAlways{}; } CoroFace get_return_object() { return {}; } auto final_suspend() noexcept { std::cout << "CoroFace::promise_type::final_suspend()\n"; return SuspendAlways{}; } void return_void () { std::cout << "CoroFace::promise_type::return_void()\n"; } void unhandled_exception() { } ~promise_type() { std::cout << "CoroFace::promise_type dtor\n"; } }; }; CoroFace foo() { std::cout << "Hello Coroutine!\n"; co_await SuspendAlways{}; } int main() { /* # OUTPUT # operator new called. size : 40 CoroFace::promise_type default ctor. CoroFace::promise_type::initial_suspend() SuspendNever::await_ready() SuspendNever::await_suspend() */ foo(); } * Örnek 8, #include #include class CrtTask { public: struct promise_type; using CoroutineHandle = std::coroutine_handle; private: CoroutineHandle m_handle; public: CrtTask(auto h): m_handle{ h } {} ~CrtTask() { if (m_handle) { m_handle.destroy(); } } CrtTask(const CrtTask&) = delete; CrtTask& operator=(const CrtTask&) = delete; bool resume() const { if (!m_handle || m_handle.done()) { return false; } m_handle.resume(); return !m_handle.done(); } }; struct CrtTask::promise_type { CrtTask get_return_object() { return CrtTask{ CoroutineHandle::from_promise(*this) }; } auto initial_suspend() { return std::suspend_always{}; } void unhandled_exception() { std::terminate(); } void return_void() {} auto final_suspend() noexcept { return std::suspend_always{}; } }; CrtTask crfunc(int n) { std::cout << " crfunc " << n << " start\n"; for (int i{ 1 }; i <= n; ++i) { std::cout << " crfunc " << i << '/' << n << '\n'; co_await std::suspend_always(); } std::cout << " crfunc " << n << " end\n"; } int main() { auto ctask = crfunc(3); std::cout << "crfunc() started\n"; while (ctask.resume()) { std::cout << "crfunc() suspended\n"; } std::cout << "crfunc() done\n"; } * Örnek 9.0, #include #include class Coro{ public: struct promise_type; using handle_type = std::coroutine_handle; void resume() { h_.resume(); } bool done() const { return h_.done(); } struct promise_type { Coro get_return_object() { return Coro{ handle_type::from_promise(*this) }; } auto initial_suspend() { return std::suspend_never{}; } auto final_suspend() noexcept { return std::suspend_always{}; } void unhandled_exception() { } void return_void() { } }; private: Coro(handle_type h) : h_(h) {} handle_type h_; }; Coro foo() { std::cout << "foo [1]\n"; co_await std::suspend_always{}; std::cout << "foo [2]\n"; } int main() { /* # OUTPUT # foo [1] Main continues! */ auto f = foo(); std::cout << "Main continues!\n"; } * Örnek 9.1.0, #include #include class Coro{ public: struct promise_type; using handle_type = std::coroutine_handle; void resume() { h_.resume(); } bool done() const { return h_.done(); } struct promise_type { Coro get_return_object() { return Coro{ handle_type::from_promise(*this) }; } auto initial_suspend() { return std::suspend_never{}; } auto final_suspend() noexcept { return std::suspend_always{}; } void unhandled_exception() { } void return_void() { } }; private: Coro(handle_type h) : h_(h) {} handle_type h_; }; Coro foo() { std::cout << "foo [1]\n"; co_await std::suspend_always{}; std::cout << "foo [2]\n"; } int main() { /* # OUTPUT # foo [1] foo [2] Main continues! */ auto f = foo(); f.resume(); std::cout << "Main continues!\n"; } * Örnek 9.1.1, #include #include class Coro{ public: struct promise_type; using handle_type = std::coroutine_handle; void resume() { h_.resume(); } bool done() const { return h_.done(); } struct promise_type { Coro get_return_object() { return Coro{ handle_type::from_promise(*this) }; } auto initial_suspend() { return std::suspend_never{}; } auto final_suspend() noexcept { return std::suspend_always{}; } void unhandled_exception() { } void return_void() { } }; private: Coro(handle_type h) : h_(h) {} handle_type h_; }; Coro foo() { std::cout << "foo [1]\n"; co_await std::suspend_never{}; std::cout << "foo [2]\n"; } int main() { /* # OUTPUT # foo [1] foo [2] Main continues! */ auto f = foo(); std::cout << "Main continues!\n"; } * Örnek 10, #include #include class Coro{ public: struct promise_type; using handle_type = std::coroutine_handle; void resume() { h_.resume(); } bool done() const { return h_.done(); } struct promise_type { Coro get_return_object() { return Coro{ handle_type::from_promise(*this) }; } auto initial_suspend() { return std::suspend_always{}; } auto final_suspend() noexcept { return std::suspend_always{}; } void unhandled_exception() { } void return_void() { } }; private: Coro(handle_type h) : h_(h) {} handle_type h_; }; Coro foo() { std::cout << "foo [1]\n"; co_await std::suspend_always{}; // co_await std::suspend_never{}; std::cout << "foo [2]\n"; } int main() { /* # OUTPUT # Main continues! */ auto f = foo(); std::cout << "Main continues!\n"; } * Örnek 11, #include #include class Coro{ public: struct promise_type; using handle_type = std::coroutine_handle; void resume() { h_.resume(); } bool done() const { return h_.done(); } struct promise_type { Coro get_return_object() { return Coro{ handle_type::from_promise(*this) }; } auto initial_suspend() { return std::suspend_always{}; } auto final_suspend() noexcept { return std::suspend_always{}; } void unhandled_exception() { } void return_void() { } }; private: Coro(handle_type h) : h_(h) {} handle_type h_; }; Coro foo() { std::cout << "foo [1]\n"; co_await std::suspend_always{}; std::cout << "foo [2]\n"; } int main() { /* # OUTPUT # Main starts! [1] foo is NOT done yet. Main continues! [2] foo [1] foo is NOT done yet. Main continues! [3] foo [2] foo is DONE. main is DONE [4] */ std::cout << "Main starts! [1]\n"; auto f = foo(); if (!f.done()) std::cout << "foo is NOT done yet.\n"; else std::cout << "foo is DONE.\n"; std::cout << '\n'; std::cout << "Main continues! [2]\n"; f.resume(); if (!f.done()) std::cout << "foo is NOT done yet.\n"; else std::cout << "foo is DONE.\n"; std::cout << '\n'; std::cout << "Main continues! [3]\n"; f.resume(); if (!f.done()) std::cout << "foo is NOT done yet.\n"; else std::cout << "foo is DONE.\n"; std::cout << '\n'; std::cout << "main is DONE [4]\n"; } /*================================================================================================================================*/