C++20’deki Ranges Kitaplığı: Tasarım Kararları

Adanali

Active member
C++20’deki Ranges Kitaplığı: Tasarım Kararları


  1. C++20’deki Ranges Kitaplığı: Tasarım Kararları

Ranges kütüphanesi sayesinde Standart Şablon Kütüphanesi (STL) ile çalışmak çok daha rahat ve güçlü hale geldi. Öncelikle Ranges kütüphanesinin algoritmaları tembeldir, doğrudan konteyner üzerinde çalışabilir ve birleştirilebilirler. Ayrıca Ranges kütüphanesi, farkında olunması gereken bazı özel tasarım kararları almıştır.

Duyuru








Rainer Grimm uzun yıllardır yazılım mimarı, ekip ve eğitim yöneticisi olarak çalışmaktadır. C++, Python ve Haskell programlama dilleri üzerine makaleler yazmaktan hoşlanıyor, aynı zamanda özel konferanslarda sık sık konuşmaktan da hoşlanıyor. Modern C++ adlı blogunda C++ tutkusunu yoğun bir şekilde ele alıyor.













C++20’deki Ranges kütüphanesi hakkında daha fazla ayrıntıya girmeden önce Ranges’in en önemli üç özelliğini birkaç cümleyle özetlemek istiyorum: Ranges algoritmaları doğrudan konteyner üzerinde çalışabilir, gerekirse argümanlarını değerlendirebilir ve bestelendi.

Doğrudan konteynerin üzerinde


Ranges kütüphanesi benzer bir konteynere izin verir std::ranges::sort doğrudan konteyner üzerinde çalışabilir:


// sortRanges.cpp

#include <algorithm>&#13;
#include <iostream>&#13;
#include <vector>&#13;
&#13;
int main() {&#13;
&#13;
std::vector<int> myVec{-3, 5, 0, 7, -4};&#13;
std::ranges::sort(myVec); // (1)&#13;
for (auto v: myVec) std::cout << v << " "; // -4, -3, 0, 5, 7&#13;
&#13;
}


Aksine klasik olan işe yarar std::sort iki yineleyici tarafından tanımlanan bir aralıkta: std:sort(myVec.begin(), myVec.end()).

Aralık algoritmaları tembeldir ve oluşturulabilir.

Tembel değerlendirme ve fonksiyonel kompozisyon


Aşağıdaki program primesLazy.cpp her iki özelliği de uygular. Bir milyondan başlayarak ilk on asal sayıyı oluşturun.


// primesLazy.cpp&#13;
&#13;
#include <iostream>&#13;
#include <ranges>&#13;
&#13;
&#13;
bool isPrime(int i) {&#13;
for (int j=2; j*j <= i; ++j){&#13;
if (i % j == 0) return false;&#13;
}&#13;
return true;&#13;
}&#13;
&#13;
int main() {&#13;
&#13;
std::cout << 'n';&#13;
&#13;
auto odd = [](int i){ return i % 2 == 1; };&#13;
&#13;
for (int i: std::views::iota(1'000'000) | std::views::filter(odd) &#13;
| std::views::filter(isPrime) &#13;
| std::views::take(10)) {&#13;
std::cout << i << " "; &#13;
}&#13;
&#13;
std::cout << 'n';&#13;
&#13;
}


Fonksiyonun bileşimi soldan sağa okunabilir: kullanarak sonsuz bir veri akışı oluşturuyorum. 1'000'000 başlangıç (std::views::iota(1'000'000)) ve iki filtre uygulayın. Her filtre bir yüklem gerektirir. İlk filtre tek elemanların geçişine izin verir (std::views::filter(odd)) ve ikinci filtre asal sayıların geçmesine izin verir (std::views::filter(isPrime)). On sayıdan sonra (std::views::take(10)) Sonsuz veri akışını sonlandırıyorum. Son olarak bir milyon ile başlayan ilk on asal sayıyı sizler için derledik.








Bu, bu veri hattını kimin işlemeye başlayacağı sorusunu gündeme getiriyor. Yani sağdan sola doğru gidiyor. Veri havuzu (std::views::take(10)) bir sonraki değeri ister ve öncülünü sorgular. Bu istek, aralık tabanlı for döngüsü bir sonraki değeri döndürene kadar devam eder. Döngü sonsuz bir veri akışı üretebilir, ancak bunu yalnızca gerektiğinde yapar. Bu tembel bir değerlendirmedir.

Bu benim kısa özetimdi. Sentinels, Projection ve Concepts dahil Ranges kütüphanesi hakkında daha fazla bilgi edinmek istiyorsanız önceki makalelerime göz atın:

  1. Campi Kütüphanesi
  2. Ranges kütüphanesi ile fonksiyonel modeller
  3. C++20’deki Ranges kütüphanesi: daha fazla ayrıntı
  4. Aralıklı projeksiyonlar
  5. Nöbetçiler ve aralıklı konseptler
  6. Aralıklarla geliştirilmiş yineleyiciler
  7. Ranges kütüphanesini Pythonically kullanma: range ve filter
  8. Python’un aralık işlevi, ikincisi
  9. Python’un harita işlevi
Ama şimdi yeni bir şey hakkında yazmak istiyorum.

Tasarım kararları


Verimlilik adına Ranges kütüphanesi bazı benzersiz tasarım kararları almıştır. Bunları bilmek ve takip etmek önemlidir.

kim begin– Üye işlevi std::ranges::filter_view incelendikten sonra aşağıdakilere karşılık gelen bir kod bulun:


if constexpr (!ranges::forward_range<V>)&#13;
return /* iterator */{*this, ranges::find_if(base_, std::ref(*pred_))};&#13;
else&#13;
{&#13;
if (!begin_.has_value())&#13;
begin_ = ranges::find_if(base_, std::ref(*pred_)); // caching&#13;
return /* iterator */{*this, begin_.value())};&#13;
}


Yuvalanmış olan if-Yapı analiz edilmeye değerdir: ilk olarak derleyici, begin_.has_value() true VE. Aksi halde karar verir begin_. Bu, bu üye fonksiyonunun içindeki sonucu döndürdüğü anlamına gelir. std::ranges::filter_viewnesne sonraki çağrılarda kullanılmak üzere önbelleğe alınır. Bu önbelleğe almanın ilginç sonuçları vardır. Bunu bir kod pasajı kullanarak açıklayayım.


// cachingRanges.cpp&#13;
&#13;
#include <numeric>&#13;
#include <iostream>&#13;
#include <ranges>&#13;
#include <vector>&#13;
&#13;
int main() {&#13;
&#13;
std::vector<int> vec(1'000'000);&#13;
std::iota(vec.begin(), vec.end(), 0);&#13;
&#13;
for (int i: vec | std::views::filter([](auto v) { return v > 1000; }) &#13;
| std::views::take(5)) {&#13;
std::cout << i << " "; // 1001 1002 1003 1004 1005&#13;
}&#13;
&#13;
}


Gelen ilk çağrı std::views::filter([](auto v) { return v > 1000; }) İlk yineleyiciyi belirler ve onu sonraki çağrılarda yeniden kullanır. Bu önbelleğe almanın avantajı açıktır: ardışık düzenin daha sonraki birçok yinelemesinden kaçınılır. Ancak ciddi dezavantajlar da var: önbellek sorunları ve tutarlılık sorunları.

Önbellek


İşte en önemli iki aralık önbelleğe alma kuralı:

  • Değiştirilmiş bir aralık için görünüm kullanmayın!
  • Bir görünümü kopyalamayın!
Önceki programa yapılan aşağıdaki değişiklik cachingRanges.cpp her iki kuralı da çiğniyor:


// cachingIssuesRanges.cpp&#13;
&#13;
#include <concepts>&#13;
#include <forward_list>&#13;
#include <iostream>&#13;
#include <numeric>&#13;
#include <ranges>&#13;
#include <vector>&#13;
&#13;
void printElements(std::ranges::input_range auto&& rang) {&#13;
for (int i: rang) {&#13;
std::cout << i << " "; &#13;
}&#13;
std::cout << 'n';&#13;
}&#13;
&#13;
int main() {&#13;
&#13;
std::cout << 'n';&#13;
&#13;
std::vector<int> vec{-3, 10, 4, -7, 9, 0, 5, -5}; // (1)&#13;
std::forward_list<int> forL{-3, 10, 4, -7, 9, 0, 5, -5}; // (2)&#13;
&#13;
auto first5Vector = vec | std::views::filter([](auto v) { return v > 0; }) // (3)&#13;
| std::views::take(5);&#13;
&#13;
auto first5ForList = forL | std::views::filter([](auto v) { return v > 0; }) // (4) &#13;
| std::views::take(5); &#13;
&#13;
printElements(first5Vector); // 10 4 9 5 // (5)&#13;
printElements(first5ForList); // 10 4 9 5 // (6)&#13;
&#13;
std::cout << 'n';&#13;
&#13;
vec.insert(vec.begin(), 10);&#13;
forL.insert_after(forL.before_begin(), 10);&#13;
&#13;
&#13;
printElements(first5Vector); // -3 10 4 9 5&#13;
printElements(first5ForList); // 10 4 9 5&#13;
&#13;
std::cout << 'n';&#13;
&#13;
auto first5VectorCopy{first5Vector}; // (7)&#13;
auto first5ForListCopy{first5ForList}; // (8)&#13;
&#13;
printElements(first5VectorCopy); // -3 10 4 9 5&#13;
printElements(first5ForListCopy); // 10 10 4 9 5&#13;
&#13;
std::cout << 'n';&#13;
&#13;
}&#13;



Sorunun daha iyi anlaşılabilmesi için çıktıyı doğrudan kaynak koduna yazdım. Program aşağıdaki adımları bir std::vector biridir std::forward_list Başından sonuna kadar. Öncelikle her iki konteyner de başlatma listesiyle başlatılır. {-3, 10, 4, -7, 9, 0, 5, -5} (1) ve (2)’yi başlatır. Daha sonra iki görünüm (3) ve (4) oluşturuyorum. İki vizyon first5Vector VE first5ForList 0’dan büyük ilk beş elementten oluşur. Karşılık gelen değerler (5) ve (6)’da görünür.

Şimdi ilk kuralı çiğniyorum: “Değiştirilmiş alanlarda görünüm kullanmayın.” Numarayı her iki kabın üstüne ekliyorum 10 A. Öyleyse göster first5Vector THE -3 onun first5ForList Girilen 10’u dikkate almayın. (7) ve (8)’deki ikinci “Görünümü kopyalama” kuralını çiğnedikten sonra, önbellek first5ForListCopy geçersiz. first5VectorCopy hala yanlış rakamları gösteriyor. Son olarak programın çıktısı aşağıdadır.








İşte basit bir temel kural: Görünümleri tanımladıktan hemen sonra kullanın!

İşlev printElements argümanlarını, yönlendirme referansı olarak da bilinen evrensel referans yoluyla alır.

Sıradaki ne?


Bir sonraki yazımda bir fonksiyonun neden evrensel referans yoluyla herhangi bir görüşü kabul etmesi gerektiğini yazacağım.

İşte benim adıma başka bir not


Çok ciddi, ilerleyici bir sinir hastalığı olan ALS hastası olduğumu size bildirmekten üzüntü duyuyorum. Bu yüzden bu bloga ne kadar devam edebilirim bilmiyorum. Şu anda eşimin yardımıyla yalnızca eğitim kursları veya konferanslar için seyahat edebiliyorum. Biz (ben ve ailem) bu zorlukla agresif bir şekilde mücadele etmeye karar verdik. Bu nedenle siz sadık okurlarıma hastalığımı duyurmak benim için önemliydi.


(kendim)



Haberin Sonu
 
Üst