C++ enable_if & SFINAE nedir ?

Gorkem Demirtas
4 min readFeb 20, 2023

--

Giriş: Templates
Template(Şablonlar) sayesinde birbirinin alternatifi kodları tekrar yazma gereği duymadan derleme zamanında verilen tipe göre çalışacak kodlar yazabiliyoruz örnek vermek gerekirse;

int sum(int num1, int num2) {
return num1 + num2;
}

fonksiyonunu float veri tipi ile çağırıyor olsaydık aynı şekilde yine bir hata ile karşılaşmayacaktık bunun sebebi float veri tipinin derleyici tarafından örtük(Implicit) olarak integer’a cast ediliyor olması fakat aldığımız sonuç yine int tipinde olacaktı bu durumu çözmek için sum fonksiyonunu aşırı yüklememiz(overload) gerekiyor

float sum(float num1, float num2) {
return num1 + num2;
}

ya başka bir veri tipi ile daha çalışmasını istiyor olsaydık ? bu tür durumlarda templateleri kullanıyoruz.

template<typename T>
T sum(T num1, T num2) {
return num1 + num2;
}

yukarıdaki fonksiyon çağırıldığında derleme zamanında girilen parametrelere göre T yerine uygun veri tipi derleyici tarafından konulacak(substitution).
fonksiyonu
sum(1,2) şeklinde çağırsaydık;

int sum(int num1, int num2) {
return num1 + num2;
}

gibi bir kod üretilecekti. Şuana kadar herşey yolunda fakat örneğimizi biraz değiştirerek sum fonksiyonu ile gelen STL konteynırlarının eleman sayılarının toplamını yazdıralım

template<typename T>
size_t sum(T x, T y) {
return x.size() + y.size();
}

int main() {
std::vector<int> v = {1,2,3,4,5};
std::vector<int> v2 = {6,7,8,9,10};

std::cout << sum(v,v2) << std::endl; //10
}

Yukarıdaki örnek çalıştırıldığında 10 çıktısını vericektir, ama aynı örnek
sum(1,2) veya public bir size() üye fonksiyonuna sahip olmayan bir veri tipi ile çağırıldığında(aynı şekilde size fonksiyonundan dönen tipin + operatörünün durumuna da bağlı olarak) derleme hatası alırız bu gibi durumlarda templateleri sınırlandırmamız gerekiyor.

enable_if & SFINAE
enable_if’i öğrenmeden önce C++ da ki SFINAE prensibini anlamamız gerekiyor.
SFINAE(Substitution Failure is not an Error) yani yerleştirmede başarısızlık bir hata değildir. Overload edilmiş template fonksiyonlarda derleyici derleme zamanında girilen parametlerlerden çıkarım yaparak yerleştirme işlemini yapar bu işlem sırasında bir hata meydana gelirse öncelikle diğer şablon aşırı yüklemelerine bakılır geçerli bir şablon türü bulunursa o kullanılır aksi halde hata verir.

az önceki örneğimize geri dönersek sum(1) şeklinde çağırdımız zaman derleme hatası ile karşılaşıyorduk şimdi aynı örneğe geri dönerek sum fonksiyonunu aşırı yükleyelim

template<typename T>
size_t sum(T x, T y) {
return x.size() + y.size();
}

//..

size_t sum(int x, int y) {
return x + y;
}

kodu sum(1) olarak çağırılıp derlendiğinde her iki örnekte derleyici için başlangıçta uygun çünkü T her veri tipini karşılıyor fakat int türünün size() üye fonksiyonu olmadığı ve bir hataya yol açacağı için bu örnek yok sayılıp 2. örnek kullanılacak. Bu sadece int veri tipi için geçerli float bir değer girersek tekrardan hata ile karşılaşacağız, henüz tam olarak istediğimizi alamadık şimdi işin en eğlenceli yerine geliyoruz.

std::enable_if
Templateler çok güçlü bir yapı, fakat girilen her veri tipini alıyor olması sorun yaratabiliyor işte tamda burada imdadımıza c++11 ile gelen std::enable_if sınıfı yetişiyor bu sayede şablonları farklı özelliklere sahip veri türlerine göre sınırlarken generic fonksiyon ve sınıflar yazmaya devam edebiliyoruz.

template< bool B, class T = void >
struct enable_if;

using enable_if_t = typename enable_if<B,T>::type; //C++14

şeklinde implemente edilebilir;

template<bool B, class T = void>
struct enable_if {};

template<class T>
struct enable_if<true, T> { typedef T type; };

bir tanesi opsiyonel olmak üzere iki adet paremetre alır, bu parametrelerin ilki koşul ikincisi ise tiptir. Eğer koşul sağlanırsa T tipinde public type tanımlanır.

Birkaç farklı şekilde kullanılabilir;

  • Fonksiyon parametresi olarak(sabit sayıdaki operatör aşırı yüklemeleri hariç)
  • Dönüş tipi olarak(Constructor ve Destructorlar dönüş tipinde sahip olmadıkları için parametre olarak kullanılmalıdır)
  • Fonksiyon veya sınıf şablon parametresi olarak

Şimdi birkaç somut örnek verip onları incelersek daha iyi anlayacağız ama öncesinde bilmeyenler için kısaca is_integral sınıfından bahsetmek istiyorum,
aslında kendisi basit bir özellik sınıfı girilen veri tipi eğer bir tamsayı ise sahip olduğu
value özelliği true oluyor. Yazımızın sonraki bölümünde de bir benzerini biz implemente ediyor olacağız.
Örneğimize geçecek olursak;

template<typename T>
typename std::enable_if<
std::is_integral<T>::value, T> >::type sum(T num1, T num2) {

return num1 + num2;
}

// veya

template<typename T>
T sum(T num1, T num2,
typename std::enable_if<std::is_integral<T>::value> >* = 0) {

return num1 + num2;
}

sum fonksiyonuna parametre olarak tam sayı veri tipi verilirse, şablonumuzdaki T tipi öncelikle is_integral’e parametre olarak geçecek ve T tipi bir tam sayı tipi olduğu için value değeri true oluyor olacak buradan elde ettiğimiz değer ile enable_if yapımızın koşulunu sağlamış olacağız ve aynı şekilde yine T tipi ile type eşit olacağı için(yukarıdaki implementasyondan hatırlayalım) sum fonksiyonumuz aslında gelen veri tipini döndürmüş olacak.
Diyelim ki bir tam sayı girmedik bu senaryoda ise type tanımlı olmayacağı için başka uygun bir aşırı yükleme yoksa derleme sırasında hata alacağız.

ikinci örnekte de benzer bir durum geçerli tek fark dönüş tipini T olarak verip default değeri olan ekstra parametre eklememiz bu sayede bu örnekte sum fonksiyonu 3 parametreye sahip olmasına rağmen 3. parametre default değere sahip olduğu için bu parametre belirtilmeden fonksiyon kullanılabiliyor.

Şimdi ise öğrendiklerimizi pekiştirerek yukarıda verdiğimiz örneklerdeki hataları düzeltelim. Öncelikle yine sum fonksiyonu üzerinden gidelim parametre olarak tam sayı girildiğinde girilen değerlerin toplamını döndürelim eğer ki parametre olarak girilen değer bir STL konteyner sınıfı ise eleman sayılarını alalım ayrıca bu durum için is_container implementasyonu yazalım.

template<typename T>
struct is_container
{
static const bool value = false;
};

template<typename T>
struct is_container<std::vector<T>>
{
static const bool value = true;
};

template<typename T, typename U>
struct is_container<std::map<T,U>>
{
static const bool value = true;
};


template<typename T>
T sum(T num1, T num2,
typename std::enable_if<std::is_integral<T>::value, T>::type* = 0)
{
return num1 + num2;
}

template<class T>
size_t sum(T x, T y,
typename std::enable_if<is_container<T>::value, T>::type* = 0)
{
return x.size() + y.size();
}

int main()
{
std::vector<int> v = {1,2};
std::vector<int> v2 = {1,2};

std::map<int,int> m = {std::pair<int,int>(1,2)};
std::map<int,int> m2(m);

std::cout << sum(1,2) << std::endl; //3
std::cout << sum(v,v2) << std::endl; //4
std::cout << sum(m,m2) << std::endl; //2
}

Son olarak kısaca std::vector sınıfının yapıcı(constructor) methodlarına bakalım

vector (size_type n, const value_type& val = value_type());

template <class InputIterator>
vector (InputIterator first, InputIterator last);

2. yapıcı method template olduğu için herhangi özel birşey yapmamız durumunda gelen her veri tipini kabul ediyor yani
std::vector<int> v(1,2);
şeklinde bir kulanım için 2. yapıcı çağırılmış olacak, bunun sebebi 1 int veri tipi size_t karşılamıyor diğer yapıcı içinse bu durum geçerli değil parametre olarak InputIterator alıyor olması derleyici için birşey ifade etmiyor.

Kaynaklar

--

--