티스토리 뷰

※ 주의 사항 ※

  • 이 글의 목적은 '지식의 전달'이 아닌 '학습의 기록'입니다.
  • 따라서 제가 이해하는 그대로의 내용이 포함됩니다.
  • 따라서 이 글은 사실과는 다른 내용이 포함될 수 있습니다.

 

상수 표현식, inline 함수, 상수 표현식 비교

C++11부터 constexpr 키워드로 상수 표현식을 사용할 수 있습니다. 상수 표현식은 inline 함수와 상당히 비슷합니다. 먼저 피보나치수열을 상수 표현식으로 구현한 예제를 보겠습니다.

#include <iostream>

constexpr unsigned long fibonacci(unsigned long i)
{
	return (i <= 1) ? i : fibonacci(i - 1) + fibonacci(i - 2);
}

int main()
{
	unsigned long i = fibonacci(5);
	std::cout << i << " = fibonacci(5)" << std::endl;

	return 0;
}

/*
실행 결과

5 = fibonacci(5)

*/

 

위 예제를 인라인 함수로 똑같이 구현한 예제와 비교해서 보면 그 모양새가 아주 비슷하다는 것을 알 수 있습니다. 겨우 키워드 하나만 바뀌었으니까요.

#include <iostream>

inline unsigned long fibonacci(unsigned long i)
{
	return (i <= 1) ? i : fibonacci(i - 1) + fibonacci(i - 2);
}

int main()
{
	unsigned long i = fibonacci(5);
	std::cout << i << " = fibonacci(5)" << std::endl;

	return 0;
}

/*
실행 결과

5 = fibonacci(5)

*/

 

여기서 하나만 더 비교해 보겠습니다. 바로 일반 함수입니다.

#include <iostream>

unsigned long fibonacci(unsigned long i)
{
	return (i <= 1) ? i : fibonacci(i - 1) + fibonacci(i - 2);
}

int main()
{
	unsigned long i = fibonacci(5);
	std::cout << i << " = fibonacci(5)" << std::endl;

	return 0;
}

/*
실행 결과

5 = fibonacci(5)

*/

 

일반 함수도 위의 상수 표현식이나 inline 함수와 매우 비슷합니다. 키워드가 있고 없고의 차이이므로 당연합니다. 하지만 이들 세 종류의 문법은 동작 방식을 비교해 보면 매우 다르다는 것을 알 수 있습니다.

 

먼저 일반 함수의 동작 방식을 보겠습니다. 일반 함수는 fibonacci() 함수가 정의되면 이 함수를 메모리에 저장하게 됩니다. 그리고 필요할 때마다 이 메모리에 접근하여 함수를 호출합니다. 

 

반면 inline 함수의 동작 방식은 함수를 호출한 자리에 함수의 내용을 그대로 복사 붙여 넣기 하는 것과 같습니다. 이는 컴파일 타임에 수행됩니다. 그래서 앞서 보인 inline 함수 예제는 다음과 같이 수행됩니다.

#include <iostream>

/*
inline unsigned long fibonacci(unsigned long i)
{
	return (i <= 1) ? i : fibonacci(i - 1) + fibonacci(i - 2);
}
*/

int main()
{
	//함수의 내용을 붙여 넣음
	unsigned long i = (5 <= 1) ? 5 : 
		(
			(4 <= 1) ? 4 : 
			(
				(3 <= 1) ? 3 : 
				(
					(2 <= 1) ? 2 : 
					(
						(1 <= 1) ? 1 : /*호출되지 않는 영역*/-1
					) + 
					(
						(0 <= 1) ? 0 : /*호출되지 않는 영역*/-1
					)
				) + 
				(
					(1 <= 1) ? 1 : /*호출되지 않는 영역*/-1
				)
			) + 
			(
				(2 <= 1) ? 2 : 
				(
					(1 <= 1) ? 1 : /*호출되지 않는 영역*/-1
				) + 
				(
					(0 <= 1) ? 0 : /*호출되지 않는 영역*/-1
				)
			)
		) + 
		(
			(3 <= 1) ? 3 : 
			(
				(2 <= 1) ? 2 : 
				(
					(1 <= 1) ? 1 : /*호출되지 않는 영역*/-1
				) + 
				(
					(0 <= 1) ? 0 : /*호출되지 않는 영역*/-1
				)
			) + 
			(
				(1 <= 1) ? 1 : /*호출되지 않는 영역*/-1
			)
		);
	std::cout << i << " = fibonacci(5)" << std::endl;
	

	return 0;
}

/*
실행 결과

610 = fibonacci(15)

*/

 

연산 중에 호출되지 않는 부분은 /*호출되지 않는 영역*/으로 구분하고, 값을 비워둘 수는 없기에 임의로 -1의 리터럴 값을 넣어주었습니다. 눈치챘겠지만 inline 함수의 이러한 특징으로 인해 컴파일하고 생성되는 바이너리 코드의 길이가 매우 길어질 수 있다는 단점도 가지고 있습니다.

 

마지막으로 상수 표현식의 작동 방식입니다. 상수 표현식도 inline 함수와 같이 함수를 호출하는 코드 대신 해당 자리에 무엇인가를 붙여 넣습니다. 이는 컴파일 타임에 수행됩니다. inline 함수가 함수의 내용을 붙여 넣었다면, 상수 표현식은 함수 내용을 쭉 실행하고 반환되는 결과 값을 붙여 넣는다는 특징을 가지고 있습니다. 단, 함수의 결과는 상수로 반환되어야 합니다. 이러한 작동 방식에 의해서 상수 표현식의 예제는 다음과 같이 바뀐다고 볼 수 있습니다.

#include <iostream>

/*
constexpr unsigned long fibonacci(unsigned long i)
{
	return (i <= 1) ? i : fibonacci(i - 1) + fibonacci(i - 2);
}
*/

int main()
{
	unsigned long i = 5;    //fibonacci(5)를 계산한 결과인 5을 붙여 넣음
	std::cout << i << " = fibonacci(5)" << std::endl;
	

	return 0;
}

/*
실행 결과

5 = fibonacci(5)

*/

 

inline 함수와는 매우 대조적입니다. inline 함수와는 달리 컴파일 이후 생성되는 바이너리 코드의 길이가 매우 길어질 걱정은 할 필요 없습니다.

 

상수 표현식은 이처럼 함수나 식을 하나의 상수처럼 인식하기 때문에 일반 함수나 식을 실행할 때 필요한 에러 처리나 함수 제어에 들어가는 작업, 메모리 등의 사용을 많이 줄여주게 됩니다. 따라서 프로그램의 성능에 긍정적인 영향을 줄 수 있습니다.

 

상수 표현식 사용 조건

위의 예만 보면 상수 표현식이 inline 함수에 비해서도 월등히 좋아 보이기 때문에 상수 표현식을 많이 사용해야겠다고 생각할지도 모르겠습니다. 하지만 상수 표현식은 그 특징상 컴파일 타임에 함수의 내용을 수행하는 것이므로 런타임에 함수의 내용을 수행하는 것과는 달리 여러 제약이 걸릴 수밖에 없습니다.

 

다음은 constexpr 키워드를 함수에 적용하여 상수 표현식으로 사용할 때 주의해야 할 조건들에 대해 설명한 것입니다. 참고로 constexpr 키워드는 함수뿐만 아니라 변수에도 적용할 수 있습니다. constexpr 키워드를 변수에 사용할 경우 const 선언한 것처럼 상수로 취급됩니다. 아래의 조건들은 constexpr 키워드를 함수에 적용한, constexpr 함수에 대한 내용입니다.

  • constexpr 함수는 리터럴 형식만 매개 변수로 받거나 반환할 수 있습니다. 리터럴 형식이란 컴파일 타임에 해당 레이아웃이 결정될 수 있는 형식을 의미하며 다음을 포함합니다.
    • void 또는 void 배열
    • 스칼라 형식(char, short, int, long, float, double, boolean 등) 또는 스칼라 형식의 배열
    • reference 또는 reference 배열
    • 'trivial 소멸자'와 '이동 또는 복사 생성자가 아닌 constexpr 생성자'를 하나 이상 포함하는 클래스. 해당 클래스는 또한 비정적 데이터 멤버 및 기초 클래스가 모두 리터럴 형식이어야 하고, volatile이 아니어야 함
  • constexpr 함수는 재귀적일 수 있습니다.
  • 가상 함수에는 사용할 수 없습니다.
  • 함수의 본문은 if문, switch문과 모든 반복문(for, 범위 기반 for, while, do while 등)을 포함할 수 있으나, 그 외 다른 모든 문과 블록(goto, try-catch 등)은 포함할 수 없습니다.
  • 지역 변수를 선언할 수 있지만 반드시 해당 변수를 초기화해야 합니다. 그리고 해당 지역 변수는 리터럴 형식이어야 하며 static 선언은 불가합니다. 지역 변수는 const일 필요는 없으며 값이 변경될 수 있습니다.

 

constexpr 생성자

constexpr은 클래스의 생성자에도 사용할 수 있습니다. 자세한 내용은 클래스 생성자를 배울 때 설명합니다.

 

'공부 일지 > CPP 공부 일지' 카테고리의 다른 글

C++ | mutable 키워드  (0) 2021.08.10
C++ | 범위 기반 for문  (0) 2021.08.09
C++ | 열거형  (0) 2021.08.06
C++ | 묶음 타입(구조체와 공용체)  (0) 2021.08.06
C++ | char 타입과 wchar_t 타입  (0) 2021.08.06
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/10   »
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 31
글 보관함