Bir diğer Modern C++ yazım ile sizlerle birlikteyim 🙂 Önceki Modern C++ yazılarıma aşağıdaki bağlantılardan ulaşabilirsiniz.
Modern C++ (1): nullptr, enum sınıfları, range-based döngüler, auto
Modern C++ (2): C++ 11 Yenilikleri
Bu yazımızda C++ 11 ile gelen yenilikleri incelemeye devam edeceğiz. C++11 ile gelen bütün yenilikler için yukarıdaki ikinci yazıma ulaşabilirsiniz. Bu yazımda diğer yenilikler kadar büyük değişiklikler olmasa da genel ilklendirme ve tanımlama yaklaşımlarını değiştirdiği için öğrenilmesinde fayda olduğunu düşündüğüm bir takım yapılardan bahsedeceğim. Yazımın sonuna doğru ise daha ufak çapta olan değişikliklerden bahsedecğim.
İçerik
1. Initialization Lists/Uniform Initialization
C++ 11 ile gelen en önemli özelliklerden birisi de veri yapıları/tiplerinin ortak bir şekilde ilklendirilmesine yönelik sunulan “Uniform Initialization” olarak adlandırılan mekanizmadır. Peki C++ 03’de durum neydi? Şimdi hızlıca daha önceki duruma ve sıkıntılara bakalım, bu anlamda üç temel eksiklik bulunmaktaydı bunlar:
- Sınıf üyesi dizilerin ilklendirilememesi,
- Konteynerların (STL konteynerleri dahil) ilklendirilmesine ilişkin standart bir yolun olmaması,
- Dinamik olarak oluşturulan POD veri yapılarının ilklendirilememesi.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class C { int x[100]; C(); // x i el ile ilklendirme dışında standart bir yöntem yok }; // buff dizinin elemanlarını ilklendirmenin bir yöntemi yok char *buff=new char[1024]; // STL konteynerlerin standart bir ilklendirme yöntemi yok vector <string> colors; colors.push_back("Beyaz"); colors.push_back("Siyah"); colors.push_back("Mavi"); |
Bunlarında yanında C++ 03 çok farklı ve ortak olmayan ilklendirme yöntemleri içeriyordu (temel tiplerin ilklendirilmesi, sınıf ve nesnelerin “data member” larının ilklendirilmesi, dizi ve karmaşık veri yapıların ilklendirilmesi). Şimdi hızlıca bunlara bakalım:
- Basit veri yapılarının ilklendirilmesi için (=) operatörü kullanılmakta:
1 2 3 4 5 6 7 |
unsigned int n = 0U; void *p = 0; char c = 'A'; // Ayrıca () operatörü de kullanılabilir unsigned int n(0U); // Yukarıdaki ile aynı double dbl(0.5); |
- Constructor ları tanımlanmış sınıflar için constructor’lar aracılığı ile member initialization list, nesnelerin ilklendirilmesi için ise ” ( ) ” operatörü kullanılmaktaydı.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
//C++03 initialization of classes and objects struct ExampleStruct { explicit ExampleStruct(int n, int m) : x(n), y(m){} private: int x, y; }; // Nesne ilklendirilmesi için parantezler kullanılmakta ExampleStruct s(0,1); // Derleme hatası (özel olarak constructor tanımlandığı için hata alınmakta) // error: converting to 'ExampleStruct' from initializer list would use explicit constructor 'ExampleStruct::ExampleStruct(int, int)' ExampleStruct s2={0, 1}; // Derleme hatası // error: conversion from 'int' to non-scalar type 'ExampleStruct' requested ExampleStruct s2=(0, 1); |
- POD diziler ve karmaşık veri yapıları için ise ” { } ” kullanılmaktaydı.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
//C++03: dizi ilklendirilmesi int c1[2]={0,2}; // Char dizisine özel ilklendirme char c2[]="message"; // Char dizisinin standart bir şekilde ilklendirilmesi char c3[]={'m','e','s','s','a','g','e','\0'}; struct S { int a,b; }; // Karmaşık veri yapısının ilklendirilmesi // özel olarak constructor tanımlanMAdığı için hata alınMaMakta S s={0,1}; |
Evet gelelim C++ 11 ile gelen yeni “Uniform Initialization”‘a. C++ 11 yeni ilklendirme yöntemi ile ile her bir veri tipinin (POD değişkeni, kullanıcı tanım constructor içeren sınıflar, POD dizi, dinamik olarak oluşturulan dizi ve nihayetinde STL konteynerları) ilklendirilmesi için ortak bir yaklaşım sunmaktadır. Bu yaklaşım ” { } ” ilklendirmesi olarak ta ifade edilebiliyor ve ilklendirme için bu kullanılıyor.
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 |
// C++11 parantez-ilklendirme örnekleri int a{0}; string s{"hello"}; // Copy constructor için de kullanılabilir string s2{s}; vector <string> colors{"Beyaz", "Mavi", "Siyah"}; map<string, string> team { {"Ahmet", "Yaş:26"}, {"Mehmet", "Yaş:28"}}; double* pd= new double [3] {0.5, 1.2, 12.99}; class C { int x[4]; public: // Üye dizi ilklendirmesi C(): x{0,1,2,3} {} }; // C++11 varsayılan ilklendirme örnekleri int n{}; // sıfır ile ilklendirme n 'in değeri 0 int *p{}; // p ye nullptr atanır double d{}; // d 0.0 ile ilklendirilir char s[12]{}; // bütün 12 karakter de '\0' olarak ilklendirilir string s{}; // string s; tanımı ile aynı (default constructor) char *p=new char [5]{}; // bütün 5 karakter de '\0' olarak ilklendirilir |
Yukarıdaki örneklerde “={}” yerine “{}” kullanımına dikkat edecek olursanız, ilki C++03 de kullanıllan yaklaşım. Bunun yanında “{}” boş parantez kullanımı da varsayılan ilklendirme anlamına gelmektedir (POD ve basit veri tipleri için 0 ile ilklendirme, sınıflar ve karmaşık veri yapıları için default constructor).
Bunlar ile birlikte kendi sınıflarınıza ve fonksiyonlarınıza da özellikle STL konteynerlerinin ilklendirilmesi benzer bir ilklendirme kazandırmak için C++ std::initializer_list template sınıfını sunuyor. Bunun sayesinde sınıflarınızı ilklendirme listeleri ile oluşturabilirsiniz. Aşağıda örnek bir sayı liste sınıfını bu şekilde nasıl ilklendirebileceğimize bakalım.
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 |
#include <iostream> #include <vector> class CNumberList { public: CNumberList(const <strong>std::initializer_list</strong><int> &v) { for (auto itm : v) { mVec.push_back(itm); } } void print() { for (auto itm : mVec) { std::cout << itm << " "; } } private: std::vector<int> mVec; }; int main() { CNumberList m = { 1, 2, 3, 4 }; m.print(); // 1 2 3 4 return 0; } |
2. Override ve Final anahtar kelimeleri
Özellikle C++ “inheritance” mekanizması için bu anahtar kelime eklenmiştir. Bu anahtar kelime derleyiciye bunun eklendiği metodun bir üst sınıfta virtual olarak aynı şekilde tanımlanıp/tanımlanmadığını kontrol etmesini sağlar. C++ 03 ve önceki C++ larda aşağıdaki gibi durumlar kolay bir şekilde gözden kaçabilmekteydi.
1 2 3 4 5 6 7 8 9 10 11 |
// C++03 Hali class Base { virtual void f(int); }; class Derived : public Base { // Derleyici bu tanımlamaya hata vermez ve bunun yeni bir tanımlama olduğunu kabul eder virtual void f(float); }; |
Burada aslında amaç bir üst sınıfta tanımlanmış olan f metodunu tanımlamaktı ama metodun tanımı farklı olduğu için ikinci bir sanal metot tanımlandı. Aşağıda ise yeni kullanım var ve bu durumda derleyici hata vercektir.
1 2 3 4 5 6 7 8 9 10 11 |
// C++11 hali class Base { virtual void f(int); }; class Derived : public Base { // derleyici bir üst sınfa bakacak ve benzer bir tanım göremeyeceği için hata verecek virtual void f(float) <strong>override</strong>; }; |
“Inheritance” ile ilgili eklenen bir diğer anahtar kelime ise “final” dır. Bu anahtar kelime ile işaretlenen sınıf ve metotlar alt sınıflar tarafından tekrar tanımlanmasının önüne geçiyor. Bazı durumlar diğer sınıfların geliştirdiğimiz sınıfları inheritance üzerinden değiştirmeleri istemeyebiliriz (kripto, sisteml servisleri, vb.). Normal şartlarda aslında vector ve list benzeri sınıflar da bu tarz sınıflar içerisine dahil edilebilir. İşte bu amaçla aşağıdaki gibi sınıflarınız tanımlayıp bunların diğer sınıflar tarafından inheritance yolu ile değiştirilmesinin önüne geçebilirsiniz. Benzer şekilde metotlar da bu şekilde tanımlanarak, bu metotların diğer sınıflar tarafından geçersiz kılınmasının (override) önüne geçilebilir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// Hiç bir sınıf A sınıfından inheritance ile türetilemez class A <strong>final</strong> { virtual void f(int); }; class B { virtual void f(); }; class C : public B { // Hiç bir sınıf f() metodunu geçersiz kılamaz/tekrar tanımlayamaz virtual void f() override <strong>final</strong>; }; class D : public B { // Derleyici hatası. C sınıfı içerisinde bu metot final olarak işaretlenmiş virtual void f() override; }; |
3. Default ve Delete anahtar kelimeleri
C++ 11 öncesinde herhangi bir sınıf içerisinde eğer bir “constructor” tanımlar iseniz, derleyici sizin için “default constructor” tanımlamaz ve aşağıdaki durumda derleme hatası alırsınız.
1 2 3 4 5 6 7 8 |
class A { public: A(int a){}; }; // Derleme hatası A a; |
İşte “default” anahtar kelimesi ekleyerek derleyicinin bizim için “default constructor” oluşturmaya zorlayabiliriz.
1 2 3 4 5 6 7 8 9 |
class A { public: A(int a){}; A() = <strong>default</strong>; }; // Derleme hatası almayız A a; |
“delete” anahtar kelimesi ile de metotların tanımlanması ve çağrılmasını engelleyebiliriz. Örneğin:
1 2 3 4 5 6 7 8 9 10 |
class A { public: A(int a){}; }; // Yukarıdaki tanımlama ile aşağıdaki ifadelerin hepsi geçerlidir A a(10); // OK A b(3.14); // OK 3.14, 3'e dönüştürülür (bunu istemeyebiliriz) a = b; // OK Derleyeci tarafindan otomatik oluşturukan atama operatoru cağrılır |
Yukarıdaki örneğin ikinci ve üçüncü ifadeleri engellemek isteyebiliriz. Aşağıdaki tanımlama ile buun önüne geçmiş oluyoruz.
1 2 3 4 5 6 7 8 9 |
class A { public: A(int a){}; A(short) = delete; // Short ile oluşturulmasının önüne geçelim A(double) = delete; // Otomatik dönüşüm engellendi A(const A&) = delete; // Copy contructor engellendi A& operator=(const A&) = delete; // Atama operatoru engellendi }; |
4. constexpr
constexpr anahtar kelimesi derleyici tarafından derleme zamanında hesaplama yapılması için eklenmiş olan bir anahtar kelimedir. constexpr ile sadece değişkenler değil aynı zamanda metotlar ve dahi sınıf metotları da derleme zamanında hesaplanacak diye belirtilebilmektedir. Bu anlamda nesneler ile kullanımı const, metotlar ile kullanımı da aslında inline kullanımına benzerdir. Burada yine Uniform Initialization gibi tek bir yöntem kullanılması amacı da güdülmüştür.
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 |
constexpr int square(int x) { return x * x; } int square2(int x) { return x * x; } // square(2) derleme zamanında hesaplanır int a = square(2); // çalışma zamanında ilgili metot çağrılır int b = square2(2); const int x = 123; // Hata -- 'y' constexpr değişkeni bir constexpr ile ilklendirilmelidir constexpr const int& y = x; // Sınıflar için kullanım örneği class Complex { public: constexpr Complex(double r, double i) : re(r), im(i) { } constexpr double real() { return re; } constexpr double imag() { return im; } private: double re; double im; }; constexpr Complex I(0, 1); |
constexpr ile tanımlanacak değişkenler aşağıdaki koşulları karşılamalıdır:
- LiteralType olmalıdır,
- Hemen oluşturulmalı ya da bir değer atanmalıdır,
- Constructor’a ya da ilgili değişkene atılacak değerler sadece literal değerleri, constexpr değişkenler ve metotlar olabilir,
- Nesne oluşturulmasında kullanılacak olan constructor, constexpr constructor gereksinimlerini karşılamalıdır. Explicit constructor olması durumunda constexpr olarak tanımlanmalıdır.
constexpr ile tanımlanacak olan metotlar aşağıdaki koşulları karşılamalıdır:
- virtual olmamalıdır,
- Dönüş değeri ve var ise parametreleri LiteralType olmalıdır,
- Fonksiyon tanımı ya deleted/default olarak işaretlenmeli ya da sadece aşağıdakileri içermelidir,
- null ifadeleri,
- static_assert tanımlamaları,
- class veya enum dışındaki typedef ya da alias tanımlamaları,
- using tanımlamaları ve komutları,
- tek bir return kullanımı
- Fonksiyon tanımı aşağıdakileri içermemelidir
- asm tanımlaması,
- goto ifadeleri,
- try-catch bloğu,
- Literal olmayan, statik veya thread veri tanımlamalar.
constexpr ile tanımlanacak olan constructorlar aşağıdaki koşulları karşılamalıdır:
- Parametreleri LiteralType olmalıdır,
- Virtual base sınıflar olmamalıdır,
- try-catch bloğu olmamalıdır,
- Constructor tanımı ya deleted/default olarak işaretlenmeli ya da sadece aşağıdakileri içermelidir,
- null ifadeleri,
- static_assert tanımlamaları,
- class veya enum dışındaki typedef ya da alias tanımlamaları,
- using tanımlamaları ve komutları,
- Bütün base sınıflar ve statik olmayan üyeler ilklendirilmelidir. Kullanılan bütün constructorlar da constexpr olmalıdır.
Daha detaylı koşullar için ConstExpr sayfasına başvurabilirsiniz.
5. Statik olmayan sınıf verilerinin ilklendirilmesi
C++ 11 ile artık statik olmayan sınıf üyeleri deklare edildikleri yerde ilklendirilebilecekler. Bu sayede daha temiz (construtor küçülecek) ve okunabilir kodlar elde edebileceksiniz.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// C++11 sınıf üye verilerinin ilklendirilmesi class Human { Human() : age(), name("Ahmet"), ptr(0) {} private: unsigned age; std::string name; int* ptr; }; // C++ 11 yeni ilklendirme yaklaşımı. Daha az ve okunabilir kod class Human { private: unsigned age{0}; std::string name = "Ahmet"; int* ptr = nullptr; }; |
6. Sağ Template Ayraç Boşlukları
C++ 11 ile artık typedef tanımlamalarında garip boşluklar bırakmanıza gerek kalmıyor.
1 2 3 |
// aralarındaki boşlukları silerseniz derleme hatası alırsınız :) typedef std::map<int, std::map <int, std::map <int, int> > > cpp98LongTypedef; typedef std::map<int, std::map <int, std::map <int, int>>> cpp11LongTypedef; |
7. Inline Namespaces
C++ 11 ile eklenen bir diğer özellikte inline namespaces. Inline namespaces temelde size varsayılan olarak bir namespace içerisindeki tanımlamaların otomatik olarak bir üst namespace ile sunulmaısna olanak sağlıyor. Bu da bazı kabiliyetlerin özelleşmesi ve versiyonlama işini kolaylaştırıyor. Örneğin:
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 |
namespace Networking { namespace V1 { class TCPSocket; } // Inline tanımlaması ile birlikte Networking namespaceini kullanan herkes aynı // zamanda V2 ile gelen tanımlamalara da ulaşabiliyor olacak. <strong>inline</strong> namespace V2 { class TCPSocket; } class UDPSocket; } // Özellikle V1/V2 belirtilmediği için Networking::V2::TCPSocket kullanılacak Networking::TCPSocket *t; // Özellikle belirtildiği için Networking::V1::TCPSocket kullanılacak Networking::V1::TCPSocket *t2; // Özellikle belirtildiği için Networking::V2::TCPSocket kullanılacak Networking::V2::TCPSocket *t3; using namespace Networking; // Networking::V2::TCPSocket TCPSocket *t4; // Networking::V1::TCPSocket V1::TCPSocket *t5; // Networking::V2::TCPSocket V2::TCPSocket *t6; |
Özellikle framework ve kütüphane geliştiricilerin hangi namespacelerin otomatik olarak varsayılan işaretlenmesine olanak sağlayacak bir mekanizma. Ağer bu tarz bir yazılım geliştirmiyor iseniz çok kullanma ihtiyacı duymayabilirsiniz.
8. Delegating Constructors
C++ 11 ile birlikte artık ilklendirme listesi içerisinde aynı sınıfa ait diğer constructorları çağırabilirsiniz. Bu sayede yine kod azaltılarak okunabilirlik arttırılabilir. foo.foo; // == 0
1 2 3 4 5 6 7 8 9 10 |
struct Foo { int foo; Foo(int foo) : foo(foo) {} // Bir daha bu constructor içerisinde foo'yu doldurmamıza gerek yok Foo() : <strong>Foo(0)</strong> {} }; Foo foo{}; foo.foo; // == 0 |
9. static_assert
Bu yazımda sizlere bahsedeceğim son kabiliyet ise static_assert. static_assert ile derleme zamanında kontroller yapabilirsiniz.
1 2 3 4 5 6 7 8 9 10 11 |
#include <assert.h> int main(void) { static_assert(2 + 2 == 4, "2+2 isn't 4"); static_assert(sizeof(int) < sizeof(char), "this program requires that int is less than char"); constexpr int x = 0; constexpr int y = 1; static_assert(x == y, "x != y"); } |
Evet uzun bir yazının daha sonuna geldik. Bu yazı ile birlikte bir kaç konu dışında C++ 11 yeniliklerinin de bir çoğunun üzerinden geçmiş olduk, geriye “Smart Pointer”, “Lambda Expressions” ve “R-Value References” gibi baba konular kaldı. Bir de tabi STL’e gelen yenilikler var.
Muhtemelen bir sonraki yazımı da “Smart Pointer” ‘lara bakıyor olacağız.
Görüşmek dileğiyle…