Çok eski bir web tarayıcısı kullanıyorsunuz. Bu veya diğer siteleri görüntülemekte sorunlar yaşayabilirsiniz.. Tarayıcınızı güncellemeli veya alternatif bir tarayıcı kullanmalısınız.
Bu başlık altında konu dışına çıkmadan, YMIR’in yaklaşık 20 yıl önce yazdığı ve günümüz standartlarına göre oldukça problemli olan kod bloklarını refaktör edip, ortaya çıkan sonuçları birbirimizle paylaşalım.
YMIR bu oyunu geliştirmeye başladığında, Visual Studio’da bugün bizim için son derece temel olan bazı kavramlar bile yoktu. Örneğin false ve true gibi değerler dahi makro olarak elle tanımlanmış:
Biz bu antik kod tabanına mahkum olmayalım ve modern C++26 özelliklerini, güncel yazılım prensiplerini ve daha sağlıklı kod pratiklerini kullanarak altyapımızı ciddi anlamda iyileştirelim.
Amacım bu başlıkta:
Eski, problemli veya gereksiz kodları tespit etmek
Daha temiz, okunabilir ve modern hallerini paylaşmak
Birbirimizden öğrenerek ortak bir bilgi havuzu oluşturmak
Kod Refaktörizasyonu Yapmanın Başlıca Sebepleri:
Kodun okunabilirliğini artırır
Karmaşıklığı azaltır
Kaynak kodun bakımını kolaylaştırır
Sistemin genişletilebilirliğini artırır
Performans iyileştirmeleri sağlar
Daha stabil ve hızlı çalışan bir yazılım ortaya çıkarır
Herkesin katkı sunabileceği, örneklerle zenginleşen bir başlık olmasını istiyorum.
Lütfen paylaşımlarınızı önce-sonra kod örnekleri ile izahlı yapmaya özen gösterin.
int iAlignIndex;
if (GetRealAlignment() >= 120000)
{
iAlignIndex = 0;
}
else if (GetRealAlignment() >= 80000)
{
iAlignIndex = 1;
}
else if (GetRealAlignment() >= 40000)
{
iAlignIndex = 2;
}
else if (GetRealAlignment() >= 10000)
{
iAlignIndex = 3;
}
else if (GetRealAlignment() >= 0)
{
iAlignIndex = 4;
}
else if (GetRealAlignment() > -40000)
{
iAlignIndex = 5;
}
else if (GetRealAlignment() > -80000)
{
iAlignIndex = 6;
}
else if (GetRealAlignment() > -120000)
{
iAlignIndex = 7;
}
else
{
iAlignIndex = 8;
}
Refaktör edilmeli, çünkü;
C++:
GetRealAlignment()
Fonksiyonu dokuz kere çağırılıyor, ayrıca denetlemesi zor, bozması kolay.
Bu kod bloğunu refaktör edip şu şekilde yazarak iyileştirebiliriz:
Server\Source\game\char_battle.cpp:
Arayın:
void CHARACTER::ItemDropPenalty(LPCHARACTER pkKiller)
Üzerine yardımcı fonksiyonu ekleyin:
static constexpr int GetAlignIndex(int align)
{
if (align >= 120000) return 0;
if (align >= 80000) return 1;
if (align >= 40000) return 2;
if (align >= 10000) return 3;
if (align >= 0) return 4;
if (align > -40000) return 5;
if (align > -80000) return 6;
if (align > -120000) return 7;
return 8;
}
Şimdi ise o koca kod bloğu yerine sadece bunu yapıştırın:
int iAlignIndex = GetAlignIndex(GetRealAlignment());
Bu Refaktörün Sağladığı Faydalar:
Tekrarlı fonksiyon çağrıları ortadan kaldırıldı. GetRealAlignment() fonksiyonu artık yalnızca bir kez çağrılıyor.
Kod okunabilirliği ciddi şekilde arttı. Uzun ve karmaşık if / else zinciri yerine, amacı net bir yardımcı fonksiyon kullanıldı.
Bakımı ve değiştirilmesi kolaylaştı. Alignment aralıkları tek bir yerde tanımlı. Değerler değişirse yalnızca bu fonksiyon güncellenir.
Hata yapma riski azaltıldı. Çok sayıda koşullu blok yerine, merkezi ve kontrollü bir mantık kullanıldı.
Modern C++ yaklaşımına daha uygun. constexpr kullanımı sayesinde derleyici optimizasyonlarına açık, niyeti net bir kod elde edildi.
Davranış değişmeden iyileştirme yapıldı. Mevcut oyun mantığı korunarak sadece kod kalitesi artırıldı.
Bu bölümde etc_drop_item.txt dosyasını ve okuma mantığını refaktör edelim.
Eğer Koreli bir YMIR geliştiricisi değilseniz bu dosyayı okumak zor.
Server\Binary\share\locale\xx\etc_drop_item.txt:
흰색 댕기+ 2.0
흰색 댕기 1.0
화장품 4.0
호랑이발톱 2.0
호랑이가죽 4.0
헝겊조각+ 2.0
헝겊조각 2.0
풀잎 4.0
표창+ 1.0
표창 1.0
투지범의 이빨 4.0
전갈의 집게발 2.0
전갈의 독침 4.0
전갈의 꼬리+ 2.0
전갈의 꼬리 4.0
저주의 서+ 4.0
저주의 서 4.0
웅귀의 어금니+ 2.0
웅귀의 어금니 1.0
웅귀의 부적+ 2.0
웅귀의 부적 1.0
오랑케의 전리품 2.0
얼음조각+ 2.0
얼음조각 4.0
얼음뿔범고래의 뿔 2.0
얼음구슬+ 4.0
얼음구슬 3.0
알 수 없는 약+ 2.0
알 수 없는 약 4.0
알 수 없는 부적+ 5.0
알 수 없는 부적 5.0
실타래 4.0
설인의 털+ 2.0
설인의 털 4.0
사귀의 유품+ 4.0
사귀의 유품 4.0
사귀의 보석+ 2.0
사귀의 보석 4.0
비녀 5.0
붉은 댕기 2.0
불타는 갈퀴 2.6
뱀의 꼬리 5.0
뱀가죽 2.6
백호가죽 2.0
밀교입문서+ 6.6
밀교입문서 5.0
무인의 증표 4.0
멧돼지의 어금니 3.0
돼지코 2.0
늑대털+ 3.0
늑대털 3.0
늑대발톱+ 5.0
늑대발톱 3.0
녹슨 단검조각 2.6
노리개 1.0
낡은 흑색도복+ 0.8
낡은 흑색도복 0.8
깨진 사기그릇 0.6
깨진 보석조각 1.3
깨진 갑옷조각 1.3
깃발 2.0
곰의 쓸개+ 2.0
곰의 쓸개 2.5
곰발바닥+ 2.0
곰발바닥 2.5
거미줄 2.0
거미의 독주머니 5.0
거미의 눈 5.0
거미알집 4.0
거미다리 4.0
개구리혓바닥 4.0
개구리다리 2.0
Biz bu dosyayı okuyan fonksiyonu refaktör edip, işlem yükü olarak daha ağır olan item adını protoda arayıp Vnum'u elde etme mantığını bırakacağız, bunun yerine dosyay doğrudan Vnum yazacağız, ve Vnum'un hangi iteme tekabül ettiğini anlayabilmek için de yorum satırı desteği ekleyeceğiz.
Korece konuşmayan bize bir faydası olmadığı gibi hem kod kalabalığı yapıyor hem de encoding bozulması gibi sebeplerle sık sık geliştiricilere sorun çıkarıyor. Bence kaldırmak en doğrusu. Özetle proto'lardaki Korece isim / Lokal isim ikilemini kaldırıp tek sütun kullanacağız.
fonksiyonlarını refaktör etmenin iyi olacağını düşündüm.
fonksiyonlarını refaktör etmenin iyi olacağını düşündüm.
Eski hâllerinde bu iki fonksiyon şu problemleri taşıyordu:
(mob/item)_proto_test.txt için ayrı bir “override” mantığı vardı: ekstra dosya okuma, ekstra tablo üretme, ekstra map ve set yönetimi.
(mob/item)_names.txt için map<int, const char*> gibi sahipliği belirsiz pointer map’i kullanılıyordu.
Aynı proto dosyaları birden fazla kez okunup addNumber gibi ek hesaplarla gereksiz karmaşa yaratıyordu.
new[], memset, iterator/itertype gibi yapılarla hem okunması zor, hem bozması kolay, hem de riskli bir akış vardı.
Bu refaktörde yapılanlar (özet):
item_proto_test.txt ve mob_proto_test.txt mantıkları tamamen kaldırıldı.
item_names.txt / mob_names.txt tarafı modernize edildi:
atoi yerine str_to_number ile daha güvenli parse,
std::unordered_map<int, std::string_view> ile daha net lookup.
Proto dosyaları tek bir akışla okunur hâle getirildi:
Dosya aç → header atla → reserve → emplace_back ile parse → logla → vnum’a göre sırala.
memset ve gereksiz default initialization’lar kaldırıldı.
resize + index yerine reserve + emplace_back modeli kullanıldı.
Sonuç olarak kod hem kısaldı hem de container kullanımı açısından daha doğru bir yapıya kavuştu.
“Test override” kaynaklı karmaşa tamamen ortadan kalktı.
Bu refaktörün sağladığı faydalar:
Dosya okuma/parsing akışı tek hâle geldi → denetlemesi kolay
Gereksiz ctor / assignment / reallocation yok → daha temiz ve ölçeklenebilir
test_map / vnumSet / addNumber gibi yan yapılar yok → karmaşa azaldı
Re-load senaryosu daha temiz → tablolar kontrollü şekilde yeniden kuruluyor
Modern C++ yaklaşımına daha uygun → unordered_map, reserve + emplace_back, net object lifetime
Fonksiyonları bu şekilde refaktör ettim.
Aynı yaklaşım dump_proto tarafına da uygulanarak server / client / tool zinciri tutarlı hâle getirildi.
amacım konunuzu baltalamak değil lütfen yanlış anlamayın, hatta zaman ayırabilirsem katkı sağlamak isterim, mantıklı bir başlık. söylediklerim sadece genel bilgilendirme. kullandığınız C++26 değil ki,
burda mesela vector'un default ctorunu çağırmışsınız, bu işlem zaten "rowCount" değeri kadar "TClientItemTable" structını default ctor ile ramde oluşturur.
"fill" algoritması ise default ctor ile bir adet nesneyi hayata getirir ve her elemana bu nesneyi kopyalar (yani örneğin rowCount değeri 1k olsun bu 1k kez kopya oluşturmak demek, gereksiz bir maliyet) yani;
1-> "std::vector" default ctor ile x tane "TClientItemTable" oluşturdu
2-> std::ranges::fill, bir kez default ctor "rowCount" kez copy assign operator çağrıldı
niyete göre "reserve" edilip eleman pushlanabilir, assign benzeri member funclar kullanılabilir vesaire, ama sizin kullanımız pek doğru değil.
yani bu iki işlemde aslında siz rowCount +1 kez default ctor ve rowCount kadar kopya oluşturdunuz
C++:
const std::string_view name = it->second;
strlcpy(itemTable->szName, name.data(), sizeof(itemTable->szName));
burda bir UB kokusu var ama csvparser ne yapıyor bir fikrim yok bu yüzden net bir şey söyleyemiyorum, null string'den kaynaklı bir sorun meydana gelebilir(ihtimal, kodu bilmiyorum. string_view bir nesnenin başlangıç adresini ve sizeını tutar, genelde karşılaşılacak sorun lifetime ve null stringdir).
diğer kodlarınızda patch olarak nitelendirilebilir, örneğin "GetAlignIndex"
gerçek bir modernizasyonda sanırsam şöyle bir tablo olabilirdi(işte branchless yapılabilirdi, o olabilirdi bu olabilirdi, mikro optimizasyon konuşmuyorum(derleyiciden akıllı değiliz, erken optimizasyon optimizasyon değildir), ilk aklıma gelen yöntem bu "modernize" etmek açısından, derleyici optimizasyonuyla assembly düzeyde büyük ihtimalle aynıya yakın kod üretilir, kontrol etmek gerek);
(yanlış hatırlamıyorsam C++20'de constexpr fonksiyonların içinde static tanımlayamıyorduk, bu kod C++ sürümünüze göre derlenmeyebilir ama 23'de legal)
C++:
[[nodiscard("please use the align index ret value")]] static constexpr std::uint8_t GetAlignIndex(int align) noexcept
{
static constexpr std::array align_arr_idx_table{
120'000, //0
80'000, // 1
40'000, //2
10'000, //3
0, // 4
-40'000, //5
-80'000, //6
-120'000, // 7
};
const auto iter{std::ranges::lower_bound(align_arr_idx_table, align, std::ranges::greater{})};
if (iter == align_arr_idx_table.cend())
{
return align_arr_idx_table.size();
}
return static_cast<std::uint8_t>(std::ranges::distance(align_arr_idx_table.begin(), iter));
}
çok uzatmadan; C++26'dan önce C++20 ve 23'e odaklanmanızı tavsiye edebilirim, en basitinden bu txt'leri run time maliyetinden kurtararak tamamen compile timeda okumanız bile mümkün, yaptığınız o zaman refactor olabilir naçizane, emeğinize sağlık.
amacım konunuzu baltalamak değil lütfen yanlış anlamayın, hatta zaman ayırabilirsem katkı sağlamak isterim, mantıklı bir başlık. söylediklerim sadece genel bilgilendirme. kullandığınız C++26 değil ki,
burda mesela vector'un default ctorunu çağırmışsınız, bu işlem zaten "rowCount" değeri kadar "TClientItemTable" structını default ctor ile ramde oluşturur.
"fill" algoritması ise default ctor ile bir adet nesneyi hayata getirir ve her elemana bu nesneyi kopyalar (yani örneğin rowCount değeri 1k olsun bu 1k kez kopya oluşturmak demek, gereksiz bir maliyet) yani;
1-> "std::vector" default ctor ile x tane "TClientItemTable" oluşturdu
2-> std::ranges::fill, bir kez default ctor "rowCount" kez copy assign operator çağrıldı
niyete göre "reserve" edilip eleman pushlanabilir, assign benzeri member funclar kullanılabilir vesaire, ama sizin kullanımız pek doğru değil.
yani bu iki işlemde aslında siz rowCount +1 kez default ctor ve rowCount kadar kopya oluşturdunuz
C++:
const std::string_view name = it->second;
strlcpy(itemTable->szName, name.data(), sizeof(itemTable->szName));
burda bir UB kokusu var ama csvparser ne yapıyor bir fikrim yok bu yüzden net bir şey söyleyemiyorum, null string'den kaynaklı bir sorun meydana gelebilir(ihtimal, kodu bilmiyorum. string_view bir nesnenin başlangıç adresini ve sizeını tutar, genelde karşılaşılacak sorun lifetime ve null stringdir).
diğer kodlarınızda patch olarak nitelendirilebilir, örneğin "GetAlignIndex"
gerçek bir modernizasyonda sanırsam şöyle bir tablo olabilirdi(işte branchless yapılabilirdi, o olabilirdi bu olabilirdi, mikro optimizasyon konuşmuyorum(derleyiciden akıllı değiliz, erken optimizasyon optimizasyon değildir), ilk aklıma gelen yöntem bu "modernize" etmek açısından, derleyici optimizasyonuyla assembly düzeyde büyük ihtimalle aynıya yakın kod üretilir, kontrol etmek gerek);
(yanlış hatırlamıyorsam C++20'de constexpr fonksiyonların içinde static tanımlayamıyorduk, bu kod C++ sürümünüze göre derlenmeyebilir ama 23'de legal)
C++:
[[nodiscard("please use the align index ret value")]] static constexpr std::uint8_t GetAlignIndex(int align) noexcept
{
static constexpr std::array align_arr_idx_table{
120'000, //0
80'000, // 1
40'000, //2
10'000, //3
0, // 4
-40'000, //5
-80'000, //6
-120'000, // 7
};
const auto iter{std::ranges::lower_bound(align_arr_idx_table, align, std::ranges::greater{})};
if (iter == align_arr_idx_table.cend())
{
return align_arr_idx_table.size();
}
return static_cast<std::uint8_t>(std::ranges::distance(align_arr_idx_table.begin(), iter));
}
çok uzatmadan; C++26'dan önce C++20 ve 23'e odaklanmanızı tavsiye edebilirim, en basitinden bu txt'leri run time maliyetinden kurtararak tamamen compile timeda okumanız bile mümkün, yaptığınız o zaman refactor olabilir naçizane, emeğinize sağlık.
Konuyu tam olarak bunun için yoruma açık tutuyorum, katkınız için teşekkür ederim.
Başlığa C++26 yazmamın sebebi en azından C++26 ile derlenebilir kod almak ve daha modern yaklaşımları teşvik etmek. Yoksa evet bu özellikler C++20 ile geldi.
UB kokusu var dediğiniz yerde hakkınız var, ancak pratikte CSV parser \0’lu buffer veriyor.
Bug yok ancak kod sadece suboptimal. Yine de eski koda göre hala çok daha temiz bu hali.
Bu rowcount kere yapılan copy assignment'i de evet gereksiz iş yükü olmuş, işaret ettiğiniz için teşekkür ederim. Bunu da direkt fill fonksiyonunu tamamen kaldırarak elimine edebiliriz. Çünkü resize(rowCount) zaten rowCount adet TItemTable default-construct eder.
Eski kod memset ile byte-level sıfırlamaya güveniyordu. Bu C kodu için normal ama C++ için güvenli değil. Artık default initialization üzerinden ilerlemek daha doğru. C++'ın kendisine bıraktım yani işi.
Ana yorumdaki std::ranges::fill kısımlarını siliyorum, daha iyi bir öneriniz varsa lütfen yorum yapınız.
Tekrardan, katkınız ve yapıcı elestiriniznicin teşekkür ederim. Lütfen ileride de yaptığım patch/refaktörleri eleştirmeye devam edin.
Konuyu tam olarak bunun için yoruma açık tutuyorum, katkınız için teşekkür ederim.
Başlığa C++26 yazmamın sebebi en azından C++26 ile derlenebilir kod almak ve daha modern yaklaşımları teşvik etmek. Yoksa evet bu özellikler C++20 ile geldi.
UB kokusu var dediğiniz yerde hakkınız var, ancak pratikte CSV parser \0’lu buffer veriyor.
Bug yok ancak kod sadece suboptimal. Yine de eski koda göre hala çok daha temiz bu hali.
Bu rowcount kere yapılan copy assignment'i de evet gereksiz iş yükü olmuş, işaret ettiğiniz için teşekkür ederim. Bunu da direkt fill fonksiyonunu tamamen kaldırarak elimine edebiliriz. Çünkü resize(rowCount) zaten rowCount adet TItemTable default-construct eder.
Eski kod memset ile byte-level sıfırlamaya güveniyordu. Bu C kodu için normal ama C++ için güvenli değil. Artık default initialization üzerinden ilerlemek daha doğru. C++'ın kendisine bıraktım yani işi.
Ana yorumdaki std::ranges::fill kısımlarını siliyorum, daha iyi bir öneriniz varsa lütfen yorum yapınız.
Tekrardan, katkınız ve yapıcı elestiriniznicin teşekkür ederim. Lütfen ileride de yaptığım patch/refaktörleri eleştirmeye devam edin.
m_vec_itemTable, isminden gördüğüm kadarıyla bir member variable.
Örneğin; siz boot sekansında database'den okuyup bu vector'e nesneyi pushluyorsunuz veya emplace ediyorsunuz.
Yine örneğin; bu tabloda 3 tane kayıt var. (Bu tablo item_proto'yu tutuyorsa binlerce kayıt vardır demek; anlaşılması açısından sayıları minimal tutuyorum.)
İlk öğe pushlandı -> Sorun yok. Duruma göre ya default ctor + copy veyahut move ctor (veya direkt emplace edilirse placement-new ile alakalı ctor) çalıştı.
İkinci öğe pushlandı -> Üsttekiyle aynı + re-allocation oluştu. Neden? Kapasite doldu. RAM'de yeterli alan reserved olmadığından dolayı, C++ runtime RAM'de yeni boyuta uygun yeni bir alan tahsis etti ve nesneler henüz Object Lifetime'ı devam ederken yen bellek bölgesine taşındı. (Şayet move ctor sağlandıysa [noexcept garantisi olması gerekir] move ctor veyahut default edildiyse, burada vector'deki nesneler teker teker move edilir ama move ctor not declared ve copy operator declared veyahut provided ise copy ctor çağırılır maliyetlidir)
Bu re-allocation, kapasite doldukça tekrar edilir ve maliyetli bir işlemdir; hele ki copy ctor çalışıyor ve struct'ınız trivially copyable değilse.
Örneğin; birkaç record eklediniz ve bu fonksiyonu yeniden tetiklediniz (reload vs. yoluyla);
Önce clear çağrıldı -> Bu yöntemle kapasite aynı kalır ve tüm elemanlar silinir. Yani örneğin; sizin kapasiteniz şu an 10. (Her derleyici bu kapasite artırım katsayısını kendisi implemente ediyor, ISO tarafında bir standart yok ama genelde 2 katı olarak artırım sağlanır.)
Sonra "resize" member func tetiklendi;
-> Yeni tabloda kaç tane record varsa o kadar adet default ctor çağrıldı.
-> Buradan sonrasında indeksteki elemana atama yapmanız gerekir mantıken. Örneğin:
Burada tekrardan bir nesne hayata getirmiş olacaksınız ve bunu move edeceksiniz (yukarıdaki kodda copy assign operator çalışır). Bu da yeniden gereksiz maliyet sayılabilir (zaten rowCount kadar nesneyi RAM'de hayata getirdiniz resize fonksiyonu ile). Sonradan bir nesne emplace/push ederseniz kapasite büyük ihtimalle yeniden arttırılmaya çalışacak ve yine re-allocation yaşayacaksınız.
Bu hangi durumda kullanılabilir?
Şayet <algorithm> başlık dosyasından bir algoritmayı koşturacaksanız bu zaten zaruriyettir. Alakalı container'da yeterli eleman(boş bir containersa) yoksa ve algoritma koşturursanız (örneğin std::transform) programınız crash yiyecektir(seg fault).
Ben olsam şunu yapardım:
C++:
m_vec_itemTable.clear();
m_vec_itemTable.reserve(rowCount);
for(std::size_t row_idx{}; row_idx < rowCount; ++row_idx)
{
m_vec_itemTable.emplace_back(arg1, arg2...); // placement-new uygulanır, yani nesne direkt burda oluşut ve uygun ctor'a iletilir
// Veyahut
m_vec_itemTable.emplace_back(std::move(item_table)); // push_back de olabilir, ikisi de move ctor'u çağıracaktır.
}
Neden?
"reserve" member func, size argüman geçtiğiniz sayı kadar RAM'de bir alan tahsis(raw memory) eder, Object Lifetime burda başlamaz yani hiçbir nesne henüz hayata gelmemiştir ve bu alan dolana kadar re-allocation yaşanmaz.
Tabloyu okursunuz ve container içine push/emplace işlemini yaparsınız.
Run-time'da bu container'a bir şey eklemeyeceğinizi düşünürsek, kullanabileceğiniz en mantıklı yöntemlerden biri budur.
Umarım çok fazla teknikte boğmamış ve anlatabilmişimdir. C++; 2 satır kodun altında bile çok fazla teknik detay yatan bir dil, ben minimal tutmaya çalıştım. Haddime olmasa da Bjarne'nin bir konuşmasında "kullanacağınızı kadar öğrenin yeter, her detayı bilmenize gerek yok" benzeri söylemini kendimce yanlış buluyorum(lütfen "Bjarne'den iyi mi bileceksin kardeşim! kimsin sen ya?" diyecekler demesin, bu benim tamamen şahsi görüşümdür, herkesin bir görüşü ve hatası olabilir), şahsımca mükemmel ve çok güçlü bir dil(ki bundan para kazanıyorum, sevdam sorgulanamaz) ama kontrolsüz güç, güç değildir. Günümüz "rust" sevicileride burdan çıkyıor zaten, yine de Metin2'nin kaynak kodlarına göre(zaten berbat bir mühendislik) bu detaylar "over-engineering" gibi tanımlanabilir bazı arkadaşlar tarafından ama olması gereken, prod-levelde kullanılan ve doğrusu budur. Saygılarımla.
m_vec_itemTable, isminden gördüğüm kadarıyla bir member variable.
Örneğin; siz boot sekansında database'den okuyup bu vector'e nesneyi pushluyorsunuz veya emplace ediyorsunuz.
Yine örneğin; bu tabloda 3 tane kayıt var. (Bu tablo item_proto'yu tutuyorsa binlerce kayıt vardır demek; anlaşılması açısından sayıları minimal tutuyorum.)
İlk öğe pushlandı -> Sorun yok. Duruma göre ya default ctor + copy veyahut move ctor (veya direkt emplace edilirse placement-new ile alakalı ctor) çalıştı.
İkinci öğe pushlandı -> Üsttekiyle aynı + re-allocation oluştu. Neden? Kapasite doldu. RAM'de yeterli alan reserved olmadığından dolayı, C++ runtime RAM'de yeni boyuta uygun yeni bir alan tahsis etti ve nesneler henüz Object Lifetime'ı devam ederken yen bellek bölgesine taşındı. (Şayet move ctor sağlandıysa [noexcept garantisi olması gerekir] move ctor veyahut default edildiyse, burada vector'deki nesneler teker teker move edilir ama move ctor not declared ve copy operator declared veyahut provided ise copy ctor çağırılır maliyetlidir)
Bu re-allocation, kapasite doldukça tekrar edilir ve maliyetli bir işlemdir; hele ki copy ctor çalışıyor ve struct'ınız trivially copyable değilse.
Örneğin; birkaç record eklediniz ve bu fonksiyonu yeniden tetiklediniz (reload vs. yoluyla);
Önce clear çağrıldı -> Bu yöntemle kapasite aynı kalır ve tüm elemanlar silinir. Yani örneğin; sizin kapasiteniz şu an 10. (Her derleyici bu kapasite artırım katsayısını kendisi implemente ediyor, ISO tarafında bir standart yok ama genelde 2 katı olarak artırım sağlanır.)
Sonra "resize" member func tetiklendi;
-> Yeni tabloda kaç tane record varsa o kadar adet default ctor çağrıldı.
-> Buradan sonrasında indeksteki elemana atama yapmanız gerekir mantıken. Örneğin:
Burada tekrardan bir nesne hayata getirmiş olacaksınız ve bunu move edeceksiniz (yukarıdaki kodda copy assign operator çalışır). Bu da yeniden gereksiz maliyet sayılabilir (zaten rowCount kadar nesneyi RAM'de hayata getirdiniz resize fonksiyonu ile). Sonradan bir nesne emplace/push ederseniz kapasite büyük ihtimalle yeniden arttırılmaya çalışacak ve yine re-allocation yaşayacaksınız.
Bu hangi durumda kullanılabilir?
Şayet <algorithm> başlık dosyasından bir algoritmayı koşturacaksanız bu zaten zaruriyettir. Alakalı container'da yeterli eleman(boş bir containersa) yoksa ve algoritma koşturursanız (örneğin std::transform) programınız crash yiyecektir(seg fault).
Ben olsam şunu yapardım:
C++:
m_vec_itemTable.clear();
m_vec_itemTable.reserve(rowCount);
for(std::size_t row_idx{}; row_idx < rowCount; ++row_idx)
{
m_vec_itemTable.emplace_back(arg1, arg2...); // placement-new uygulanır, yani nesne direkt burda oluşut ve uygun ctor'a iletilir
// Veyahut
m_vec_itemTable.emplace_back(std::move(item_table)); // push_back de olabilir, ikisi de move ctor'u çağıracaktır.
}
Neden?
"reserve" member func, size argüman geçtiğiniz sayı kadar RAM'de bir alan tahsis(raw memory) eder, Object Lifetime burda başlamaz yani hiçbir nesne henüz hayata gelmemiştir ve bu alan dolana kadar re-allocation yaşanmaz.
Tabloyu okursunuz ve container içine push/emplace işlemini yaparsınız.
Run-time'da bu container'a bir şey eklemeyeceğinizi düşünürsek, kullanabileceğiniz en mantıklı yöntemlerden biri budur.
Umarım çok fazla teknikte boğmamış ve anlatabilmişimdir. C++; 2 satır kodun altında bile çok fazla teknik detay yatan bir dil, ben minimal tutmaya çalıştım. Haddime olmasa da Bjarne'nin bir konuşmasında "kullanacağınızı kadar öğrenin yeter, her detayı bilmenize gerek yok" benzeri söylemini kendimce yanlış buluyorum(lütfen "Bjarne'den iyi mi bileceksin kardeşim! kimsin sen ya?" diyecekler demesin, bu benim tamamen şahsi görüşümdür, herkesin bir görüşü ve hatası olabilir), şahsımca mükemmel ve çok güçlü bir dil(ki bundan para kazanıyorum, sevdam sorgulanamaz) ama kontrolsüz güç, güç değildir. Günümüz "rust" sevicileride burdan çıkyıor zaten, yine de Metin2'nin kaynak kodlarına göre(zaten berbat bir mühendislik) bu detaylar "over-engineering" gibi tanımlanabilir bazı arkadaşlar tarafından ama olması gereken, prod-levelde kullanılan ve doğrusu budur. Saygılarımla.
Haklısın, resize + index üzerinden doldurma yaklaşımı çalışıyor ama gereksiz default ctor çağrılarına yol açıyor ve uzun vadede ölçeklenebilir değil. Bu yüzden düzgün bir refactor için clear + reserve + emplace_back modeline geçmek daha doğru.
Şöyle yaptım:
Eski:
/* - [4] Allocate target vector --------------------------------------------
We re-load to get accurate row count after header.
RowCount includes header, so we subtract 1.
-------------------------------------------------------------------------- */
data.Destroy();
if (!data.Load("mob_proto.txt", '\t'))
{
fprintf(stderr, "Failed to reload mob_proto.txt\n");
return false;
}
data.Next(); // skip header row
m_vec_mobTable.resize(data.m_File.GetRowCount() - 1);
/* - [5] Parse rows ---------------------------------------------------------
Parse row-by-row into pre-allocated vector slots.
index guard prevents overflow if file has unexpected row count issues.
-------------------------------------------------------------------------- */
std::size_t index = 0;
while (data.Next() && index < m_vec_mobTable.size())
{
TMobTable& mob = m_vec_mobTable[index];
if (!Set_Proto_Mob_Table(&mob, data, localMap))
{
// Non-fatal: we keep going to load as much as possible.
fprintf(stderr, "Failed to parse mob_proto row.\n");
}
/* - [6] Debug log ----------------------------------------------------- */
sys_log(1, "MOB #%-5d %-24s level: %-3u rank: %u empire: %d",
mob.dwVnum,
mob.szName,
mob.bLevel,
mob.bRank,
mob.bEmpire);
++index;
}
Yeni:
/* - [4] Reserve target vector capacity -------------------------------------
We re-load the file to get an accurate row count.
reserve() allocates raw memory only; no TMobTable objects are constructed yet.
This prevents re-allocation during parsing.
-------------------------------------------------------------------------- */
data.Destroy();
if (!data.Load("mob_proto.txt", '\t'))
{
fprintf(stderr, "Failed to reload mob_proto.txt\n");
return false;
}
data.Next(); // skip header row
const std::size_t rowCount = data.m_File.GetRowCount() - 1;
m_vec_mobTable.clear();
m_vec_mobTable.reserve(rowCount);
/* - [5] Emplace and parse rows ---------------------------------------------
Each TMobTable is constructed directly inside the vector via emplace_back().
Set_Proto_Mob_Table fills the object in-place.
No temporary objects, no copy/move, no index-based assignment.
-------------------------------------------------------------------------- */
while (data.Next())
{
m_vec_mobTable.emplace_back();
TMobTable& mob = m_vec_mobTable.back();
if (!Set_Proto_Mob_Table(&mob, data, localMap))
{
fprintf(stderr, "Failed to parse mob_proto row.\n");
m_vec_mobTable.pop_back();
continue;
}
sys_log(1, "MOB #%-5d %-24s level: %-3u rank: %u empire: %d",
mob.dwVnum,
mob.szName,
mob.bLevel,
mob.bRank,
mob.bEmpire);
}
Artık bu şekilde
Reallocation riski ortadan kalkıyor
Gereksiz ctor / assignment maliyeti oluşmuyor
Nesneler doğrudan doğru ctor ile, tek seferde hayata geliyor
Ana refaktör yorumumu da buna göre düzenleyeceğim. Bu sefer oldu gibi, ne dersin?
Detaylı ve yapıcı açıklama için ayrıca teşekkür ederim.
assign(count, value) de fill gibi count kez copy assignment yapar; bu yüzden bilinçli olarak kullanılmadı. az önce eleştirilen fill yaklaşımının aynısı bu.
bu son yaptığım reserve + emplace_back en doğru yaklaşım olsa gerek
Haklısın, resize + index üzerinden doldurma yaklaşımı çalışıyor ama gereksiz default ctor çağrılarına yol açıyor ve uzun vadede ölçeklenebilir değil. Bu yüzden düzgün bir refactor için clear + reserve + emplace_back modeline geçmek daha doğru.
Şöyle yaptım:
Eski:
/* - [4] Allocate target vector --------------------------------------------
We re-load to get accurate row count after header.
RowCount includes header, so we subtract 1.
-------------------------------------------------------------------------- */
data.Destroy();
if (!data.Load("mob_proto.txt", '\t'))
{
fprintf(stderr, "Failed to reload mob_proto.txt\n");
return false;
}
data.Next(); // skip header row
m_vec_mobTable.resize(data.m_File.GetRowCount() - 1);
/* - [5] Parse rows ---------------------------------------------------------
Parse row-by-row into pre-allocated vector slots.
index guard prevents overflow if file has unexpected row count issues.
-------------------------------------------------------------------------- */
std::size_t index = 0;
while (data.Next() && index < m_vec_mobTable.size())
{
TMobTable& mob = m_vec_mobTable[index];
if (!Set_Proto_Mob_Table(&mob, data, localMap))
{
// Non-fatal: we keep going to load as much as possible.
fprintf(stderr, "Failed to parse mob_proto row.\n");
}
/* - [6] Debug log ----------------------------------------------------- */
sys_log(1, "MOB #%-5d %-24s level: %-3u rank: %u empire: %d",
mob.dwVnum,
mob.szName,
mob.bLevel,
mob.bRank,
mob.bEmpire);
++index;
}
Yeni:
/* - [4] Reserve target vector capacity -------------------------------------
We re-load the file to get an accurate row count.
reserve() allocates raw memory only; no TMobTable objects are constructed yet.
This prevents re-allocation during parsing.
-------------------------------------------------------------------------- */
data.Destroy();
if (!data.Load("mob_proto.txt", '\t'))
{
fprintf(stderr, "Failed to reload mob_proto.txt\n");
return false;
}
data.Next(); // skip header row
const std::size_t rowCount = data.m_File.GetRowCount() - 1;
m_vec_mobTable.clear();
m_vec_mobTable.reserve(rowCount);
/* - [5] Emplace and parse rows ---------------------------------------------
Each TMobTable is constructed directly inside the vector via emplace_back().
Set_Proto_Mob_Table fills the object in-place.
No temporary objects, no copy/move, no index-based assignment.
-------------------------------------------------------------------------- */
while (data.Next())
{
m_vec_mobTable.emplace_back();
TMobTable& mob = m_vec_mobTable.back();
if (!Set_Proto_Mob_Table(&mob, data, localMap))
{
fprintf(stderr, "Failed to parse mob_proto row.\n");
m_vec_mobTable.pop_back();
continue;
}
sys_log(1, "MOB #%-5d %-24s level: %-3u rank: %u empire: %d",
mob.dwVnum,
mob.szName,
mob.bLevel,
mob.bRank,
mob.bEmpire);
}
Artık bu şekilde
Reallocation riski ortadan kalkıyor
Gereksiz ctor / assignment maliyeti oluşmuyor
Nesneler doğrudan doğru ctor ile, tek seferde hayata geliyor
Ana refaktör yorumumu da buna göre düzenleyeceğim. Bu sefer oldu gibi, ne dersin?
Detaylı ve yapıcı açıklama için ayrıca teşekkür ederim.
Olabilir evet, bu arada emplace_back son eklenen nesnenin referansını return eden bir overload sahip. İleridek kullanımlarınız için söylüyorum, en son eklediğiniz nesnenin referansına ulaşabilirsiniz.
İlk yapılanda şayet Set_Proto_Mob_Table fonksiyonu noexcept garantisi vermiyorsa, exception throw ettiği durumlarda başınız ağrıyabilir.
Direkt bir nesne emplace ettiniz ve şunlar oluştu:
-> vector'un sonunda default-construct edilmiş bir nesne var
-> Set_Proto_Mob_Tableexception throw ettiği senaryoda catch ile eklediğini silmezseniz(pop_back ile yani) eklediğiniz nesne asla silinmez.
-> Potansiyel olarak yine re-allocation yaşanabilir
Size gönderdiğim kodda ise:
-> Set_Proto_Mob_Table bir exception throw etse bile bunu try catch ile yakalamanız yeterlidir, stack unwinding sayesinde "mob_table" nesnesi zaten otomatik yok olacaktır.
Olabilir evet, bu arada emplace_back son eklenen nesnenin referansını return eden bir overload sahip. İleridek kullanımlarınız için söylüyorum, en son eklediğiniz nesnenin referansına ulaşabilirsiniz.
İlk yapılanda şayet Set_Proto_Mob_Table fonksiyonu noexcept garantisi vermiyorsa, exception throw ettiği durumlarda başınız ağrıyabilir.
Direkt bir nesne emplace ettiniz ve şunlar oluştu:
-> vector'un sonunda default-construct edilmiş bir nesne var
-> Set_Proto_Mob_Tableexception throw ettiği senaryoda catch ile eklediğini silmezseniz(pop_back ile yani) eklediğiniz nesne asla silinmez.
-> Potansiyel olarak yine re-allocation yaşanabilir
Size gönderdiğim kodda ise:
-> Set_Proto_Mob_Table bir exception throw etse bile bunu try catch ile yakalamanız yeterlidir, stack unwinding sayesinde "mob_table" nesnesi zaten otomatik yok olacaktır.
Metin2'nin neresi exception-safe ki? Bir de Set_Proto_Mob_Table zaten bool döndürüyor. Sizin verdiğiniz kod server ve Dump Proto parsing için over-engineering değil mi? Yanlış mı düşünüyorum?
Metin2'nin neresi exception-safe ki? Bir de Set_Proto_Mob_Table zaten bool döndürüyor. Sizin verdiğiniz kod server ve Dump Proto parsing için over-engineering değil mi? Yanlış mı düşünüyorum?
Yani o Metin2'nin sorunu benim yazdıklarım başlıktakı "Modern C++ ile Refaktör" ile ilgili, Metin2 evet exception-safe değil ama sonuçta yaptığımız "Moder C++ Refaktörü", Metin2'de olmaması bu durumun "yok" olduğu anlamına gelmiyor ki sadece Metin2'de ele alınmamış veyahut yok sayılmış oluyor, halbuki çok önemli bir konudur.(gerçi C++11 öncesi direkt noexcept specifier yoktu ama farklı yöntemlerle sağlanabiliyordu[detaylarına aşırı hakim değilim Modern C++ öncesinde ne yaptıklarının.]).
Verdiğim kodda neden over-engineering olduğunu düşündüğünüzü söylerseniz onu da açıklayabilirim ama böyle bir durum yok ki, sizin yönteminizde mantık şu; "Önce çantaya bir tane kalem koyayım, gerekli şartları sağlanmazsa kalemi çantadan geri çıkarayım."
Benim verdiğim ise şu; "Önce şartları kontrol edeyim, şartlar tamam ise kalemi çantaya koyayım." (ek olarak Exception Safety konusuna değinmeyeceğim.)
Kod olarak bakacak olursanız kodda fazladan 2 satır silindi ve emplace kontrolden ve logdan sonraya eklendi, neden böyle düşündüğünüze anlam veremedim açıkçası.
Yani o Metin2'nin sorunu benim yazdıklarım başlıktakı "Modern C++ ile Refaktör" ile ilgili, Metin2 evet exception-safe değil ama sonuçta yaptığımız "Moder C++ Refaktörü", Metin2'de olmaması bu durumun "yok" olduğu anlamına gelmiyor ki sadece Metin2'de ele alınmamış veyahut yok sayılmış oluyor, halbuki çok önemli bir konudur.(gerçi C++11 öncesi direkt noexcept specifier yoktu ama farklı yöntemlerle sağlanabiliyordu[detaylarına aşırı hakim değilim Modern C++ öncesinde ne yaptıklarının.]).
Verdiğim kodda neden over-engineering olduğunu düşündüğünüzü söylerseniz onu da açıklayabilirim ama böyle bir durum yok ki, sizin yönteminizde mantık şu; "Önce çantaya bir tane kalem koyayım, gerekli şartları sağlanmazsa kalemi çantadan geri çıkarayım."
Benim verdiğim ise şu; "Önce şartları kontrol edeyim, şartlar tamam ise kalemi çantaya koyayım." (ek olarak Exception Safety konusuna değinmeyeceğim.)
Kod olarak bakacak olursanız kodda fazladan 2 satır silindi ve emplace kontrolden ve logdan sonraya eklendi, neden böyle düşündüğünüze anlam veremedim açıkçası.
Ben aslında onu "Metin2 için bu kadar da kasmaya gerek var mı?" Kafasıyla yazmıştım. Ama haklısınız, nihai kod kalitesi perspektifinden bakınca exception safety önemli bir konu. Eve gidince sağladığınız düzenlemeyi ekleyeceğim.