Evet sevgili yazılımperver dostlarım uzun bir aradan sonra nasıl bir yazı yazayım diye düşünürken, “blog backlog”‘um da bulunan std::transform konusuna değinmeye karar verdim. std::transform’a, aşina olduktan sonra, özellikle veri dizileri ile uğraşıyorsanız, bir çok problem için kullanabileceğiniz bir araç haline geleceğini düşünüyorum. O sebeple de bu yazımda kısaca farklı kullanımlarına değineceğim.
Peki std::transform nedir?
std::transform, verilen bir ya da daha fazla girdiyi alarak, her bir elemanına, verilen fonksiyonu uygulayarak, yeni bir dizi oluşturmamıza olanak sağlayan STL kütüphanesi fonksiyonudur. Bu fonksiyonun iki temel kullanımı bulunmaktadır.
Bunlardan birincisi, tek girdili kullanım (unary). Bu kullanımda verilen tek bir diziye, ilgili fonksiyon uygulanarak, dize değerleri değiştirilir. Birazdan göreceğimiz üzere, bu verilen dizi üzerine yazılarak da olabilir ya da yeni bir dizi de dönülebilir.
Bir diğeri ise ikili kullanımdır (binary). Bu kullanımda ise iki girdiden alınan değerler yine işlenerek yeni bir dizi elde edilir. Aşağıda farklı internet sitelerinde bulduğum ve bu kabiliyeti güzel özetleyen figürleri görebilirsiniz.
Tekli kullanım:
İkili kullanım:
Diğer programlama dillerinde bulunan (Ör. javascript) “map” kullanımına oldukça benziyor. Javascriptte de, map() fonksiyonu, mevcut bir diziden, bir fonksiyonu bütün elemanlarına uygulayarak yeni bir dizi oluşturmak için kullanılmakta. Aşağıda kısa bir örnek bulabilirsiniz:
const numbers = [1, 2, 3, 4];
const doubled = numbers.map(item => item * 2);
console.log(doubled); // [2, 4, 6, 8]
Şimdi gelelim C++ örneklerine.
Temel, Tekli Kullanım
Örneğin verilen bir dizideki sayıların iki katını hesaplamaya ihtiyacınız olduğunu düşünelim. Aşağıda, std::transform ile bunu nasıl yapabileceğiniz görebilirsiniz:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <iostream> #include <vector> #include <algorithm> int32_t main() { std::vector<int32_t> numbers = {1, 2, 3, 4, 5}; // Cikti dizi ayni boyutta olmali std::vector<int32_t> squared(numbers.size()); std::transform(numbers.begin(), numbers.end(), squared.begin(), [](int32_t n) { return n * n; } ); for (auto n : squared) { std::cout << n << " "; } // Cikti: 1 4 9 16 25 return 0; } |
Peki yukarıdaki işlemi aynı dizi üzerinde yapmak isteseydik ne yapardık? Tam olarak aşağıdaki gibi 🙂
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <iostream> #include <vector> #include <algorithm> int32_t main() { std::vector<int32_t> numbers = {1, 2, 3, 4, 5}; std::transform(numbers.begin(), numbers.end(), numbers.begin(), [](int32_t n) { return n * n; } ); for (auto n : squared) { std::cout << n << " "; } // Cikti: 1 4 9 16 25 return 0; } |
İkili Kullanım
İkili kullanım için, iki dizinin elemanlarının toplanmasına yönelik bir örnek koda ihtiyacımız olduğunu düşünelim. Aşağıda buna yönelik kodu bulabilirsiniz:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include <iostream> #include <vector> #include <algorithm> int main() { std::vector<int32_t> vec1 = {1, 2, 3, 4, 5}; std::vector<int32_t> vec2 = {10, 20, 30, 40, 50}; std::vector<int32_t> result(vec1.size()); // Lambda fonksiyonu kullanarak iki vektörün elemanlarını toplayalım std::transform(vec1.begin(), vec1.end(), vec2.begin(), result.begin(), [](int32_t a, int32_t b) { return a + b; } ); // Ciktilar: 11, 22, 33, 44, 55 for (int32_t num : result) { std::cout << num << " "; } } |
Yukarıdaki lambda yerine C++ 14 ile gelen bir fonksiyon nesnesi de kullanabiliriz acaba ne? std::plus. std::plus ile lambdayı hiç yazmadan da yukarıdaki kodu aşağıdaki gibi kullanabilirdik. Daha okunabilir ne dersiniz? Tabi <functional> başlık dosyasını unutmayın 😉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> #include <vector> #include <algorithm> int main() { std::vector<int32_t> vec1 = {1, 2, 3, 4, 5}; std::vector<int32_t> vec2 = {10, 20, 30, 40, 50}; std::vector<int32_t> result(vec1.size()); // Lambda fonksiyonu kullanarak iki vektörün elemanlarını toplayalım std::transform(vec1.begin(), vec1.end(), vec2.begin(), result.begin(), std::plus<>()); // Ciktilar: 11, 22, 33, 44, 55 for (int32_t num : result) { std::cout << num << " "; } } |
Bu noktada, std::for_each ile, std::trasnform’un ne farkı var diye aklınıza bir soru gelebilir hemen bakalım:
- std::for_each ile yeni bir dizi oluşturulmaz, iki kullanımı bulunmaz,
- std::transform, fonksiyonel programlamaya daha yatkındır ve daha fazla işlev sunar,
- std::for_each performans anlamında daha hafiftir.
std::transform kullanırken dikkat etmeniz gereken bir takım hususlara da burada değinmek istiyorum:
- Öncelikle, çıktı dizisinin büyüklüğü en az girdi kadar olmalıdır,
- İkili kullanımda, ikinci dizinin boyutu da en az birincisi kadar olmalıdır,
- Uygulanacak olan fonksiyonların “side-effect”‘leri ya da duruma bağlı işlevleri olmamalıdır. Bu fonksiyonların uygulanma sırası farklı olabilir ve buna göre çıktı değişmemelidir.
Sonuç olarak aşağıdaki gibi işlevleriniz için artık döngüleri kullanmadan, std::transform ile daha okunabilir kod yazabilirsiniz:
- Matematiksel işlemler (Kare alma, negatif alma, artırma vb.),
- Metin dönüşümleri (Büyük-küçük harf dönüşümü vb.),
- Birden fazla veri kaynağını işlemek (İki vektörü toplamak vb.),
- Diziler üzerinde fonksiyonel programlama yaklaşımıyla işlem yapmak.
Konuyu kapatmadan önce şu husustan da bahsetmekte fayda var. C++ 20 ile birlikte ranges::transform da dile ile sunuldu ama bu konuya genel olarak ranges konusuna bakarken değineceğimiz için burada bir daha detaylarına girmedim.