본문 바로가기
C,C++/C++

[C++] assert 매크로를 사용하자 / assert에 message 넣는 법

by woohyeon 2019. 12. 15.
반응형

프로그램을 개발하는 도중 발생하면 안되는 오류 또는 예외를 처리하기 위해 throw나 if문 등으로 예외를 처리를 한다. 하지만 C++ 에선 디버깅 중 예외 또는 오류를 더 쉽게 찾도록 assert라는 매크로를 제공한다. assert는 예외 처리라기보다는 프로그램 개발 도중 발생해선 안되는 조건을 잡아 그 것을 고칠 수 있도록 해주는 일종의 트랩이라고 생각하면 된다.


assert는 조건 식(condition)을 인자로 받아 해당 조건식이 거짓(false, 0,  '\0', NULL, nullptr)이면 프로그램을 중단시킨다. 그리고 assert에 사용된 조건식과 몇 라인에서 발생하였는지 출력한다. 따라서 개발자는 오류가 어디서 발생했는지 정확한 위치와 이유를 알 수 있다. 

assert는 런타임에 검사를 진행하며 컴파일 타임에 검사를 진행하는 static_assert라는 매크로도 있다. static_assert는 인자로 문자열 리터럴을 추가해 오류와 함께 메시지를 전달할 수 있다. 기본적으로 assert 매크로에선 메세지를 전달할 수 없지만 일종의 꼼수로 전달할 수 있는 방법 또한 알아볼 것이다.

그전에 먼저 assert의 사용 방법을 알아보기 위해 정확히 어떤 상황에 쓰는 것이 좋을지 가정해보자. 인자 2개를 받아 나눗셈을 수행하고 그 결과를 반환하는 함수를 작성한다고 가정해보자.

나눗셈은 분모가 0이 되어서는 안된다. 따라서 분모로 0을 입력했다면 재입력을 받던지 개발자가 따로 예외 처리를 해주어야 한다.하지만 개발자가 이 부분을 신경쓰지 못하고 다음과 같이 작성했다고 생각해보자.

double division(int a, int b)
{
	return (double)a / b;
}


위 함수 호출 시 인자 b에 0을 전달하면 보통 프로그램을 중단시키지만 내 컴파일러에선 중단 없이 inf라는 값을 출력한다.
따라서 디버깅 시 이 함수의 문제점을 쉽게 발견하지 못할 수 있다.


만약 다음과 같이 작성한다고 해보자.
(assert는 <assert.h> 헤더 파일에 정의되어 있고, release 모드가 아닌 debug 모드에서만 동작한다.)

double division(int a, int b)
{
	assert(b != 0);
	return (double)a / b;
}



assert는 조건식이 거짓일 경우 프로그램 실행 도중 프로그램을 중단시킨다고 했다.
위 함수의 인자 b에 0을 전달할 경우 조건 식이 거짓이 되기 때문에 프로그램을 중단시키고 아래와 같이 조건식과 함께 assert 라인의 넘버를 출력한다.


개발자는 이를 통해 해당 함수에 대한 대처를 할 것이고, 함수의 안정성은 높아지게 된다.
따라서 프로그램 작성 시 assert를 많이 사용할 수록 프로그램의 안정성은 높아지게 된다.
참고로 assert 1개당 조건식 하나만 사용하는 것이 좋다.
하나의 assert에 여러 조건 식을 넣으면 오류 잡기가 힘들고, assert는 프로그램의 성능에 영향을 미치지 않기 때문.



두 번째 예제를 살펴보기 전에 처음에 말한 메세지를 사용할 수 있는 방법에 대해 알아보자.



assert 매크로에 메세지를 사용하기 위해선 다음과 같이 사용할 수 있다.

assert(b != 0 && "Variable b CAN NOT be a zero");

문자열 리터럴은 항상 참이고 참은 앞의 조건 식이 참이든 거짓이든 결과에 영향을 줄 수 없다.
따라서 일종의 꼼수로 메시지를 출력할 수 있는 방법이다.



위와 같이 사용할 수도 있고 다음과 같이 보기 좋게 나만의 매크로로 다시 재정의하여 사용할 수 있다.

#define Assert(expression, message) assert(expression && message)




위와 같이 정의하면 다음과 같이 호출할 수 있다.

Assert(b != 0, "Variable 'a' CAN NOT be a ZERO");





이제 두 번째 예제를 살펴보면..
먼저 switch문에서 enum 클래스 타입 변수를 받아 개체에 따라 정보를 처리한다고 해보자.
eFruit enum 클래스의 멤버들의 값을 따로 정해주지 않았기 때문에 각 멤버들은 순서대로 0~3의 값을 가진다. 

enum class eFruit
{
	APPLE,
	MELON,
	STRAWBERRY,
	PEAR=3
};
void main()
{
	eFruit fruit = (eFruit)5;
	switch (fruit)
	{
	case eFruit::APPLE:
		std::cout << "Apple";
		break;
	case eFruit::MELON:
		std::cout << "Melon";
		break;
	case eFruit::STRAWBERRY:
		std::cout << "Strawberry";
		break;
	case eFruit::PEAR:
		std::cout << "Pear";
		break;
	default:
		Assert(fruit < (eFruit)4  , "Enum type variable 'fruit' must be a enum eFruit class");
		break;
	}
}

예를 들어 eFruit의 멤버 외엔 절대 다른 값이 나와선 안된다고 가정해보자.
그러면 case 4개 이외엔 default의 구문이 발생하면 안된다는 의미이다. 따라서 default 구문에 우리가 정의한 Assert를 추가한다.
인자로 받은 fruit의 값이 멤버의 최대 값인 3을 초과하면 안되기 때문에 조건 식을 위와 같이 적으면 0~3 이외의 값이 들어올 때 다음과 같이 프로그램을 중단시키며 우리가 추가한 메세지 또한 확인할 수 있다.




여기까지 대략적으로 assert에 대해 알아보았고 

마지막으로 assert 사용 시 주의 사항에 대해 다시 한 번 정리해보면

1. 발생해서는 안되는 조건식에만 사용해야 한다. 충분히 사용자의 실수로 발생할 수 있는 정상적인 오류에 대해선 사용하면 안된다.
2. 하나의 assert에 여러 개의 조건식을 넣지 말자. 하나의 assert엔 하나의 조건식만.
3. assert가 메세지를 포함할 수 있도록 재정의해서 사용하자.




댓글