C++ Dilinde noexcept Specifier
- Yusuf Hançar
- Jun 6, 2024
- 9 min read
Updated: Jun 10, 2024
C++11 standardı ile birlikte, throw anahtar kelimesiyle belirli istisnaların fırlatılabileceğini bildirme yöntemi yerini noexcept anahtar kelimesine bırakmıştır. noexcept specifier, bir fonksiyonun exception fırlatmayacağını belirtmek için kullanılır ve bu da derleyicinin daha iyi optimizasyonlar yapmasına olanak tanır. Bu durum özellikle taşıma semantiği ve move constructor, move assignment operatörleri ile ilgili olarak önemlidir.
C++98/03 standartlarında, bir fonksiyonun belirli türde istisnalar fırlatabileceğini belirtmek için throw anahtar kelimesi kullanılırdı.
#include <new>
void func() throw(std::bad_alloc)
{
// codes
}
*****************************
AÇIKLAMA: func fonksiyonunun yalnızca std::bad_alloc sınıfı türünden exception fırlatabileceğini ve başka türde bir istisna fırlatmayacağını belirtir.
*****************************
C++11 ile noexcept bu durumu değiştirdi.
void func() noexcept
{
// codes
}
*****************************
AÇIKLAMA: exception throw etmeme garantisi verir.
*****************************
noexcept(condition) kullanımında condition ifadesi logic olarak derleme zamanında(compile-time) hesaplanarak yorumlanır. Default olarak true olarak değerlendirilir.
void func() noexcept(false) {}
*****************************
AÇIKLAMA: exception throw edebilir olarak değerlendirilir.
*****************************
void func() noexcept(true) {}
*****************************
AÇIKLAMA: exception throw etmeme garantisi verir.
*****************************
void func() noexcept(sizeof(int) == 4) {}
******************************
AÇIKLAMA : int türünün sizeof değeri 4 ise exception throw etmeme garantisi verir.
******************************
std::is_nothrow_constructible_v (C++11) -> https://en.cppreference.com/w/cpp/types/is_constructible
template <typename E>
void func(E) noexcept(std::is_nothrow_constructible_v<E>);
******************************
AÇIKLAMA : E türünün constructor'ı exception throw etmeme garantsi veriyorsa bizde exception throw etmeme garantisi vermiş oluruz.
******************************
#include <iostream>
#include <type_traits>
template <typename E>
void func(E param) noexcept(std::is_nothrow_constructible_v<E>)
{
std::cout << "Called\n";
}
struct nothrow_ctor
{
nothrow_ctor() noexcept {} // exception throw etmeyecek.
};
struct throw_ctor
{
throw_ctor() {} // exception throw edebilir.
};
int main()
{
nothrow_ctor nothrow_obj;
throw_ctor throw_obj;
func(nothrow_obj); // noexcept olur çünkü nothrow_ctor noexcept ctor'a sahip.
func(throw_obj); // noexcept olmaz çünkü throw_ctor noexcept olmayan bir ctor'a sahip.
return 0;
}
template <typename E>
void func(E val) noexcept(noexcept(val * val));
******************************
AÇIKLAMA : dışarıdaki specifier, içerideki operator olarak ele alınır. noexcept operatorunun de işlem kodu üretilmeyen bağlam kodu(unevaluated context) oluşturur.
******************************
noexcept specifier, derleyiciye belirli bir ifadenin veya fonksiyonun bir exception fırlatıp fırlatmayacağını değerlendirmesi gerektiğini belirtir. Ancak, noexcept ifadesinin kendisi bir derleme zamanı ifadesidir, yani çalışma zamanında gerçekleşmez. Bunun yerine, derleme sırasında belirli bir kod parçasının değerini belirlemek için kullanılır. Bu tür ifadeler "unevaluated context" olarak adlandırılır. noexcept ifadesi kullanıldığında, ifade veya fonksiyon belirli bir değer üretmeyecek ve işlem kodu üretilmeyecektir. Sadece derleyici, ifadeyi değerlendirmek zorunda kalmadan, ifadenin veya fonksiyonun istisna fırlatıp fırlatmayacağını belirlemek için uygun kod üretir. Bu kullanım performansı artırabilir, çünkü kodun gerçekten çalıştırılmasına gerek kalmadan istisna güvenliği hakkında bilgi edinilir.
noexcept artık fonksiyonların imzasının bir parçası ancak function overloading için kullanılamamaktadır.
void func(type) noexcept;
void func(type);
******************************
AÇIKLAMA : redecleration (syntax error)
******************************
void func(int) noexcept{}
int main()
{
void (*fp)(int) = func;
constexpr auto val = noexcept(fp(24));
}
******************************
AÇIKLAMA : compile time da val false olur.
******************************
void func(int) noexcept{}
int main()
{
void (*fp)(int)noexcept = func;
constexpr auto val = noexcept(fp(24));
}
******************************
AÇIKLAMA : fonksiyon çağrıları exception throw etmeme garantisi verir ve true döner.
******************************
void func(int){}
int main()
{
void (*fp)(int)noexcept = func;
}
******************************
CEVAP : syntax error.
******************************
AÇIKLAMA : fp kullanılarak yapılacak çağrılar exception throw etmez. ancak func exception throw etme ihtimali vardır.
******************************
void func(int) noexcept{}
int main()
{
auto fn = func;
constexper bool val = noexcept(fn(23));
}
******************************
CEVAP : true değer üretir.
******************************
C++ dilinde virtual fonksiyonların override edilmesi, taban sınıfın fonksiyon imzasının korunması ile ilgilidir. Taban sınıfın verdiği garantileri korumak ve türetilmiş sınıfların bu garantileri aşmaması gerekmektedir.
class Base {
public:
virtual void func(int){}
};
class Derive : public Base {
public:
void func(int) noexcept override{}
};
int main()
{
Derive drv;
drv.func(24);
}
******************************
CEVAP : legal
******************************
AÇIKLAMA : Base sınıftaki func fonksiyonunda noexcept kullanılmamış, türetilmiş sınıf Derive içindeki func fonksiyonu noexcept ile tanımlanmıştır. Bu durum, SOLID prensiplerinden biri olan Liskov Substitution Prensibi'ne uygundur. Bu prensip, türetilmiş sınıfların, taban sınıfın yerine kullanılabileceğini garanti eder. noexcept kullanımı, fonksiyonun hata fırlatmayacağını garanti ederek taban sınıfın verdiği garantiden daha fazlasını sunar, yani daha az risk içerir. Bu nedenle bu kod geçerlidir.
******************************
class Base {
public:
virtual void func(int)noexcept{}
};
class Derive : public Base {
public:
void func(int) override{}
};
int main()
{
Derive drv;
drv.func(24);
}
******************************
CEVAP : syntax error
******************************
AÇIKLAMA : Base sınıfındaki func fonksiyonu noexcept olarak tanımlanmış, yani bu fonksiyonun hata fırlatmayacağı garanti edilmiştir. Ancak, Derive sınıfındaki func fonksiyonu noexcept belirtimi olmadan tanımlanmıştır. Bu durum, türetilmiş sınıfın fonksiyonunun taban sınıfın fonksiyonundan farklı bir garanti vermesi anlamına gelir ve bu da geçerli değildir. Taban sınıfın fonksiyonu noexcept olduğu için türetilmiş sınıfın fonksiyonu da aynı garantiyi vermelidir. Bu nedenle bu kod geçerli değildir ve derleme hatası verir.
******************************
Özellikle sınıfların move constructor'ları, move assignment operator'ları ve swap fonksiyonlarının memory reallocation fonksiyonlarının beklenen biçimde noexcept garantisi vermesi gerekir. Bu garantiler, programın verimliliğini artırmak ve exception güvenliği sağlamak açısından önemlidir.
Move Constructor, Move Assignment Operator ve Swap fonksiyonlarının noexcept garantisi vermesi, container sınıflarının (örneğin, std::vector, std::list) verimli bir şekilde çalışabilmesi için kritiktir. Bu fonksiyonların noexcept olması, konteynerlerin elemanlarını taşırken (move) veya swap ederken exception fırlatmayacaklarını garanti eder.
Bir sınıfın destructor'ı noexcept olarak bildirilmemiş olsa bile, bu destructor implicitly olarak noexcept kabul edilir. Yani, destructor'lar varsayılan olarak noexcepttir. Bu, destructor'ların exception fırlatmaması gerektiği anlamına gelir ve exception güvenliği açısından önemlidir.
Eğer özel üye fonksiyonlar (constructor, destructor, copy/move constructor, copy/move assignment operator) derleyici tarafından yazılıyorsa, derleyici bu fonksiyonların noexcept olup olmadığını statik olarak koda bakıp belirleyebilir. Bu durum, derleyicinin özel üye fonksiyonların exception güvenliği hakkında bilgi sahibi olmasını sağlar ve bu bilgiye göre optimizasyonlar yapabilir.
class SmartCode {};
int main()
{
using namespace std;
cout.setf(ios::boolalpha);
cout << "is def ctor noexcept : " << is_nothrow_default_constructible_v<SmartCode> << "\n";
cout << "is dtor noexcept : " << is_nothrow_destructible_v<SmartCode> << "\n";
cout << "is copy ctor noexcept : " << is_nothrow_copy_constructible_v<SmartCode> << "\n";
cout << "is move ctor noexcept : " << is_nothrow_move_constructible_v<SmartCode> << "\n";
cout << "is copy assignment noexcept : " << is_nothrow_copy_assignable_v<SmartCode> << "\n";
cout << "is move assignment noexcept : " << is_nothrow_move_assignable_v<SmartCode>;
}
******************************
CEVAP :
true
true
true
true
true
true
******************************
class SmartCode {
public :
std::string str{};
};
int main()
{
using namespace std;
cout.setf(ios::boolalpha);
cout << "is def ctor noexcept : " << is_nothrow_default_constructible_v<SmartCode> << "\n";
cout << "is dtor noexcept : " << is_nothrow_destructible_v<SmartCode> << "\n";
cout << "is copy ctor noexcept : " << is_nothrow_copy_constructible_v<SmartCode> << "\n";
cout << "is move ctor noexcept : " << is_nothrow_move_constructible_v<SmartCode> << "\n";
cout << "is copy assignment noexcept : " << is_nothrow_copy_assignable_v<SmartCode> << "\n";
cout << "is move assignment noexcept : " << is_nothrow_move_assignable_v<SmartCode>;
}
******************************
AÇIKLAMA : string sınıfının copy memberları bu garantiyi vermediğinden derleyici de vermedi. SmartCode sınıfı bir std::string üyesi (str) içerdiğinden, std::string sınıfının copy constructor ve copy assignment operator'ları noexcept garantisi vermez. Bu nedenle, SmartCode sınıfının copy constructor ve copy assignment operator'ları da noexcept garantisi vermez. Ancak, std::string sınıfının move constructor ve move assignment operator'ları noexcept garantisi verdiği için, SmartCode sınıfının move constructor ve move assignment operator'ları da noexcept garantisi verir.
******************************
CEVAP :
true
true
false
true
false
true
******************************
class Data{};
class SmartCode {
public :
Data str{}{}
};
int main()
{
using namespace std;
cout.setf(ios::boolalpha);
cout << "is def ctor noexcept : " << is_nothrow_default_constructible_v<SmartCode> << "\n";
cout << "is dtor noexcept : " << is_nothrow_destructible_v<SmartCode> << "\n";
cout << "is copy ctor noexcept : " << is_nothrow_copy_constructible_v<SmartCode> << "\n";
cout << "is move ctor noexcept : " << is_nothrow_move_constructible_v<SmartCode> << "\n";
cout << "is copy assignment noexcept : " << is_nothrow_copy_assignable_v<SmartCode> << "\n";
cout << "is move assignment noexcept : " << is_nothrow_move_assignable_v<SmartCode>;
}
******************************
AÇIKLAMA : Data sınıfı bu garantiyi verdiği için hepsi bu garantiyi verdi.
******************************
CEVAP :
true
true
true
true
true
true
******************************
class Data{
public :
Data();
};
...
******************************
AÇIKLAMA : Data sınıfının default constructor'ı programcı tarafından yazıldığı ve noexcept garantisi vermediğinden default ctor false olacaktır.
******************************
class Data {
public :
Data(){}
};
using namespace std;
class SmartCode {
public :
static_assert(is_nothrow_default_constructible_v<SmartCode>);
Data str{};
};
int main()
{
cout.setf(ios::boolalpha);
cout << "is def ctor noexcept : " << is_nothrow_default_constructible_v<SmartCode> << "\n";
cout << "is dtor noexcept : " << is_nothrow_destructible_v<SmartCode> << "\n";
cout << "is copy ctor noexcept : " << is_nothrow_copy_constructible_v<SmartCode> << "\n";
cout << "is move ctor noexcept : " << is_nothrow_move_constructible_v<SmartCode> << "\n";
cout << "is copy assignment noexcept : " << is_nothrow_copy_assignable_v<SmartCode> << "\n";
cout << "is move assignment noexcept : " << is_nothrow_move_assignable_v<SmartCode>;
}
******************************
CEVAP : syntax error
******************************
AÇIKLAMA : static_assert sayesinde garantiye alındı.
******************************
move constructor noexcept olarak tanımlanması !!!
#include <iostream>
#include <vector>
using namespace std;
class Data {
public :
Data(std::string name) : m_name{name} {}
std::string get_name() const
{
return m_name;
}
Data(const Data& oth) : m_name{oth.m_name}
{
cout << "copy " << m_name << endl;
}
Data(Data&& oth) : m_name{ std::move(oth.m_name) }
{
cout << "move " << m_name << endl;
}
private:
std::string m_name;
};
int main()
{
std::vector<Data> dvec { "first"s, "sec"s, "third"s };
cout << "capacity : " << dvec.capacity() << "\n";
}
******************************
CEVAP : copy first
copy sec
copy third
capacity : 3
******************************
AÇIKLAMA :
******************************
int main()
{
std::vector<Data> dvec { "first"s, "sec"s, "third"s };
cout << "capacity : " << dvec.capacity() << "\n";
dvec.push_back("fourth");
cout << "capacity : " << dvec.capacity() << "\n";
}
******************************
CEVAP : copy first
copy sec
copy third
capacity : 3
move fourth
copy first
copy sec
copy third
capacity : 6
******************************
AÇIKLAMA : first sec third öğeleri reallocation sırasında eski bellek alanından yeni bellek alanına aktarılırken copy ctor çağırıldı. std::string sınıfı için copy ve move ctor arasından ciddi bir maliyet farkı vardır. Biri pointerları kopyalar diğeri doğrudan yazıyı kopyalar. move ctor noexcept olmadığı için bu durum gerçekleşir.
-> dvec.push_back("fourth"s); ifadesi, "fourth" değerini vektöre eklemeye çalışır. Mevcut kapasite "3" dolduğu için vektörün kapasitesinin artırılması gerekir. STL std::vector kapasitesini genellikle iki katına çıkarır ve 6 olarak hesaplanır. Kapasite artırıldığında, mevcut elemanlar yeni bellek alanına taşınır. Bu, move constructor kullanılarak yapılır. Bu nedenle, "move first", "move sec", "move third" mesajları ekrana yazdırılır. Yeni eklenen eleman "fourth" ise copy constructor kullanılarak eklenir, çünkü yeni bir std::string oluşturuluyor.
******************************
C++ standardında, std::vector ve diğer birçok STL konteynerleri, elemanlarını resize veya reallocate, mümkün olduğunda move constructor kullanmaya çalışır. Ancak, bu move constructor'ın noexcept olması beklenir. Çünkü, noexcept değilse ve bir move işlemi sırasında bir exception oluşursa, konteynerin durumu bozulabilir. Bu durumda, güvenlik açısından move constructor kullanmak yerine copy constructor tercih edilebilir.
#include <iostream>
#include <vector>
using namespace std;
class Data {
public :
Data(std::string name) : m_name{name} {}
std::string get_name() const
{
return m_name;
}
Data(const Data& oth) : m_name{oth.m_name}
{
cout << "copy " << m_name << endl;
}
Data(Data&& oth) noexcept : m_name{ std::move(oth.m_name) }
{
cout << "move " << m_name << endl;
}
private:
std::string m_name;
};
int main()
{
std::vector<Data> dvec { "first"s, "sec"s, "third"s };
cout << "capacity : " << dvec.capacity() << "\n";
dvec.push_back("fourth");
cout << "capacity : " << dvec.capacity() << "\n";
}
******************************
CEVAP : copy first
copy sec
copy third
capacity : 3
move fourth
move first
move sec
move third
capacity : 6
******************************
AÇIKLAMA : push_back strong guarenta sunar.
******************************
If an exception is thrown (which can be due to Allocator::allocate() or element copy/move constructor/assignment), this function has no effect (strong exception guarantee). "https://en.cppreference.com/w/cpp/container/vector/push_back"
Copy constructor'ın strong guarantee (güçlü garanti) sağlamasında herhangi bir engel yoktur. Çünkü copy constructor, yeni bellek alanındaki öğeleri önce hayata getirir ve eğer bir exception fırlatılırsa, bu öğeler destroy edilir. Dolayısıyla, diğer nesnenin kaynağında herhangi bir değişiklik olmaz.
Move constructor noexcept olmadığında neden strong guarantee sağlayamaz?
Eski bellek alanındaki öğeler, yeni bellek alanına taşınır ve taşınma işlemi sırasında kaynak çalınır. push_back fonksiyonu strong exception guarantee (güçlü istisna garantisi) verdiği için, move constructor noexcept olmazsa, strong guarantee sağlamak için kopyalama yapmak zorunda kalırız. Taşıma yaparsak, strong guarantee sağlama şansımız kalmayacaktır.
#include <iostream>
#include <vector>
#include <chrono>
using namespace std;
using namespace std::chrono;
class SmartCode {
public :
SmartCode() : m_str(1000, 'A') {}
private :
std::string m_str;
};
int main()
{
vector<SmartCode> vec(1'000'000);
cout << "vec.capacity() : " << vec.capacity() << "\n";
auto tp_start = steady_clock::now();
vec.reserve(vec.capacity() + 1);
auto tp_end = steady_clock::now();
cout << duration<double, milli>(tp_end - tp_start).count() << "\n";
cout << "vec.capacity() : " << vec.capacity() << "\n";
}
******************************
CEVAP :
vec.capacity() : 1000000
118.012
vec.capacity() : 1000001
******************************
AÇIKLAMA : kapasite arttığına göre eski bellek alanındaki öğelerin yeni bellek alanına taşınması gerekir.
******************************
vec.reserve(vec.capacity() + 1); ifadesi, mevcut kapasitenin artırılması gerektiğinde tüm elemanların yeni bir bellek alanına taşınmasını gerektirir. Bu işlem sırasında SmartCode nesnelerinin taşınması (move) veya kopyalanması (copy) gerekir. SmartCode sınıfının move constructor'ının noexcept olup olmaması bu işlem için önemlidir. Eğer move constructor noexcept değilse, std::vector kopyalama işlemini tercih eder çünkü taşınma işlemi sırasında bir istisna fırlatılabilir. Yukarıda da özetlediğimiz gibi, std::vector gibi STL konteynerleri, strong exception guarantee sağlamak için, taşınma veya kopyalanma sırasında istisna fırlatılmadığını varsayar. Eğer taşınma işlemi sırasında bir istisna fırlatılırsa, std::vector işlemin güvenliğini sağlamak için kopyalama yapar. Bu durumda, SmartCode sınıfının move constructor'ı noexcept değilse, std::vector kopyalama yaparak strong guarantee sağlamak zorunda kalır.
...
SmartCode(SmartCode&&) noexcept = default;
...
Strong Guarantee
Strong exception guarantee, bir işlem sırasında bir exception fırlatılırsa, programın durumu işlemden önceki duruma geri döner. Yani, işlem ya tamamen başarılı olur ya da hiç yapılmamış gibi olur.
Destructor ve Exception Throw Süreci
Default olarak yıkıcı işlevler noexcept olarak tanımlanırlar. Ancak, destructor'ınızın kodu içinde istisna fırlatma potansiyeli varsa, bu durumdan kaçınılmalıdır. Destructor'lar genellikle strong guarantee sağlamaz çünkü bir destructor zaten nesneyi yok eder. Yani bu guarantee bir nesnenin durumu değiştirilirken gereklidir. Destructor'un bir işi geri alması veya nesneyi önceki duruma geri getirmesi gerekmez. Destructor'lar için asıl önemli olan nokta, istisna fırlatmamaları ve temiz bir şekilde çalışmalarıdır. Eğer bir destructor içinde istisna fırlatma potansiyeli varsa, bu durum noexcept ile belirtilmelidir.
class SmartCode {
public :
~SmartCode(){}
};
******************************
AÇIKLAMA : bu durumda dtor nothrow garantisi verir. destructor bu garantiyi vermediğinde stack unwinding süreci yönetilir. exception'ı handle eden programa kadar programın akışı stack geri sarılır ve içerideki tüm otomatik ömürlü nesneler için dtor çağırılır. o dtor'lardan birinin exception throw etme ihtimali olursa, bu durumda doğrudan terminate çağırılır.
******************************
#include <iostream>
#include <chrono>
#include <exception>
class Resource {
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() noexcept(false)
{
std::cout << "Resource released\n";
throw std::runtime_error("Exception in destructor");
}
};
class SmartCode {
public:
SmartCode() { std::cout << "SmartCode acquired\n"; }
~SmartCode() { std::cout << "SmartCode released\n"; }
};
void smart_func()
{
SmartCode sc;
Resource rsc;
throw std::runtime_error("Exception in function");
}
int main()
{
try
{
smart_func();
}
catch (const std::exception& exc)
{
std::cerr << "Caught exception: " << exc.what() << '\n';
}
return 0;
}
******************************
AÇIKLAMA :
SmartCode acquired
Resource acquired
Resource released
terminate called after throwing an instance of 'std::runtime_error'
what(): Exception in destructor
******************************

Comments