I'm FanJae.

[C++] stack unwinding 본문

C++/Basic

[C++] stack unwinding

FanJae 2024. 8. 29. 22:36

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
Comments