12. 함수
12.1 함수 선언
12.1.1 왜 함수인가?
함수의 목적은 복잡한 계산을 의미있는 덩어리로 쪼개고, 그것들에 이름을 붙이자는 것이다.
이는 함수의 가독성을 높이자는 것이고, 이는 유지보수를 편하게 하기 위한 첫걸음이다.
코드에서 오류의 개수는 코드의 양과 코드의 복잡성과 강력한 상관관계를 갖는데, -> 두 문제 모두 더 짧은 함수를 많이 사용함으로 해결할 수 있다.
가장 기본적인 조언은 한 화면에서 전체를 볼 수 있는 정도로 함수의 크기를 유지하라는 것이다. -> 많은 프로그래머들에게 함수당 대략 40줄 정도의 함수를 만들라고 이야기한다. -> 필자가 이상적으로 생각하는 수준은 7줄이다.
실질적으로 거의 모든 경우 함수 호출 비용은 문제의 원인이 아니다. -> 호출이 많아져 호출 비용의 문제가 생길 경우 인라인 함수를 사용해 비용을 줄일 수 있다.
12.1.2 함수 선언문의 구성
호출되는 모든 함수는 어딘가에서 선언 및 정의되야 하며, 함수 정의는 함수의 본체가 표시되는 함수의 선언이다.
다음을 보자
또는, 아래와 같이 선언과 정의의 매개변수의 이름이 다를수도 있다. -> 필수는 아니지만, 가독성을 위해서 일치시킨다.
아래와 같이, 이름 없는 인자를 사용할 수도 있다. -> 이름을 붙이지 않을때는, 해당 인자가 함수 정의에서 사용되지 않는다는 점을 나타낸다. -> 이는 코드를 간단히 하기 위한 목적이나, 확장을 염두해 둔 사전 설계 때문에 등장한다.
12.1.4 값 반환
전통적으로 함수 선언에서 반환 타입이 맨 앞에 위치한다.
하지만, 함수 선언은 반환 타입을 매개변수 뒤에 위치시키는 문법을 이용해서 사용될 수도 있다.
아래 두 선언은 동일하다.
후위형 반환 타입이 사용될 때는, 함수 템플릿에서 중요해진다.
아래 코드를 보자 -> 람다와 비슷하게 생겼다.
매번 함수가 호출될 때마다, 스택에 변수가 할당된다.
함수가 반환된 후 해당 공간은 재사용되므로, 지역변수를 가리키는 포인터는 절대로 반환되지 않아야한다. -> 참조나 포인터가 아닌 유의미한 값이 반환되어야 한다.
다행스럽게도 컴파일러는 지역변수에 대한 참조자 반환에 대해서 경고를 보낸다.
아래 코드는 좋지 않은 예
함수를 빠져나가는 5가지 방법
return 문의 실행
단순하게 함수의 끝에 도달
지역적으로 잡히지 않는 예외를 던진다.
예외가 던져지고 noexcept 에서 지역적으로 잡히지 못하는 것으로 인한 종료
반환하지 않는 시스템 함수(ex, exit()) 를 직간접적으로 호출
12.1.5 inline 함수
inline 함수는 컴파일러에 의해 함수 호출이 해당 함수 본문의 내용을 대체되는 것이다. -> 인라인 함수는함수 호출의 오버헤드를 줄이고 실행 속도를 높일 수 있다.
내용이 너무 길 경우에는 코드의 크기가 증가하고, 캐시의 많은 메모리가 사용될 수 있다. 또한 함수를 변경할 때마다 모든 호출 지점이 다시 컴파일되어야 하기 때문에, 내용이 너무 길 경우에는 인라인 함수를 사용하지 않는다.
함수는 아래와 같이 inline 으로 정의될 수 있다.
inline 지정자는 해당 함수가 인라인 호출 코드를 생성하려 시도한다는 것을 컴파일러에게 알려주는 중요한 단서이다.
inline 지정자를 선언해도 무조건적으로 인라인화가 되는 것은 아니다. 컴파일러가 판단한다.
어떤 값이 반드시 컴파일 타임에 계산되게 하고 싶다면, 해당 값을 constexpr 로 선언하고 해당 값의 평가에 사용될 모든 함수를 constexpr 로 만들어야 한다.
12.1.6 constexpr 함수
일반적으로 함수는 컴파일 타임에 평가될 수 없으므로 상수 표현식에 호출될 수 없다.
하지만, constexpr 함수로 지정하면 해당 함수가 상수 표현식 안에서 사용되게 만들 수 있다. -> 컴파일 타임에 초기화식을 평가하겠다는 뜻이다.
컴파일 타임에 평가되려면 함수는 충분히 간단해야 한다. (constexpr 은 순수 함수)
때문에 다음과 같은 제약조건을 가지게 된다.
단순한 표현식:
constexpr
함수는 기본적으로 단순한 표현식을 포함해야 합니다. 복잡한 런타임 로직을 포함할 수 없습니다.
한정된 함수 본문:
함수 본문은 한정된 수의 명령문과 연산만을 포함할 수 있습니다. 특히 컴파일 타임에 평가될 수 있어야 합니다.
조건문 (
if
), 반복문 (for
,while
) 등을 사용할 수 있지만, 이들 역시 컴파일 타임에 평가될 수 있어야 합니다.
상수 표현식만 사용:
함수 내에서 사용되는 모든 변수와 객체는 상수 표현식이어야 합니다.
constexpr
함수 내부에서 비상수 데이터나 포인터 조작은 허용되지 않습니다.
반환 타입:
함수의 반환 타입은
literal type
이어야 합니다. 기본적으로 기본 자료형과constexpr
로 정의된 사용자 정의 타입이 이에 해당합니다.
함수 호출:
constexpr
함수는 다른constexpr
함수나 상수 표현식에서 호출될 수 있습니다.
12.1.6.2. 조건부 평가
constexpr 함수 내에서 결과를 알 수 없는 조건 표현식은 컴파일 타임에 결과 값이 계산되지 않는다. -> 이는 해당 조건 표현식의 결과 값이 런타임에 계산된다는 뜻이다.
다음 코드를 살펴보자.
low 와 high 는 컴파일 타임에 알 수 이는 환경 매개변수이며,
f(x,y,z) 는 구현에 따라서 결과 값이 달라진다는 것을 알 수 있다.
12.1.7 [[noreturn]] 함수
[[...]] 구조는 속성(attribute) 라고 불리며, c++ 문법 거의 어디에나 넣을 수 있다.
표준 속성은 다음 두가지가 있다.
[[noreturn]] : 함수 선언 맨 앞에 두면 해당 함수의 반환을 기대하지 않는다는 의미이다.
[[carries_dependency]] :
다음 코드를 살펴보자 -> [[noreturn]] 속성에도 불구하고, 함수가 반환한다면 무슨 일이 일어날지는 정의되어 있지 않다.
12.1.8 지역 변수
어떤 함수 내에 정의된 변수를 지역 변수(local name) 이라고 부른다.
지역 변수나 상수는 실행 스레드가 자신의 정의에 다다를 때 초기화된다.
변수는 static 으로 선언돼 있지 않는 한 함수가 호출될 때마다 해당 변수의 복사본이 만들어진다. -> 만약 static 지역 변수가 있다면 실행 스레드가 자신의 정의의 다다를 때 딱 한번 초기화 될 것이다.
12.2 인자 전달
12.2.1 참조자 인자
참조에 의한 호출 인자를 변경하는 함수는 프로그램을 읽기 어렵게 만들기 때문에, 대부분의 경우 피해야 한다.
하지만 대규모 개체의 경우 값에 의한 전달보다 참조에 의한 전달이 눈에 띄게 효율적일 수 있다. -> 복사가 일어나지 않기 때문이다. (해당 경우는 참조자를 매개변수로 사용하는게 효율적이다)
호출된 함수가 개체의 값을 변경하지 못하게 하기 위해 해당 인자를 const 로 선언할 수도 있다. -> const 선언은 프로그램의 규모가 커질수록 중요해진다. (안정적인 프로그램)
리터럴, 상수 인자는
const T&
인자로 전달 될 수 있지만,T&
인자로는 전달될 수 없다. -> const T& 는 임시변수를 만들어준다.
12.2.2 배열 인자
배열이 함수의 인자로 사용된다면 배열의 첫 번째 워소를 가리키는 포인터가 전달된다. -> 배열은 값에 의해 전달되는 것이 아닌, 포인터가 전달된다(일반 타입과 다른점)
호출된 함수는 배열의 크기를 알 수 없다. (오류가 발생하는 주원인) -> 이를 방지하기 위해서 C 스타일 문자열은 '\0' 으로 종료된다는 점을 이용하거나, -> 또는, 함수에 배열의 크기를 알려주는 인자를 받는다.
void compute1 (int* vec_ptr, int vec_size);
위의 방법은 차선책이기 때문에, 대부분은 vector, array, map 과 같이 배열의 사이즈를 조절하는 컨테이너를 사용한다. (가장 좋은 방법)
하지만 이 방법도 싫다면 아래 코드와 같이 함수의 인자로 배열에 참조자 타입을 선언할 수도 있다.
12.2.3 리스트 인자
일반적으로아래처럼 리스트 인자가 사용되는 것을 볼 수 있다. -> 컴파일러는 함수가 해당 인자를 최대한 사용할 수 있도록 한다.
하지만, 함수 오버라이딩에 의해 애매한 상황이 발생한다면, initializer_list 매개변수가 우선권을 갖는다. -> 그 이유는, 리스트의 원소 개수를 기준으로 함수를 선택하게 된다면 상당히 혼동을 일으키기가 쉽다. -> 오버로딩에서 모든 형태의 혼동을 없애는 것은 불가능 하겠지만 initializer_list 매개변수가 우선권을 가지면, 대부분의 혼동 가능성을 최소화할 수 있다.
12.2.4 인자의 개수가 지정되지 않은 경우
어떤 함수에서 호출 내에 있어야 할 모든 인자의 개수와 타입을 지정할 수 없을 때 사용하는 방법은 다음 3가지 이다.
가변 인자 템플릿 사용 (다른 챕터에서 설명)
initializer_list 인자 타입으로 사용 (다른 챕터에서 설명)
매개변수를
...
로 끝내는 것 (이번 챕터에서 설명) -> <cstdarg> 에 있는 일부 매크로를 사용해 임의의 타입으로 이뤄진 임의의 개수를 인자로 처리할 수 있다. -> 하지만, 이 방식은 타입 안정적이지 않으며, 복잡한 사용자 정의 타입과 함께 쓰이기는 어렵다.
잘 설계된 프로그램이라면 인자 타입이 완벽하게 지정되지 않은 함수는 많아야 몇 개 내외여야 한다. -> 오버로딩 된 함수, 기본 인자를 사용하는 함수, initailizer_list 를 사용하는 함수, 가변 인자 템플릿을 사용하는 함수는 대부분의 상황에서 타입 체크를 수행하는데 사용 할 수 있다.
오직 인자의 개수와 인자의 타입이 모두 가변적이거나, 가변 인자 템플릿의 사용이 바람직하지 않은 경우에만
...
를 사용하는 케이스가 필요해진다.
12.3 오버로딩 함수
대다수의 경우 다른 함수에는 다른 이름을 붙이는 것이 좋은 방법이지만, 서로 다른 함수가 개념적으로 같은 작업을 다른 타입의 매개변수에 대해서 수행할 때 그들에게 같은 이름을 붙이는 것이 더 편리할 수 있다.
12.3.1 자동 오버로딩 해결
함수 fct 가 호출되면 컴파일러는 fct 란 이름의 함수 중 어떤 것을 실행할지 판단해야 한다.
이 과정은 실제 인자의 타입을 유효 범위 내애 있는 fct 란 모든 이름의 함수 매개변수 타입과 비교함으로써 이루어진다.
기본적인 방안은 인자와 가장 일치하는 함수를 실행하고, 이런 함수가 없다면 컴파일 오류를 내는 것이다.
함수 호출이 가능한지는 다음의 순서대로 판단한다. -> 다음 중 두 개의 일치가 발견되면 호출은 모호하다고 판단되어 컴파일 오류를 발생시킨다.
정확한 일치
타입 승격을 사용한 일치 (bool -> int, char -> int, short -> int ...)
표준 변환을 사용한 일치 (int -> double, double -> int, double -> long double ...)
사용자 정의 타입 변환을 사용한 일치(double -> complex<double>)
함수 선언에서
...
을 사용한 일치
12.3.2 오버로딩과 반환 타입
오버로딩은 매개변수만 고려한다.
반환타입은 고려하지 않는다.
12.3.5 수동 오버로딩 해결
함수의 오버로딩을 너무 적게 or 많이 선언하면 모호한 상황을 일으킬 수 있다.
다음과 같은 방법들을 통해 위상황을 해결할 수 있다. -> 하지만 미봉책이지, 상황에 따라서 수동으로 해결해주어야 한다.
inline void f1(int n) {f1(long(n)); }
f2(static_cast<int*>(0));
12.5 함수를 가리키는 포인터
개체와 마찬가지로 함수 본체를 위해 생성된 코드는 메모리 어딘가에 위치하므로, 그에 따른 주소를 갖는다. -> 개체를 가리키는 포인터를 가질 수 있는 것처럼 함수를 가리키는 포인터를 가질 수 있다.
하지만 여러가지 이유로 함수를 가리키는 포인터는 코드의 변경을 허용하지 않는다. -> 컴퓨터 아키텍처, 시스템 설계와 관련되어 있다.
함수를 통해 우리가 할 수 있는 것은 딱 두가지이다.
함수를 호출하던가,
그것의 주소를 얻는 것이다.
함수의 주소를 통해 얻어진 포인터는 해당 함수를 호출하는 데 사용할 수 있다.
컴파일러는 efct 가 포인터라는 점을 알아내고 그것을 가리키는 함수를 호출할 것이다. ->
efct("error");
==error("error");
위 코드와 마찬가지로 & 를 이용해서 함수의 주소를 알아내는 것 역시 선택적이다.
함수를 가리키는 포인터는 함수 자체와 완전히 똑같이 선언된 인자 타입을 갖는다. -> 포인터 대입에서는 전체 함수 리턴타입과 매개변수가 일치해야 한다.
Last updated