Evet arkadaşlar, QT’ye ilişkin biraz aydınlandıktan sonra, haftalık C++ yazılarımıza, kısa bir kod parçası yazısı ile devam edelim (damarlara C++ zerk etmeye devam :). Bu yazımda C++ 17 ile birlikte gelen Satır Arası Değişkenleri (“inline variables”) kabiliyetinden bahsedeceğim. Bu kabiliyetin de kodunuzu daha okunaklı ve temiz hale getireceğini düşünüyorum açıkçası. Ayrıca sadece başlık dosyalarından oluşan kütüphane geliştiricileri de bu kabiliyeti çok sevecekler. Kabiliyete ilişkin öneriye kaynaklardan ulaşabilirsiniz.
Daha önceki yeni kabiliyet yazılarımda olduğu gibi, yine önce nasıldı, artık nasıl olduğunu sizler ile paylaşacağım.
Normalde (C++ 17 öncesinde), aşağıdaki kodu yazdığınız zaman derleme hatası alırdınız. Çünkü C++, statik olmayan ve const olmayan değişkenlerin bu şekilde tanımlanmasına izin vermiyordu.
1 2 3 4 5 6 |
class MyClass { private: static int mIntMember = 10; static std::string mStrMember = ""; }; |
Peki bu durumda ne yapıyorduk. Aşağıdaki gibi mi yapıyorduk?
1 2 3 4 5 6 7 8 |
class MyClass { private: static int mIntMember = 10; static std::string mStrMember = ""; }; MyClass::mIntMember = 10; MyClass::mStrMember = ""; |
Dikkatli okuyucularım, bu şekilde tanımlanıp, birden fazla dosyada bu başlık dosyası eklendiğinde, bağlama hatası alacağımızı hemen fark ederler. Neden? Çünkü her bir dosya bu tanımlamaları tekrar tekrar yapıyor. Tek tanımlama kuralı ihlal ediliyor (bu kurala yazımın sonunda tekrar değineceğim). Aynı şey aslında bu seviyede yapılan global değişken tanımları için de geçerli, bir diğer ifade ile, başlık dosyalarında tanımladığınız değişkenler, bütün eklenen dosyalar için kopyalanıyor, küçük sabitler için sıkıntı olmayabilir ama büyük sınıflarda nasıl bir sıkıntı olacağını eminim tahmin edersiniz ama o noktaya da geleceğiz, şu an konumuz o değil.
Bu sıkıntıyı gidermek için ya ilgili değişkeni aşağıdaki gibi “static const” olarak tanımlayabiliriz:
1 2 3 4 5 6 |
class MyClass { private: static const int mIntMember = 10; static const std::string mStrMember = ""; }; |
Ya da ilgili değişkenleri herhangi bir Cpp dosyası içerisinde aşağıdaki gibi tanımlamanız gerekmekteydi:
1 2 |
int ExampleClass::mIntMember; std::string ExampleClass::mStrMember; |
Bu durumda eğer tek başlık dosyalı bir kütüphaneniz olduğunu düşünün, bu değişkeni nereye koyacaksınız? Tek bir başlık dosyası olmasa bile bunun konumlandırılması, açıkçası hem okunabilirliği hem de idameyi zorlaştırıyor.
C++ 17 ile birlikte gelen “inline variable” kabiliyeti ile birlikte artık yukarıdaki statik üye değişkenleri aşağıdaki gibi ilklendirebilirsiniz.
1 2 3 4 5 6 |
class MyClass { private: static inline int mIntMember = 10; static inline std::string mStrMember = ""; }; |
Ta ta! Hatırlayacağınız üzere C++ 11 de statik olmayan sınıf değişkenleri için bu tarz ilklendirmeler yapabiliyorduk. Ayrıca C++ 17 ile birlikte constexpr da inline olarak kabul ediliyor. Yani aşağıdaki ifade C++ 17 ile birlikte artık bir tanımlama olarak kabul ediliyor, yukarıdaki örnek ile aynı (deklarasyon değil, çünkü C++ 11 ve C++ 14 de bu kullanım halen geçerli idi ama tanımlama anlamına gelmiyordu):
1 2 3 4 5 6 |
class MyClass { private: static contexpr int mIntMember = 10; static contexpr std::string mStrMember = “”; }; |
Aklınıza eminim bunların ne zaman ilklendirildiği sorusu da gelmiştir (gelmemiş ise de önemli bir mevzu). Bu kullanımlarda ilklendirme, ilk kullanım karşılaşıldığında gerçekleştirilmekte.
Buraya kadar anlattığım statik üye değişkenlerin ilklendirilmesinin yanında, “inline variables” ile birlikte, artık başlık dosyalarında tanımlanan global değişkenler de inline olarak tanımlanabilecek. Bu sayede birden fazla nesne oluşturulmasının da önüne geçebileceksiniz (C++ 17 den önce, yukarıda da bahsettiğim üzere bağlama hatası alıyorduk). Başka neyin önüne geçeceğiz? Aslında boşu boşuna daha fazla bellek tüketmenin ve bir çok kez yapıcı (ve sonda yıkıcı) çağrılmasının önüne geçeceğiz (yukarıdaki büyük nesnelerin global olarak tanımlanması mevzusu). Hemen bir örnek ile bu kullanımı da taçlandıralım. Bu arada her ne kadar bazı derleyiciler C++ 17 desteği sunsalar da, bu kabiliyet beklediğimiz gibi çalışmıyor olabilir. Mesela VS 2017′, her ne kadar bu kullanımlar geçerli olarak kabul edilse de, ne yazık ki, beklenen davranış sergilenmiyor, o sebeple lütfen dikkat.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <iostream> #include <string> // Bu baslik dosyasinin birden fazla kaynak dosyasi tarafindan dahil edildigini dusunu class ExampleClass { public: ExampleClass() { std::cout << "ExampleClass is instantiated!" << '\n'; } }; // Baglama hatası. Birden fazla tanımlı! ExampleClass gGlobalInstance; // C++ 17 ile birlikte, birden fazla dosya tarafından bu başlık dosyasi eklense bile sıkıntı yok // Küçük bir not: Visual Studio 2017 15.9.14’de halen farklı nesneler oluşturuluyor (yani yukarıdaki metni iki kere görmeniz muhtemel) // ama VS 2019 16.2’de olması gerektiği gibi tek nesne oluşturulmakta. inline ExampleClass gGlobalInstance; |
inline anahtar kelimesi ayrıca “thread_local” ile de kullanılabiliyor. Bu sayede her bir thread için eşsiz bir değişken oluşturabiliyorsunuz. Aşağıda tanımlanan değişkenlerin, davranışlarını farklı tanımlamalar ile görebilirsiniz:
1 2 3 4 5 6 7 8 9 10 11 12 |
struct ThreadData { // Program içerisinde eşsiz ama her thread içerisinde aynı inline static std::string mAppName = “global app name”; // Her bir thread içerisinde eşsiz inline static thread_local std::string mThreadName = “thead 1”; // Her bir ExampleThreadData nesnesinde geçerli std::string mLocalName = ”poor object”; ... }; |
Evet “inline variables”‘ın temel işlevlerine baktıktan sonra, şimdi bu konu ile ilintili başka bir konuya da kısaca değinip yazımı sonlandıracağım. Yazımın başında da bahsettiğim bağlama sıkıntısının neden olduğunu veya bunun neye dayandığını bilmek istersiniz diye düşünüyorum.
Bunun dayandığı kural ise Tek Tanım Kuralı’dır (“One Definition Rule”). Bu kural C++ 2003 standardında tanımlanmıştır ve bunun ihlali sonucu bu tarz hatalar alırsınız. Temelde bu kuralın söylediği şey metot ya da sınıfın aynı dosya ya da program içerisinde birden fazla tanımı olmayacağıdır (bulunduğu kapsama göre, bu durum değişkenler, sınıflar, yapılar (struct), fonksiyonlar için de geçerlidir). Zaten çoğu derleyici de bu durum karşısında, bu değişkenin birden fazla tanımlandığına dair size uyarı verecektir. Tabi şunu hatırlatmakta fayda var ki, C++ da deklarasyonlar için herhangi bir yer ayrılmaz, sadece ilgili sınıfa ilişkin nesne oluşturulduğunda yer alma işlemi gerçekleştirilir.
Aşağıda, bu durumu özetleyen basit bir figür görebilirsiniz:
Burada tanımlanan değişkenlerin kapsamı, extern olup/olmaması (harici bağlama yöntemi) gibi faktörleri de göz önünde bulundurmak gerekiyor. Mesela, extern (harici) olmayan farklı dosyadaki nesneler/tanımlamalar (dahili), aynı tip ve isme sahip olsalar dahi farklı olarak değerlendiriliyorlar ve bu sebeple de bağlama hatasına sebebiyet vermiyorlar.
Varsayılan olarak cpp dosyalarınızda tanımladığınız ve const olmayan değişkenlerin hepsi extern, yani harici bağlantısı var, olarak değerlendirilir. Bir diğer ifade ile başka dosyalarda da aynı isimle değişken olursa ODR ihlal edilmiş olur. Bunun non-extern olarak tanımlanması için static anahtar kelimesini kullanabilirsin, ayrıca const global değişkenler de bu şekilde değerlendirilebilir.
Aşağıda olası bütün tanımlamaların olduğu örnek kod parçasını görebilirsiniz:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// İlklendirilmemis tanımlamalar: int gX; // İlklendirilmemis global değişken tanımlar(harici bağlama) static int gX; // İlklendirilmemis statik degisken tanımlar(dahili bağlama) const int gX; // Derleme hatasi. Const değişkenler tanımlandıkları yerde ilklendirilmelidirler // <em>Extern</em> anahtar kelimesi ile on tanimlamalar (<em>Forward declaration</em>): extern int gZ; // Baska yerde tanimlanmis global degisken icin on tanımlama extern const int gZ; // Baska yerde tanimlanmis global <em>const</em> degisken icin on tanımlama // Ilklendirilmis tanimlamalar: int gY(1); // İlklendirilimis global degisken tanımlar (harici baglama) static int gY(1); // İlklendirilimis statik degisken tanımlar (dahili baglama) const int gY(1); // İlklendirilimis statik const degisken tanımlar (dahili baglama) // <em>extern</em> anahtar kelimesi ile ilklendirilmis tanimlamalar: extern int gW(1); // İlklendirilimis global degisken tanımlar (harici baglama) // (aslindaki yukaridaki tanımlama ile aynı anlama geliyor, extern anahtar kelimesi gereksiz, cunku varsayilian olarak zaten degiskenler <em>extern </em>olarak kabul ediliyor) extern const int gW(1); // İlklendirilimis statik const degisken tanımlar (harici baglama) |
Bu konu (değişken kapsamları, bağlama yöntemleri, ömrü) biraz daha su götürür ama bence ana fikri verdiğimi düşünüyorum. Merak edenler için bu konulara ilişkin de bir kaç kaynak ekliyorum, onlara da göz atarsanız faydalı olacaktır.
Evet bir yazımızın da daha sonuna gelmiş bulunuyoruz, bir sonraki yazımda görüşmek dileğiyle.
Kaynaklar
- https://www.codingame.com/playgrounds/2205/7-features-of-c17-that-will-simplify-your-code/inline-variables
- https://cplusplus.programmingpedia.net/en/tutorial/4907/one-definition-rule-odr-
- https://www.wikiwand.com/en/One_Definition_Rule
- https://wg21.link/p0386r2
- https://en.cppreference.com/w/cpp/language/language_linkage
- https://www.youtube.com/watch?v=MW3GMqVZNDg
- https://en.cppreference.com/w/cpp/language/inline
- http://www.drdobbs.com/c-theory-and-practice/184403437
- https://www.wikiwand.com/en/ISO/IEC_14882
- https://en.cppreference.com/w/cpp/language/data_members
Harika bir kaynak olmuş elinize sağlık.
Faydalı olduysa ne ala, çok teşekkürler 😊 Bol kodlu günler diliyorum.