일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 더 지니어스 양면포커
- vector capacity
- virtual function table
- c++ basic practice
- placement new
- std::ostream
- operator overloading
- new&delete
- this call
- delete function
- pointer to member data
- C++
- dynamic_cast
- std::vector
- increment operator
- std::endl
- c++ multi chatting room
- member function pointer
- vector size
- suffix return type
- return by reference
- std::cout
- conversion constructor
- virtual destructor
- virtual inheritance
- discord bot
- base from member
- constructor
- diamond inheritance
- virtual function
- Today
- Total
I'm FanJae.
[C++] stack unwinding 본문
1. stack unwinding
#include <iostream>
struct Object
{
~Object() { std::cout << "~Object" << std::endl; }
};
void f3()
{
std::cout << "f3 start" << std::endl;
throw 1;
std::cout << "f3 finish" << std::endl;
}
void f2()
{
std::cout << "f2 start" << std::endl;
Object obj;
f3();
std::cout << "f2 finish" << std::endl;
}
void f1()
{
std::cout << "f1 start" << std::endl;
Object obj;
f2();
std::cout << "f1 finish" << std::endl;
}
int main()
{
try
{
f1();
}
catch( ... )
{
}
}
- 예제의 호출이 상당히 복잡해 보이지만, 실제로는 크게 어렵지 않은 내용이다.
- 이들의 호출 관계를 조금 더 그림으로 표현해보면 아래와 같다.
- 이 예제는 결국 f3()에서 throw가 일어남에 실행 결과를 보면, finish는 출력되지 않는다.
void f3()
{
std::cout << "f3 start" << std::endl;
throw 1;
std::cout << "f3 finish" << std::endl;
}
void f2()
{
std::cout << "f2 start" << std::endl;
Object obj;
f3();
std::cout << "f2 finish" << std::endl;
}
void f1()
{
std::cout << "f1 start" << std::endl;
Object obj;
f2();
std::cout << "f1 finish" << std::endl;
}
- 즉, 달리 말하면 f3()에서 바로 main 함수로 돌아왔다.
- 그렇다면, f2, f1()에 있던 객체는 파괴 될것인가 파괴되지 않을 것인가? 에 대해 생각해볼 수 있다.
- 이제야 실행 결과를 올려보면 아래와 같다.
1-1. Stack unwinding(스택 풀기)란?
- 이처럼 예외가 발생해서 catch로 이동할 때 중간과정에서 만든 stack에 있는 모든 변수(객체)는 파괴(소멸자 호출)된다.
- 이를 Stack unwinding이라고 한다.
※ 이것의 원리를 알기 위해서는 어셈블리에서 '함수 호출의 원리'에 대해 알아야한다고 한다.
※ 다소 관심이 생기므로, 나중에 기회되면 별도로 포스트를 다뤄보겠다.
2. RAII
#include <iostream>
class mutex
{
public:
void lock() { std::cout << "mutex lock"; }
void unlock() { std::cout << "mutex unlock"; }
};
mutex m;
void fn()
{
m.lock();
throw 1;
m.unlock();
}
int main()
{
try { fn();}
catch(...){}
}
- mutex라는 것은 서로 다른 스레드가 동시에 같은 자원에 접근할 때, 이를 막아주는 기법 중 하나다.
- 실제 구현은 당연히 저것과 다르지만 예제임을 감안하자.
- mutex는 가장 쉬운 예시로 키가 하나인 화장실을 가장 많이 예로든다.
- Lock()을 하면, 화장실 키로 문 열고 들어간 것이고, unlock()을 하면 화장실 나오고 다시 문을 잠군 개념인 셈이다.
※ 즉, 스레드 입장에서 보면 하나의 스레드가 lock을 걸었으므로, 다른 스레드 들은 이곳에 접근할 수 없다.
※ 문제는 unlock()을 안했으므로, 들어가지 못하는 것이다.
이를 해결할 방법은 '생성자'를 의존하는 방법이 있다.
#include <iostream>
class mutex
{
public:
void lock() { std::cout << "mutex lock"; }
void unlock() { std::cout << "mutex unlock"; }
};
mutex m;
class lock_guard
{
mutex& mtx;
public:
lock_guard(mutex& m) : mtx(m) { mtx.lock(); }
~lock_guard() { mtx.unlock();}
};
void fn()
{
lock_guard g(m); // m.lock()
// m.lock();
throw 1;
// m.unlock();
}
int main()
{
try { fn();}
catch(...){}
}
- 예외가 발생해서 catch로 갈때, throw 아래 코드가 실행되지는 않지만, '지역변수'는 파괴된다.
- 즉, 이를 이용하면 정상적으로 unlock됨을 보장한다.
과거, C로 멀티 스레드 서버를 구현하는 공부를 하고 구현했던 내 입장에서는 이 방법은 상당히 유용한 방법이라고 생각했다. 실제로 mutex가 unlock()을 하지 못할 경우 발생할 문제를 생각한다면 더더욱 말이다.
2-1. RAII란?
- Resource Acquisition Is Initialization이다.
- 자원의 할당/해지를 직접하지 말고 생성자/소멸자를 사용해서 관리하라는 기술이다.
3. noexcept
#include <print>
void f1() { }
void f2(int a, int b) { }
void f3(int a, int b) { throw 1; }
int main()
{
bool b1 = noexcept( f1() );
bool b2 = noexcept( f2(1, 2) );
bool b3 = noexcept( f3(1, 2) );
std::cout << b1 << std::endl;
std::cout << b2 << std::endl;
std::cout << b3 << std::endl;
}
3-1. noexcept 키워드의 2가지 용도
① 함수가 예외가 있는지 없는지 조사
- noexcept operator(연산자)
② 함수가 예외가 없음을 표기
- noexcept specifier(지정자)
3-2. 함수가 예외 가능성 여부를 조사하는 방법
bool b = noexcept ( f1() ) ;
- 이처럼 noexcept 안에 함수 호출 표현식을 넣으면, exception 가능성을 체크한다.
- noexcept는 연산자이다.
※ 다만, 선언문만 include 되도 사용할 수 있다. 즉, 구현을 확인하는 것이 아니다.
※ 함수가 예외가 없음을 알리려면 함수 선언 뒤에 noexcept를 붙여서 예외가 없음을 알려야 한다.
3-3. 함수가 예외가 없음을 표기하는 방법
f() | 예외가 발생할 수 있음 |
f() noexcept | 예외 없음 |
f() noexcept(true) | 예외 없음 |
f() noexcept(false) | 예외가 발생할 수 있음 |
- f() noexcept(false)는 왜 필요한가?
#include <print>
void f1() noexcept {}
void f2(int a) noexcept {}
void f2(double a) {}
template<typename T>
void foo(T arg) noexcept( noexcept(f2(arg)) )
{
f1();
f2(arg);
}
int main()
{
std::cout << noexcept(foo(3)) << std::endl;
std::cout << noexcept(foo(3.4)) << std::endl;
}
- noexcept는 예외가 없음을 알리기도 하지만, 조사의 역할도 한다.
- foo의 경우는 f1()은 noexcept이지만, f2()는 exception일 수 있다. 따라서, 이를 판단해야 한다.
- 즉, 내부에 사용하는 표현식을 조사할 기회를 주자는 취지이다.
3-4. noexcept 붙은 함수에서 예외가 발생하면 어떻게 되는가?
- std::terminate를 호출하고 비정상 종료한다.
3-5. 예외가 없음을 알리는 이유
- 예외 여부에 따라 최적화된 코드를 작성할 수 있다.
- std::move_if_noexcept() 예외 여부에 따라 다르게 동작하는 것도 존재한다.
void fast_alogithm()
{
// 성능은 좋지만
// 실패(예외) 가능성이 있는 알고리즘
}
void slow_algorithm() noexcept
{
// 느리지만 실패하지 않는 알고리즘
}
int main()
{
if( noexcept(fast_alogithm()) )
fast_alogithm();
else
slow_algorithm();
}
- C++을 오래 사용하지 않아서, 아직은 크게 와닿지 않는다. (공부를 열심히 해야겠다...)
- 하지만 이와 같이 작성된 코드도 확인이 가능하다고 한다.
- 만약 추후 fast_algorithm()에서 예외를 없앨 방법을 찾아낸다면, 더 빠른 알고리즘을 실행하게 만드는것이 가능한거다.
- 즉, 이 방식으로 사용자에게 선택권을 부여할 수 있다.
'C++ > Basic' 카테고리의 다른 글
[C++] Exception (0) | 2024.08.29 |
---|---|
[C++] Algorithm (3) | 2024.08.28 |
[C++] Iterator (0) | 2024.08.28 |
[C++] STL Container II (0) | 2024.08.27 |
[C++] STL Container I (0) | 2024.08.27 |