initializer_list

Serkan Eser
17 min readMar 19, 2023

--

initializer_list C++11 ile beraber özellikle konteyner sınıfların süslü parantezler içinde verilen, virgülle ayrılmış, bir liste ile iklendirilebilmelerini sağlayan bir araç olarak eklenmiştir. Bu sayede, C++11 sonrası, vector ve map gibi sınıfları aşağıdaki gibi ilklendirmek mümkün hale gelmiştir.

#include <iostream>
#include <vector>
#include <map>

int main()
{
std::vector<int> v{1, 2, 3, 4, 5};

std::map<int, std::string> m{{1, "tom"}, {2, "jerry"}};
}

Örnek kodu C++98 standartlarına göre derlemek istediğimizde bu tür bir ilklendirmenin yapılamayacağına ilişkin bir hata almaktayız. g++, vector için aşağıdaki hatayı üretmektedir.

g++ -otest test.cpp --save-temps -std=c++98
test.cpp: In function ‘int main()’:
test.cpp:7:23: warning: extended initializer lists only available with ‘-std=c++11’ or ‘-std=gnu++11
7 | std::vector<int> v{1, 2, 3, 4, 5};
| ^
test.cpp:7:22: error: in C++98 ‘v’ must be initialized by constructor, not by ‘{...}’
7 | std::vector<int> v{1, 2, 3, 4, 5};

C++11 öncesinde de süslü parantezler ilklendirme için kullanılabilmelerine karşın daha kısıtlı bir kullanıma sahipti. Ancak aggregate türler, C dilinde olduğu gibi, süslü parantezler içinde verilen bir liste ile ilklendirilebilmekteydi.

//Hem C++98 hem de C++11'de geçerli
#include <iostream>

struct data
{
int a;
int b;
int c;
char arr[4];
};
int main()
{
int a[] = {1, 2, 3, 4, 5};
data d = {1, 2, 3, {'a', 'b', 'c', 'd'}};
}

Örnekteki ilklendirmeler C++11 öncesinde de geçerli idi. Diziler aggregate türlerdir. Bir sınıf türünün (class type) ise aggregate olabilmesi için sağlaması gereken özelliklerin bir kısmı aşağıdaki gibidir.

  • Kullanıcı tanımlı bir constructor fonksiyonu içermemeli
  • Sanal (virtual) bir fonksiyon içermemeli
  • Veri elemanları public olmalı

Bir türün aggregate olup olmamasıyla ilgili kriterlerin bir kısmı standartlara göre değişiklik göstermektedir. Detayına aşağıdaki bağlantıdan bakabilirsiniz.

https://en.cppreference.com/w/cpp/language/aggregate_initialization

Not: C++11 ile beraber dile bir türün aggregate olup olmadığını sınayan
is_aggregate isimli bir type trait eklenmiştir.

Yukarıdaki örnekteki ilklendirmeler, C++11 öncesi kurallara göre, ancak atama operatörü kullanımı ile mümkündür. Örneğimizdeki atama operatörlerini kaldıralım.

#include <iostream>

struct data
{
int a;
int b;
int c;
char arr[4];
};
int main()
{
int a[]{1, 2, 3, 4, 5};
data d{1, 2, 3, {'a', 'b', 'c', 'd'}};
}

Atama operatörünü kaldırdığımızda g++ sadece bir uyarı üretmesine karşın clang++, standartlara uygun bir şeklilde, sentaks hatası üretmektedir. g++ derleyicisi, C++11 öncesinde, extension olarak bu tür bir ilklendirmeye izin vermekteydi.

g++ -otest test.cpp --save-temps -std=c++98
test.cpp: In function ‘int main()’:
test.cpp:13:12: warning: extended initializer lists only available with ‘-std=c++11’ or ‘-std=gnu++11’
13 | int a[]{1, 2, 3, 4, 5};
| ^
test.cpp:15:11: warning: extended initializer lists only available with ‘-std=c++11’ or ‘-std=gnu++11’
15 | data d{1, 2, 3, {'a', 'b', 'c', 'd'}};
clang++ -otest test.cpp --save-temps -std=c++98
test.cpp:13:9: error: definition of variable with array type needs an explicit size or an initializer
int a[]{1, 2, 3, 4, 5};
^
test.cpp:13:12: error: expected ';' at end of declaration
int a[]{1, 2, 3, 4, 5};
^
;
test.cpp:15:11: error: expected ';' at end of declaration
data d{1, 2, 3, {'a', 'b', 'c', 'd'}};

Örneğimizdeki data türünü struct yerine class anahtar kelimesi ile tanımladığımızda öngörülen erişim belirleyici (default access specifier) private olacağından artık türümüz aggregate olmaktan çıkacak ve bu durumda sentaks hatası olacaktır.

#include <iostream>

class data
{
int a;
int b;
int c;
char arr[4];
};
int main()
{
data d{1, 2, 3, {'a', 'b', 'c', 'd'}};
}
g++ -otest test.cpp --save-temps -std=c++98
test.cpp: In function ‘int main()’:
test.cpp:13:11: warning: extended initializer lists only available with ‘-std=c++11’ or ‘-std=gnu++11’
13 | data d{1, 2, 3, {'a', 'b', 'c', 'd'}};
| ^
test.cpp:13:10: error: in C++98 ‘d’ must be initialized by constructor, not by ‘{...}’
13 | data d{1, 2, 3, {'a', 'b', 'c', 'd'}};

C++11 ile beraber bu kısıtlamaları aşmak için liste ile ilklendirme (list initialization) genişletilmiştir. Artık aggerate olmayan türler de süslü parantezler içerisindeki bir ilklendirme listesi ile, atama operatörüne gerek duyulmaksızın, ilklendirilebilmektedir.

#include <iostream>
#include <vector>

int main()
{
std::vector<int> v1{};
std::vector<int> v2{1};
std::vector<int> v3{1, 2};
std::vector<int> v4{1, 2, 3};
}

Örnekteki tüm ilklendirmeler C++11 kurallarına göre geçerlidir. Peki bu ilklendirmeler nasıl geçerli olabildi?

vector sınıf şablonundan üretilen tür, bir dizi ya da struct türünün aksine, aggregate olmadığından derleyicinin doğrudan bu türü ilklendirmeye ilişkin bir kod yazması mümkün değil. vector sınıfının ilklendirilmesi yine vector sınıfının bir constructor fonksiyonu üzeriden yapılmalı. Örnekte vector türünün değişken sayıda argüman ile ilklendirilebildiğini görüyoruz.

Not: Gerçekte kast ettiğimiz tür, vector şablonundan int tür parametresi için
elde edilen tür fakat yazım sadeleği için çoğu durumda türü sadece vector
olarak isimlendireceğim.

vector şablonu yazılırken hangi sayıda argüman ile ilklendirebileceğinin bilinmesi mümkün değildir. Bu durumda ilk aklımıza gelen çözüm, C dilinden de aşina olduğumuz değişken sayıda argüman alabilen, variadic fonksiyonlar olabilir. printf fonksiyonunun da variadic bir fonksiyon olduğunu hatırlayalım. Değişken uzunluktaki bir liste ile ilklendirmeyi mümkün kılmak için vector sınıfının constructor fonksiyonu variadic olabilirdi fakat bunun yerine C++11 ile beraber dile initializer_list isimli bir araç eklenmiştir. vector için ilgili constructor fonksiyonu aşağıdaki gibi bildirilmiştir. Bu constructor sayesinde vector sınıfı süslü parantezler içerisinde virgülle ayrılmış değişken uzunluktaki bir liste ile ilklendirilebilmektedir.

vector(initializer_list<T> __l, const allocator_type& __a = allocator_type())

initializer_list’i inceledikten sonra alternatif yöntemlerin neden seçilmediğine de kısaca bakacağız.

std::initializer_list şablonu

initializer_list bir şablon olarak tanımlanmasına karşın STL içerisindeki birçok türden farklı özel bir duruma sahip. initializer_list bir anahtar kelime (reserved keywords) olmamasına karşın derleyici tarafından bilinmektedir.

std::initializer_list şablonu GNU C++ kütüphanesi içinde, initializer_list başlık dosyasında, aşağıdaki gibi tanımlanmıştır.

template<class _E>
class initializer_list
{
public:
typedef _E value_type;
typedef const _E& reference;
typedef const _E& const_reference;
typedef size_t size_type;
typedef const _E* iterator;
typedef const _E* const_iterator;
  private:
iterator _M_array;
size_type _M_len;
// The compiler can call a private constructor.
constexpr initializer_list(const_iterator __a, size_type __l)
: _M_array(__a), _M_len(__l) { }
public:
constexpr initializer_list() noexcept
: _M_array(0), _M_len(0) { }
constexpr size_type
size() const noexcept { return _M_len; }
constexpr const_iterator
begin() const noexcept { return _M_array; }
constexpr const_iterator
end() const noexcept { return begin() + size(); }
};

initializer_list şablonunun parametreli constructor’ının private olduğuna dikkat ediniz. Bu durumda _M_array ve _M_len üye değişkenlerine bizim dışarıdan bir değer atayabilmemiz mümkün değil ancak default constructor çağrılabilir.

Bir initializer_list nesnesini aşağıdaki gibi oluşturabiliriz.

#include <iostream>
#include <initializer_list>

int main()
{
std::initializer_list<int> l{1,2,3,4,5};
}

std::initializer_list şablonu 2 parametreli bir private constructor’a bir de public default constructor’a sahip. Peki bu durumda yukarıdaki örnek için hangi constructor çağrılmış olmalı?

std::initalizer_list’in bir anahtar kelime olmamasına karşın derleyici tarafından bilindiğini daha önce söylemiştik. Derleyici süslü parantezler arasında virgülle ayrılmış bir liste ile karşılaştığında, std::initializer_list türünden bir nesne oluşturması gerektiği sonucuna varırsa, bu listeyi bellekte ardışıl bir alana yerleştirdikten sonra bu alanın başlangıç adresi ve genişliğini kullanarak bir initializer_list nesnesi oluşturmaktadır.

Yukarıdaki örnek kod derleyici tarafından yaklaşık olarak aşağıdaki gibi ele alınmaktadır.

#include <iostream>
#include <initializer_list>

int main()
{
const int __a[5] = {1, 2, 3, 4, 5};
std::initializer_list<int> l(__a, 5));
}

Yukarıdaki örnekteki temsili kod initializer_list’in parametrik constructor’ı private olduğu için geçerli değil.

#include <iostream>
#include <initializer_list>

int main()
{
std::initializer_list<int> l(0, 0);
}

Benzer bir kodu derlemek istediğimizde aşağıdaki hatayı almaktayız.

clang++ -otest test.cpp
test.cpp:6:31: error: calling a private constructor of class 'std::initializer_list<int>' std::initializer_list<int> l(0, 0);

Bir initializer_list nesnesinin bellekteki temsilini aşağıdaki gibi gösterebiliriz.

initializer_list nesnesi

Bir initializer_list nesnesi derleyici tarafından bellekte oluşturulmuş geçici const bir dizinin başlangıç adresini ve genişliğini tutmaktadır.

Derleyicinin oluşturduğu bu içsel dizinin ömrü önemlidir. O yüzden derleyicinin bu diziyi nasıl oluşturduğuna biraz daha yakından bakalım. Derleyici tarafından oluşturulan bu içsel dizi, geçici bir alan olan yığında (stack) oluşturulabildiği gibi salt okunur (read only) bölümde de oluşturulabilir. Özellikle listenin değişmezlerden (literal) oluşması durumunda derleyiciler salt okunur alanı kullanabilir.

Aşağıdaki kod için derleyicinin nasıl bir kod ürettiğine bakalım.

#include <iostream>
#include <initializer_list>

int main()
{
std::initializer_list<int>l {1, 2, 3, 4, 5, 6, 7, 8, 9};
}

Derleyicinin ürettiği sembolik makine kodlarına bakabilmek için örneği aşağıdaki gibi derleyelim.

g++ -S test.cpp

İlgilendiğimiz makine kodları aşağıdaki gibidir.

main:
pushq %rbp
movq %rsp, %rbp
movq $0, -16(%rbp)
movq $0, -8(%rbp)
movq $9, -8(%rbp) #içsel dizinin genişliği
leaq C.0.36375(%rip), %rax
movq %rax, -16(%rbp) #içsel dizinin başlangıç adresi

...
.section .rodata #salt okunur bölüm
C.0.36375:
.long 1
.long 2
.long 3
.long 4
.long 5
.long 6
.long 7
.long 8
.long 9

Örneğimizde initializer_list nesnesi otomatik ömürlü yani geçici bir nesne olduğundan yığında oluşturulmasına karşın initializer_list nesnesinin oluşturulmasına sebep olan liste salt okunur bölümde oluşturulmuş. Listedeki eleman sayısını azaltıp clang ile derlediğimizde ise derleyicinin bu kez içsel dizi için yığını seçtiğini görüyoruz.

#include <iostream>
#include <initializer_list>

int main()
{
std::initializer_list<int>l {1,2,3};
}
clang++ -S test.cpp
main:
pushq %rbp
movq %rsp, %rbp
xorl %eax, %eax
leaq -28(%rbp), %rcx
movl $1, -28(%rbp) #yığında oluşturulan içsel dizinin başlangıcı
movl $2, -24(%rbp)
movl $3, -20(%rbp)
movq %rcx, -16(%rbp)
movq $3, -8(%rbp)

Not: Standartlar initializer_list türünün içsel yapısı ilgili bir kısıtlama
getirmemiştir. initializer_list derleyici tarafından oluşturulan dizinin
başlangıç adresi ve genişliğini tutabildiği gibi bu geçici dizinin başlangıç
ve bitiş adresini de tutabilir.

initializer_list nesnesi gösterdiği içsel dizinin sahibi (owner) değildir. initializer_list nesnesi kopyalandığında içsel olarak tuttuğu dizinin bir kopyası (deep copy) çıkarılmaz. Aşağıdaki örneği derleyip çalıştırdığımızda initializer_list tarafından tutulan data türünden nesneler için copy constructor fonksiyonunun çağrılmadığını görüyoruz. foo fonksiyonuna sadece initializer_list nesnesinin tuttuğu dizinin başlangıç adresi ve dizinin genişliği geçilmektedir.

#include <iostream>
#include <initializer_list>

struct data
{
data() = default;
data(const data&) { std::cout << __PRETTY_FUNCTION__ << '\n'; }
};

void foo(std::initializer_list<data> param)
{}

int main()
{
std::initializer_list<data> l{data{}, data{}, data{}};
foo(l);
}

initializer_list ile ilgili dikkat edilmesi gereken birkaç nokta bulunmaktadır. Derleyicinin, süslü parantez içindeki değerlerden, oluşturduğu içsel dizi nerede oluşturulduğundan bağımsız olarak dil düzeyinde geçici olarak ele alınmaktadır. Geçici bir alanın ömrü ancak bazı durumlarda kendisine bir referansın bağlanması durumunda uzatılmaktadır (life extension). Benzer şekilde, derleyici tarafından oluşturulan, bu geçici alan ancak bir initializer_list nesnesi tarafından gösterildiği sürece geçerlidir. Ayrıca içsel dizinin const olması bir initializer_list nesnesinin elemanlarının sonradan değiştirilemeyeceği, ekleme ya da çıkarma işlemlerinin yapılamayacağı anlamına gelmektedir.

initializer_list oluşturulan durumlar

Daha önce aggregate türlerin de süslü parantezlerle ilklendirilebildiklerini görmüştük. Derleyicinin süslü parantezler içinde virgülle ayrılmış bir liste ile karşılaştığında bir initializer_list oluşturup oluşturmayacağına karar vermesi gerekmektedir. Örneğin aşağıdaki ilklendirmerlerden ilki için bir initializer_list nesnesi oluşturulmazken ikincisi için oluşturulmakta ve vector sınıfının constructor’ına geçirilmektedir.

#include <iostream>
#include <vector>

struct data
{
int a;
int b;
int c;
};

int main()
{
data d = {1, 2, 3}; //initializer_list nesnesi oluşturulmaz
std::vector<int> v = {1, 2, 3}; //initializer_list nesnesi oluşturulur
}

Derleyicinin hangi durumlarda bir std::initializer_list nesnesi oluşturacağına tipik örnekler üzerinden bakalım.

#include <iostream>

class data
{
public:
data(std::initializer_list<int> l)
{}
};

void foo(std::initializer_list<int> l)
{}

int main()
{
std::initializer_list<int> l{1, 2, 3};
foo({1, 2, 3});
data d{1, 2, 3};
}

Yukarıdaki her 3 kullanımda da std::initializer_list türü açık bir şekilde kullanıldığından derleyici süslü parantezlerden bir initializer_list nesnesi oluşturması gerektiği sonucunu çıkarmaktadır.

#include <iostream>

int main()
{
for (auto n : {1, 2, 3}) {
std::cout << n << ' ';
}
}

range based loop içindeki süslü parantez listesinden bir initializer_list oluşturulmaktadır.

#include <iostream>

int main()
{
auto l = {1, 2, 3};
}

auto tür belirleyici ile tür çıkarımı yapıldığında süslü parantez listesinden bir initializer_list nesnesi oluşturulabilmektedir. Tür çıkarımı başlığında bu konuya değineceğiz.

initializer_list elemanlarına erişim

initializer_list C++11 ile itibari ile public arayüzünde begin, end ve size fonksiyonlarını barındırmaktadır. Sonrasında C++14 standartlarıyla rbegin, rend fonksiyonları ve C++17 standartlarıyla da data fonksiyonu eklenmiştir. Detayına aşağıdaki bağlantıdan bakabilirsiniz.

https://en.cppreference.com/w/cpp/utility/initializer_list

initializer_list bir konteyner arayüzü sunmasına karşın yukarıda anlattığımız nedenlerden dolayı gerçekte bir koyteyner değildir. initializer_list nesnesine üzerinden bir değişiklik yapmak mümkün değildir ancak tuttuğu elemanlara okuma amaçlı erişilebilir. Bir initializer_list nesnesinin tuttuğu elemanlara tipik olarak range based for döngüsü ve iteratör arayüzüyle ile erişmek mümkündür.

#include <iostream>
#include <initializer_list>

int main()
{
std::initializer_list<int>l {1, 2, 3, 4, 5};
for (auto& e : l) {
std::cout << e << ' ';
}
}
#include <iostream>
#include <initializer_list>

int main()
{
std::initializer_list<int>l {1, 2, 3, 4, 5};
for (auto it = std::begin(l); it != std::end(l); ++it) {
std::cout << *it << ' ';
}
}

initilazer_list türü [] operatörünü ya da at fonksiyonunu barındırmamaktadır yani direkt olarak bir indeks ile elemanlarına erişmek mümkün değildir. Fakat içsel dizinin başlancıç adresi ve genişliğine erişerek bir indeks değeri ile elemanlarına erişilebilir.

#include <iostream>
#include <initializer_list>

int main()
{
std::initializer_list<int>l {1, 2, 3, 4, 5};
for (int i = 0; i < l.size(); ++i) {
std::cout << l.begin()[i] << ' ';
}
}

İçsel dizinin ömrü

initializer_list nesnesinin gösterdiği içsel dizinin geçici const bir dizi olduğunu ve ancak initalizer_list nesnesi hayatta iken ömrünün uzatıldığını (life extension) söylemiştik.

Geçici bir nesnenin ömrünün ona bir referans bağlanmasıyla uzatıldığını biliyoruz. Basit bir örnek üzerinden bu duruma bakalım.

#include <iostream>

class data
{
public:
data() { std::cout << __func__ << '\n'; }
~data() { std::cout << __func__ << '\n'; }
};

int main()
{
data{};
std::cout << "enter a number:\n";
getchar();
}

Kodu derleyip çalıştırdığımızda terminale “enter a number” yazısı yazılmadan önde data türünden nesne için destructor fonksiyonunun çağrıldığını görüyoruz. data{} ifadesi sonlandığında data türünden geçici nesne sonlandırılmaktadır.

data
~data
enter a number:

Şimdi geçici nesneye bir referans bağlayıp kodu yeninden derleyelim.

#include <iostream>

class data
{
public:
data() { std::cout << __func__ << '\n'; }
~data() { std::cout << __func__ << '\n'; }
};
int main()
{
const data& r = data{};
std::cout << "enter a number:\n";
getchar();
}

Bu durumda ancak klavyeden bir giriş yaptıktan sonra destructor fonksiyonunun çağrıldığını görüyoruz.

data
enter a number:
1
~data

data türünden geçici nesne r referansı hayatta olduğu sürece sonlandırılmayacaktır. Benzer şekilde bir initializer_list oluşturulmasına neden olan diziye ancak bu initializer_list nesnesi hayatta iken erişmek güvenlidir. Fakat geçici bir alanın bağlanan bir referans yoluyla ömrünün uzatılmasıyla ilgili bazı istisnalar bulunmaktadır. Aynı istisnalar initializer_list için de geçerlidir. Bu sebeple aşağıdaki kullanımlarda initalizer_list kullanmak güvenli değildir.

  • Fonksiyondan geri dönüş değeri olarak
#include <iostream>
#include <initializer_list>

std::initializer_list<int> foo()
{
std::initializer_list<int> l{1,2,3};
return l;
}

int main()
{
auto l = foo();
}

Örnek kod için g++ aşağıdaki uyarıyı üretmektedir.

g++ -otest test.cpp
test.cpp: In function ‘std::initializer_list<int> foo()’:
test.cpp:7:12: warning: returning local initializer_list variable ‘l’ does not extend the lifetime of the underlying array

Bir initializer_list nesnesinin gösterdiği dizinin başka bir initializer_list nesnesine geçirilerek ömrünün uzatılması da mümkün değildir. initializer_list türünün bir konteyner olmadığını hatırlayalım. Yukarıdaki örnekte foo fonksiyonu sonlandığında yığında bulunan initializer_list nesnesi sonlandırılacak ve gösterdiği içsel diziye erişmeye çalışmak tanımsız bir davranış (undefined behavior) oluşturacaktır.

  • initializer_list nesnesinin sınıfın üyesi olması

Aşağıdaki 3 örnek üzerinden bu duruma bakalım.

#include <iostream>
#include <initializer_list>

class data
{
std::initializer_list<int> l = {1,2,3};
data() {}
};

int main()
{}
clang++ -otest test.cpp
test.cpp:8:5: error: backing array for 'std::initializer_list' member 'l' is a temporary object whose lifetime would be shorter than the lifetime of the constructed object
#include <iostream>
#include <initializer_list>

class data
{
std::initializer_list<int> l;
data() :l{1, 2, 3} { }
};

int main()
{}
clang++ -otest test.cpp
test.cpp:8:14: error: backing array for 'std::initializer_list' member 'l' is a temporary object whose lifetime would be shorter than the lifetime of the constructed object
#include <iostream>
#include <initializer_list>

class data
{
std::initializer_list<int> l;
data() { l = {1, 2, 3}; }
};

int main()
{}
g++ -otest test.cpp
test.cpp: In constructor ‘data::data()’:
test.cpp:9:21: warning: assignment from temporary initializer_list does not extend the lifetime of the underlying array

Her 3 örnekte de {1, 2, 3} ifadesi için oluşturulan geçici içsel dizi data türünün constructor fonksiyonu bittiğinde ömrünü tamamlayacaktır. Buna karşın sınıfın bu adresi tutan initializer_list türünden elemanı sınıf türünden nesne hayatta olduğu sürece devam edecektir. Bu durumda ilk bakışta fark edilmemesine karşın yine bir tanımsız davranış oluşacaktır.

Aşağıdaki örnek kodu derleyerek bu durumu gözleyebiliriz.

#include <iostream>
#include <initializer_list>

struct X
{
public:
~X() { std::cout << __func__ << '\n'; }
};

class data
{
std::initializer_list<X> l = {X{}, X{}, X{}};

public:
size_t size() { return l.size(); }
};

int main()
{
data d;
std::cout << "size: " << d.size() << '\n';
}
~X
~X
~X
size: 3

data türünden d nesnesi hala hayatta olmasına karşın size metodu çağrılmadan önce initializer_list türünden veri elemanının tuttuğu X türünden nesnelerin sonlandırıldığını görüyoruz.

Not: Yukarıdaki örneklere g++ ve clang++ derleyicileri farklı hata ya da uyarı mesajları üretebilmektedir. Bazı durumlarda biri mesaj üretirken diğeri sessiz kalabilmektedir.

initializer_list nesnesine atama

Bir initializer_list nesnesine atama yapılması durumunda içsel dizinin sahipliği aktarılmadığından dolayı aşağıdaki örnekteki gibi bir kullanımda tanımsız davranış oluşmaktadır.

#include <iostream>
#include <initializer_list>

int main()
{
std::initializer_list<int> l = {1, 2, 3};
l = {4, 5};

int a[128] = {666, 666};

for (auto e : l) {
std::cout << e << ' ';
}
}
666 666

{4, 5} ifadesi için geçici bir initializer_list nesnesi oluşturulmakta ve bu nesnenin tuttuğu içsel dizinin adresi l listesine kopyalanmaktadır. Tüm ifade (full expression) sonunda bu geçici initializer_list nesnesi ve dolayısıyla içsel dizi sonlandırılır. l nesnesi bu aşamadan sonra geçersiz bir adres (dangling pointer) göstereceği için liste elemanlarına erişmek tanımsız davranış oluşturacaktır. g++ ve clang++ derleyicilerinin ürettikleri uyarı mesajları aşağıdadır.

clang++
note: temporary was destroyed at the end of the full expression
g++
warning: assignment from temporary initializer_list does not extend the lifetime of the underlying array

initializer_list elemanlarının kopyalanması

initializer_list’in gösterdiği içsel dizi const olduğu için elemanları ancak kopyalanabilir, taşınmaları ise mümkün değildir. Bu durum initializer_list kullanımında dezavantajlı bir durum oluşturabilmektedir.

Konteyner sınıflar bir initializer_list ile ilklendirilmek istendiğinde fazladan bir kopyalama işlemi yapılmaktadır. Ayrıca, unique_ptr gibi, kopyalamaya kapalı türler için oluşturulacak bir initializer_list nesnesi ile bir konteyner’ı ilklendirmek mümkün değildir. Aşağıdaki örnekler üzerinden bu durumlara bakalım.

#include <iostream>
#include <vector>

class data {
public:
data() { std::cout << "ctor\n"; }

data(const data&) { std::cout << "copy ctor\n"; }

data(data&&) { std::cout << "move ctor\n"; }

data& operator=(data&&) {
std::cout << "move assn\n";
return *this;
}

data& operator=(const data&) {
std::cout << "copy assn\n";
return *this;
}

~data() { std::cout << "dtor\n"; }
};

int main()
{
std::vector<data> v{data{}};
}
ctor
copy ctor
dtor
dtor

data türünden geçici bir nesne oluşturulduğunu ve sonrasında kopyalandığını görüyoruz.

vector’ü bir initializer_list ile ilklendirmek yerine push_back fonksiyonu ile değer atadığımızda ise kopyalama yerine bir taşıma işlemi gerçekleştirilmektedir. Özellikle kopyalama maliyeti yüksek türler için initializer_list kullanımı bu yönüyle bir dezvantaj oluşturabilir.

int main()
{
std::vector<data> v;
v.push_back(data{});
}
ctor
move ctor
dtor
dtor

Kopyalamaya kapalı türden nesneler bir initializer_list ile tutulabilmelerine karşın koyteyner sınıfların bu initializer_list nesneleri üzerinden ilklendirilebilmeleri mümkün değildir.

#include <iostream>
#include <memory>

int main()
{
std::initializer_list<std::unique_ptr<int>> l{ std::make_unique<int>(0),
std::make_unique<int>(1),
std::make_unique<int>(2) };
for (auto& e : l) {
std::cout << *e << ' ';
}
}
0 1 2

Döngü içerisinde initializer_list elemanlarına referans ile eriştiğimize dikkat ediniz.

vector’ü bir unique_ptr listesi ile ilklendirmek istediğimizde clang++ aşağıdaki hatayı üretmektedir. vector sınıfı constructor’ı içerisinde initializer_list elamanlarını kopyalamaya çalışmaktadır. Bu durum unique_ptr gibi, kopyalamaya kapalı türler için hata üretilmesine sebep olmaktadır.

#include <iostream>
#include <memory>
#include <vector>

int main()
{
std::vector<std::unique_ptr<int>> l{ std::make_unique<int>(0),
std::make_unique<int>(1),
std::make_unique<int>(2) };
}
error: call to deleted constructor of 'std::unique_ptr<int, std::default_delete<int> >'

Not: Burada içsel dizi const olduğundan dolayı std::move dönüşüm fonksiyonunu kullanmak herhangi bir şekilde sonucu değiştirmeyecektir.

narrowing (daraltıcı dönüşüm)

initializer_list içinde veri kaybı ile sonuçlanabilecek daraltıcı sayısal dönüşümlere izin verilmemektedir.

#include <iostream>
#include <initializer_list>

int main()
{
std::initializer_list<int> l{1, 2.};
}
error: narrowing conversion of ‘2.0e+0’ from ‘double’ to ‘int’ [-Wnarrowing]
6 | std::initializer_list<int> l{1, 2.};

tür çıkarımı

Süslü parantezler içindeki bir liste için tür çıkarımında (type deduction) farklı kurallar işletilmektedir. Tür çıkarımı yapılan ve yapılamayan tipik durumları örnekler üzerinden inceleyelim.

  • fonksiyon şablonları

Fonksiyon şablonları için derleyici bir tür çıkarımı yapmamaktadır.

#include <iostream>
#include <initializer_list>

template<class T>
void f(T)
{}

int main()
{
f({1, 2, 3});
}

Örnek kod için g++ ve clang++ derleyicilerinin ürettikleri hata mesajları aşağıdadır.

g++:
error: no matching function for call to ‘f(<brace-enclosed initializer list>)’
clang++:
error: no matching function for call to 'f'
f({1, 2, 3});
^
note: candidate template ignored: couldn't infer template argument 'T'
  • auto tür belirleyicisi

auto belirleyici ile ise, fonksiyonun geri dönüş türü hariç, bir tür çıkarımı yapılmaktadır.

#include <iostream>
#include <initializer_list>

int main()
{
auto l = {1, 2, 3};
for (auto e : l) {
std::cout << e << ' ';
}
}
1 2 3

Örnek kod için l’nin türü std::initializer_list<int> olarak belirlenmiştir. Bu durumu aşağıdaki kod ile doğrulayabiliriz.

#include <iostream>
#include <initializer_list>
#include <type_traits>

int main()
{
auto l = {1, 2, 3};
std::cout << std::boolalpha;
std::cout << std::is_same<std::initializer_list<int>, decltype(l)>::value << ' ';
}

Süslü parantez içindeki değer sayısı ve atama operatörünün kullanılıp kullanılmamasına göre tür çıkarımı farklı yapılmaktadır. Bir örnek üzerinden bu duruma bakalım.

#include <iostream>
#include <initializer_list>
#include <type_traits>

int main()
{
auto l1 = {1, 2}; //initializer_list
auto l2 = {1}; //initializer_list
std::cout << std::boolalpha;
std::cout << std::is_same<std::initializer_list<int>, decltype(l1)>::value << '\n';
std::cout << std::is_same<std::initializer_list<int>, decltype(l2)>::value << '\n';
}

C++17 ile beraber auto ile tür çıkarımında bir değişiklik yapılmıştır. Daha önce süslü parantez listesi için tür çıkarımı initializer_list olarak yapılırken C++17 ile beraber ilklendirmede eşitlik operatörü kullanılmamışsa (direct initialization) tür çıkarımı süslü parantez içindeki değerin türü üzerinden yapılır. Bu ilklendirme biçiminde süslü parantezler içinde tek bir değer bulunmalıdır.

Örnek kodlar üzerinden bu durumlara bakalım.

#include <iostream>
#include <initializer_list>
#include <type_traits>

int main()
{
auto l1 = {1}; //initializer_list
auto l2{1}; //int
std::cout << std::boolalpha;
std::cout << std::is_same<std::initializer_list<int>, decltype(l1)>::value << ' ';
std::cout << std::is_same<int, decltype(l2)>::value << ' ';
}
true true
#include <iostream>
#include <initializer_list>
#include <type_traits>

int main()
{
auto l{1, 2}; // error
}
error: direct-list-initialization of ‘auto’ requires exactly one element

auto tür belirleyicisinin bir fonksiyonun geri dönüş türü yerine kullanılması durumunda ise süslü parantez listesi için tür çıkarımı yapılmamaktadır.

#include <iostream>
#include <initializer_list>

auto f()
{
return {1, 2, 3};
};

int main()
{}

g++ ve clang++ derleyicilerinin ürettikleri hatalar aşağıdadır.

g++:
error: returning initializer list
clang++:
error: cannot deduce return type from initializer list

Benzer şekilde bir lambda ifadesinden de süslü parantez listesi dönmek geçersizdir.

#include <iostream>
#include <initializer_list>

int main()
{
[]() { return {1, 2}; };
}
error: cannot deduce lambda return type from initializer list

Not: Bir fonksiyondan süslü parantezler içindeki değerleri dönmenin sakıncasından daha önce bahsetmiştik. Fonksiyon döndükten sonra süslü parantez içindeki değerlerin tutulduğu içsel diziye erişmek güvensiz olacaktır. Bu durumda derleyici bu tür bir koda izin verseydi tanımsız bir davranış olacaktı.

fonksiyonların yüklenmesi (function overloading)

initializer_list parametreli fonksiyonlar diğer fonksiyonlarla beraber yüklendiklerinde (overloaded) derleyici bir fonksiyon çağrısında süslü parantezlerle karşılaştığında fonksiyon seçiminde (overload resolution) initializer_list parametreli fonksiyonlara öncelik vermektedir.

Aşağıdaki örnek üzerinden fonksiyon yüklemeyi inceleyelim.

#include <iostream>
#include <initializer_list>

struct data
{
data() { std::cout << __PRETTY_FUNCTION__ << '\n'; }
data(int) { std::cout << __PRETTY_FUNCTION__ << '\n'; }
data(int, int) { std::cout << __PRETTY_FUNCTION__ << '\n'; }
data(std::initializer_list<int>) { std::cout << __PRETTY_FUNCTION__ << '\n'; }
};

int main()
{
data d1; //1 default constructor
data d2{}; //2 default constructor
data d3 = {}; //3 default constructor
data d4({}); //4 initializer_list constructor
data d5{1}; //5 initializer_list constructor
data d6(1); //6 int constructor
data d7(1, 2); //7 Parameterized constructor
data d8{1, 2}; //8 initializer_list constructor
}
data::data()
data::data()
data::data()
data::data(std::initializer_list<int>)
data::data(std::initializer_list<int>)
data::data(int)
data::data(int, int)
data::data(std::initializer_list<int>)

1. ilklendirmede deafult constructor seçilmiştir.

2. ve 3. ilklendirmelerde süslü parantezler kullanılmasına karşın, herhangi bir değer geçirilmediğinden dolayı, default constructor seçilmiştir.

4. ilklendirme de ise süslü parantezlerin içi boş olmasına karşın derleyiciye açık bir şekilde initializer_list parametreli constructor’ı çağırması gerektiği bilgisi geçirilmiştir. initializer_list hiçbir elemana sahip olmayıp boş olabilir.

5. ve 6. ilklendirmelerde sırasıyla initializer_list ve int parametreli constructor’lar seçilmiştir.

7. ve 8. ilklendirmelerde sırasıyla iki int ve initializer_list parametreli constructor’lar seçilmiştir.

Süslü ve normal parantezlerin ilklendirme işlemlerinde kullanılması bazı durumlarda kodun kullanıcı tarafından yorumlanmasını güçleştirebilmektedir.

Aşağıdaki örnekte ilk olarak 10 sonrasında ise 2 elamanlı birer vector oluşturulmuştur.

#include <iostream>
#include <vector>

int main()
{
std::vector v1(10, 1); //size: 10
std::vector v2{10, 1}; //size: 2

for (auto e : v1) {
std::cout << e << ' ';
}

std::cout << '\n';
for (auto e : v2) {
std::cout << e << ' ';
}
}
1 1 1 1 1 1 1 1 1 1 
10 1

v1 ve v2 vector’leri için sırasıyla aşağıdaki constructor fonksiyonları çağrılmıştır.

vector(size_type __n, const value_type& __value,
const allocator_type& __a = allocator_type())
vector(initializer_list<value_type> __l,
const allocator_type& __a = allocator_type())

alternatif yöntemler

initializer_list yerine, aynı amaç için, dilin diğer özelliklerini kullanmak da mümkün. Bu amaçla şablon (template) kodlar kullanılabilir. Kısaca bu yöntemlere bakalım.

  • şablon kodlarda dizi referansı kullanımı
#include <iostream>

template<size_t N>
void foo(int const (&param) [N])
{
for (int i = 0; i < N; ++i) {
std::cout << param[i] << ' ';
}
}

int main()
{
foo({1, 2, 3});
}

foo fonksiyonuna süslü parantezler içinde geçirilen değerler için geçici bir dizi oluşturulmakta ve foo fonksiyonun const dizi türünden referans parametresi bu geçici diziye bağlanmaktadır. foo içinde dizinin tüm elemanlarına erişilebilir.

Benzer şekilde, değişken sayıda argüman ile ilklendirilebilen, bir tür şablonu da yazılabilir. STL içindeki std::array şablonu da bu şekilde yazılmıştır. std::array şablonu aşağıdaki örneğe benzer şekilde gerçeklenmiştir.

#include <iostream>

namespace {
template<typename T, std::size_t N>
struct array
{
T _M_elems[N];
T* begin() { return _M_elems; }
T* end() { return _M_elems + N; }
};
}

int main()
{
array<int, 3> arr{1,2,3};
for (auto e : arr) {
std::cout << e << ' ';
}
}
  • variadic şablon kullanımı
#include <iostream>

template<typename... T>
void foo(T... args)
{
((std::cout << args << ' '), ...);
}

int main()
{
foo(1, 2, 3);
}

foo fonksiyonuna geçirilen değerler için derleyici tür çıkarımı yapmakta ve değişken sayıda değer foo fonksiyonuna geçirilmektedir.

Bu her iki yöntemle de değişken sayıda elemandan oluşan argümanlar liste şeklinde fonksiyonlara geçirebilmesine karşın fonksiyonların şablon olma zorunluluğundan dolayı bazı dez avantajları bulunmaktadır. Bu dez avantajları aşağıdaki gibi sıralayabiliriz.

  • Farklı sayıda argüman ile yapılan her çağrı için derleyici ayrı bir fonksiyon kodu üretmelidir. Bu durumda amaç kod (object code) büyüyecektir (code bloat).
  • Fonksiyon şablonlarından ilk olarak gerçek fonksiyonlar üretilmelidir. Bu üretme işlemi bir fonksiyon çağrısı yapıldığında olabildiği gibi öncesine açık bir şekilde de (explicit template instantiation) yapılabilir. Bir fonksiyon şablonundan üretilebilecek tüm gerçek fonksiyonların önceden açık bir şekilde üretilmesi mümkün ya da pratik olmadığından fonksiyon şablonları sanal (virtual) olamazlar. Aynı nedenden şablon kodları, gerçek fonksiyonların aksine, direkt olarak diğer fonksiyonlara callback olarak geçirmek veya dinamik kütüphanelerde kullanmak mümkün değildir. Ancak belli türler için bu şablonlardan oluşturulan gerçek fonksiyonlar bu amaçlarla kullanılabilir.

Bu sebeplerden dolayı STL içindeki değişken sayıda argüman alabilen her fonksiyonun şablon olması istenmemiştir. Onun yerine dile std::initializer aracı eklenmiştir.

özet

Bu yazımızda std::initializer_list aracını incelemeye çalıştık. Oldukça faydalı olan bu araç bazı dezavantajlarlı da barındırmaktadır. Özellikle bir fonksiyondan geri dönüş değeri olarak döndürülmesi ya da sınıfın üye değişkeni olması durumlarına dikkat edilmedilir. Bazı durumlarda fazladan yapılan kopyalama ve std::unique gibi kopyalamaya kapalı türlerle kullanımının kısıtlı olması da yine bir dez avantaj oluşturmaktadır.

referanslar

https://www.open-std.org/JTC1/sc22/wg21/docs/papers/2007/n2215.pdf

https://tristanbrindle.com/posts/beware-copies-initializer-list

https://blog.feabhas.com/2015/08/bitesize-modern-c-stdinitializer_list/

https://akrzemi1.wordpress.com/2016/07/07/the-cost-of-stdinitializer_list/

--

--