함수 선언과 정의
함수 선언과 정의 (Function declaration & definition)
함수 선언(function declaration)은 함수의 이름과 타입을 소개하고, 함수 정의(function definition)는 그 이름/타입을 함수 몸통(body)과 연결해요. 함수를 '어떻게 부를 수 있는지' 먼저 알리고, 그 다음 '실제로 무엇을 하는지'를 정의하는 구조라고 이해하면 됩니다.
출처: cppreference
본문
함수 선언 (Function declaration)
함수 선언은 어떤 스코프에서든 나타날 수 있어요. 클래스 스코프의 함수 선언은 클래스 멤버 함수를 소개하는데(friend 지정자를 쓰지 않는 경우), 자세한 내용은 멤버 함수와 friend 함수 문서를 참고하세요.
noptr-declarator ( parameter-list ) cv (optional) ref (optional) except (optional) attr (optional) (1)
noptr-declarator ( parameter-list ) cv (optional) ref (optional) except (optional) attr (optional) -> trailing (2) (since C++11)
(declarator 문법의 다른 형태는 선언(Declarations) 문서 참고)
- (1) 일반 함수 선언자 문법.
- (2) 후행 반환 타입(trailing return type) 선언. 이 경우 decl-specifier-seq는
auto키워드를 꼭 포함해야 해요.
| noptr-declarator | - | 유효한 선언자라면 무엇이든 가능한데, *, &, &&로 시작하면 괄호로 감싸야 합니다. |
| parameter-list | - | 비어 있을 수 있는, 함수 매개변수의 쉼표로 구분된 목록(자세한 내용은 아래 참고) |
| attr | - | (C++11부터) 속성 목록. 이 속성들은 함수 자체가 아니라 함수의 타입에 적용돼요. 함수에 대한 속성은 declarator 안의 식별자 뒤에 나타나며, 선언 맨 앞에 있는 속성(있다면)과 결합됩니다. |
| cv | - | const/volatile 한정. 비정적 멤버 함수 선언에서만 허용 |
| ref | - | (C++11부터) ref 한정(ref-qualification). 비정적 멤버 함수 선언에서만 허용 |
| except | - | 동적 예외 지정(until C++11) / 동적 예외 지정 또는 noexcept 지정(since C++11, until C++17) / noexcept 지정(since C++17) |
| trailing | - | 후행 반환 타입. 반환 타입이 인자 이름에 의존하거나(예: template<class T, class U> auto add(T t, U u) -> decltype(t + u);) 복잡할 때(예: auto fpif(int) -> int(*)(int)) 유용해요. |
선언(Declarations) 문서에서 언급한 대로, declarator 뒤에는 requires 절이 올 수 있어요. 이 절은 오버로드 해석이 함수를 선택하려면 만족해야 하는 연관 제약(associated constraint)을 선언합니다. (예: void f1(int a) requires true;) 연관 제약은 함수 시그니처의 일부이지만 함수 타입의 일부는 아니라는 점을 기억하세요. (C++20부터)
함수 선언자는 다른 선언자와 섞일 수 있어요. 선언 지정자 시퀀스가 허용하는 만큼 말이죠:
// declares an int, an int*, a function, and a pointer to a function
int a = 1, *p = NULL, f(), (*pf)(double);
// decl-specifier-seq is int
// declarator f() declares (but doesn't define)
// a function taking no arguments and returning int
struct S
{
virtual int f(char) const, g(int) &&; // declares two non-static member functions
virtual int f(char), x; // compile-time error: virtual (in decl-specifier-seq)
// is only allowed in declarations of non-static
// member functions
};
void로 한정된 객체 타입을 매개변수 타입이나 반환 타입으로 쓰는 것은 비권장(deprecated)이에요. (C++20부터)
함수의 반환 타입은 함수 타입이나 배열 타입일 수 없어요(다만 그들에 대한 포인터나 참조는 될 수 있습니다).
다른 선언과 마찬가지로, 선언 앞에 나타나는 속성과 declarator 안의 식별자 바로 뒤에 나타나는 속성 모두 선언·정의되는 엔터티(여기서는 함수)에 적용돼요. 반면 문법에서 declarator 뒤에 나타나는 속성은 함수 자체가 아니라 함수의 타입에 적용됩니다. (C++11부터)
[[noreturn]] void f[[noreturn]](); // OK: both attributes apply to the function f
void f() [[noreturn]]; // Error: this attribute has no effect on the function itself
반환 타입 추론 (Return type deduction)
함수 선언의 decl-specifier-seq가 auto 키워드를 포함하면 후행 반환 타입을 생략할 수 있고, 컴파일러가 return 문에 쓰인 피연산자의 타입에서 반환 타입을 추론해요.
반환 타입이 decltype(auto)를 쓰지 않으면, 추론은 템플릿 인자 추론의 규칙을 따릅니다:
int x = 1;
auto f() { return x; } // return type is int
const auto& f() { return x; } // return type is const int&
반환 타입이 decltype(auto)면, return 문의 피연산자를 decltype으로 감쌌을 때 얻어지는 타입이 반환 타입이 돼요:
int x = 1;
decltype(auto) f() { return x; } // return type is int, same as decltype(x)
decltype(auto) f() { return (x); } // return type is int&, same as decltype((x))
참고: const decltype(auto)&는 오류이며, decltype(auto)는 단독으로만 쓰여야 해요.
return 문이 여러 개 있으면 모두 같은 타입으로 추론되어야 해요:
auto f(bool val)
{
if (val)
return 123; // deduces return type int
else
return 3.14f; // Error: deduces return type float
}
return 문이 없거나 return 문의 피연산자가 void 표현식이면, 선언된 반환 타입은 decltype(auto)(이 경우 추론된 반환 타입은 void)이거나 (cv-한정될 수 있는) auto(이 경우 추론된 반환 타입은 동일하게 cv-한정된 void)여야 해요:
auto f() {} // returns void
auto g() { return f(); } // returns void
auto* x() {} // Error: cannot deduce auto* from void
함수에서 return 문이 한 번이라도 보이면, 그 문장에서 추론된 반환 타입은 다른 return 문을 포함해 함수의 나머지 부분에서 사용될 수 있어요:
auto sum(int i)
{
if (i == 1)
return i; // sum's return type is int
else
return sum(i - 1) + i; // OK: sum's return type is already known
}
return 문이 중괄호로 감싼 이니셜라이저 목록을 쓰면 추론이 허용되지 않아요:
auto func() { return {1, 2, 3}; } // Error
가상 함수와 코루틴(C++20부터)은 반환 타입 추론을 쓸 수 없어요:
struct F
{
virtual auto f() { return 2; } // Error
};
사용자 정의 변환 함수가 아닌 함수 템플릿은 반환 타입 추론을 쓸 수 있어요. return 문의 표현식이 의존적(dependent)이지 않아도 추론은 인스턴스화 시에 일어나요. 이 인스턴스화는 SFINAE의 목적상 immediate context가 아닙니다.
template<class T>
auto f(T t) { return t; }
typedef decltype(f(1)) fint_t; // instantiates f<int> to deduce return type
template<class T>
auto f(T* t) { return *t; }
void g() { int (*p)(int*) = &f; } // instantiates both fs to determine return types,
// chooses second template overload
반환 타입 추론을 쓰는 함수·함수 템플릿의 재선언이나 특수화는 반드시 같은 반환 타입 자리 표시자(placeholder)를 써야 해요:
auto f(int num) { return num; }
// int f(int num); // Error: no placeholder return type
// decltype(auto) f(int num); // Error: different placeholder
template<typename T>
auto g(T t) { return t; }
template auto g(int); // OK: return type is int
// template char g(char); // Error: not a specialization of the primary template g
마찬가지로 반환 타입 추론을 쓰지 않는 함수·함수 템플릿의 재선언이나 특수화는 자리 표시자를 쓰면 안 돼요:
int f(int num);
// auto f(int num) { return num; } // Error: not a redeclaration of f
template<typename T>
T g(T t) { return t; }
template int g(int); // OK: specialize T as int
// template auto g(char); // Error: not a specialization of the primary template g
명시적 인스턴스화 선언은 반환 타입 추론을 쓰는 함수 템플릿 자체를 인스턴스화하지 않아요:
template<typename T>
auto f(T t) { return t; }
extern template auto f(int); // does not instantiate f<int>
int (*p)(int) = f; // instantiates f<int> to determine its return type,
// but an explicit instantiation definition
// is still required somewhere in the program
(C++14부터)
매개변수 목록 (Parameter list)
매개변수 목록은 함수를 호출할 때 지정할 수 있는 인자를 결정해요. 쉼표로 구분된 매개변수 선언 목록이며, 각각은 다음 문법을 가져요:
attr (optional) decl-specifier-seq declarator (1)
attr (optional) this decl-specifier-seq declarator (2) (since C++23)
attr (optional) decl-specifier-seq declarator = initializer (3)
attr (optional) decl-specifier-seq abstract-declarator (optional) (4)
attr (optional) this decl-specifier-seq abstract-declarator (optional) (5) (since C++23)
attr (optional) decl-specifier-seq abstract-declarator (optional) = initializer (6)
void (7)
- (1) 이름 있는 (정식) 매개변수를 선언.
decl-specifier-seq와declarator의 의미는 선언 문서 참고:int f(int a, int* p, int(*(*x)(double))[3]); - (2) 이름 있는 명시적 객체 매개변수를 선언.
- (3) 기본값을 가진 이름 있는 (정식) 매개변수를 선언:
int f(int a = 7, int* p = nullptr, int(*(*x)(double))[3] = nullptr); - (4) 이름 없는 매개변수를 선언:
int f(int, int*, int(*(*)(double))[3]); - (5) 이름 없는 명시적 객체 매개변수를 선언.
- (6) 기본값을 가진 이름 없는 매개변수를 선언:
int f(int = 7, int* = nullptr, int(*(*)(double))[3] = nullptr); - (7) 함수가 매개변수를 받지 않음을 나타낸다. 빈 매개변수 목록의 정확한 동의어예요:
int f(void);와int f();는 같은 함수를 선언합니다.
void만이 빈 매개변수 목록과 동등한 유일한 문법이에요. 그 외의 void 매개변수 사용은 ill-formed입니다.
- 매개변수가 여러 개인 경우:
int f1(void, int);— 오류 - void 매개변수에 이름을 붙인 경우:
int f2(void param);— 오류 - void가 cv-한정된 경우:
int f3(const void);— 오류 - void가 의존적인 경우:
int f4(T);(T가 void일 때) — 오류 - void 매개변수가 명시적 객체 매개변수인 경우(C++23부터):
int f5(this void);— 오류
decl-specifier-seq에는 타입 지정자 외의 지정자도 있을 수 있음을 암시하지만, 허용되는 다른 지정자는 register와 auto뿐이었고(C++11까지) 효과가 없었어요. (C++17까지)
함수 매개변수가 자리 표시자(placeholder, auto나 개념 타입)를 쓰면, 그 함수 선언은 대신 축약 함수 템플릿(abbreviated function template) 선언이 돼요:
void f1(auto); // same as template<class T> void f1(T)
void f2(C1 auto); // same as template<C1 T> void f2(T), if C1 is a concept
(C++20부터)
this 지정자가 있는 매개변수 선언(문법 (2)/(5))은 명시적 객체 매개변수를 선언해요. 명시적 객체 매개변수는 함수 매개변수 팩일 수 없고, 다음 선언들에서만 매개변수 목록의 첫 매개변수로만 나타날 수 있어요:
- 멤버 함수나 멤버 함수 템플릿의 선언
- 템플릿화된 멤버 함수의 명시적 인스턴스화·명시적 특수화
- 람다 선언
명시적 객체 매개변수를 가진 멤버 함수는 다음 제약이 있어요: 함수가 static이 아님, 함수가 virtual이 아님, 함수의 declarator가 cv와 ref를 포함하지 않음.
struct C
{
void f(this C& self); // OK
template<typename Self>
void g(this Self&& self); // also OK for templates
void p(this C) const; // Error: "const" not allowed here
static void q(this C); // Error: "static" not allowed here
void r(int, this C); // Error: an explicit object parameter
// can only be the first parameter
};
// void func(this C& self); // Error: non-member functions cannot have
// an explicit object parameter
(C++23부터)
함수 선언에서 선언된 매개변수 이름은 보통 자기 문서화 용도로만 쓰여요. 함수 정의에서는 사용되지만(여전히 선택 사항이죠).
타입 이름이 괄호 안에 중첩되면(람다 표현식 포함, C++11부터) 매개변수 목록에서 모호성이 생겨요. 이 경우 선택지는 '함수 포인터 타입의 매개변수 선언'과 'declarator의 식별자 주변에 중복 괄호가 있는 매개변수 선언' 사이입니다. 해결책은 타입 이름을 단순 타입 지정자(즉, 함수 포인터 타입)로 간주하는 거예요:
class C {};
void f(int(C)) {} // void f(int(*fp)(C param)) {}
// NOT void f(int C) {}
void g(int *(C[10])); // void g(int *(*fp)(C param[10]));
// NOT void g(int *C[10]);
타입은 함수의 매개변수 선언이나 반환 타입에서 정의할 수 없어요:
void f(struct S1 {} s); // Error
struct S2 {} g(); // Error
매개변수 타입은 미지의 경계를 가진 배열에 대한 참조나 포인터를 포함하는 타입일 수 없어요. 그런 타입에 대한 다단계 포인터·배열이나, 그런 타입을 매개변수로 갖는 함수에 대한 포인터도 마찬가지입니다.
생략 부호(...) 사용하기
매개변수 목록의 마지막 매개변수는 생략 부호(...)일 수 있어요. 이러면 가변 인자 함수(variadic function)가 됩니다. 생략 부호 앞의 쉼표는 생략할 수 있어요(C++26에서 비권장):
int printf(const char* fmt, ...); // a variadic function
int printf(const char* fmt...); // same as above, but deprecated since C++26
template<typename... Args>
void f(Args..., ...); // a variadic function template with a parameter pack
template<typename... Args>
void f(Args... ...); // same as above, but deprecated since C++26
template<typename... Args>
void f(Args......); // same as above, but deprecated since C++26
함수 타입 (Function type)
함수의 타입은 noexcept의 유무(C++17부터), ref(C++11부터), cv, 매개변수 타입 목록(아래 참고), 반환 타입으로 형성돼요.
매개변수 타입 목록 (Parameter-type-list)
함수의 매개변수 타입 목록은 다음과 같이 결정돼요:
- 각 매개변수(함수 매개변수 팩 포함, C++11부터)의 타입은 자기 자신의 매개변수 선언에서 결정돼요.
- 각 매개변수의 타입을 결정한 후, "T의 배열" 타입이나 함수 타입
T의 매개변수는 "T에 대한 포인터"로 조정돼요. - 매개변수 타입 목록을 만든 후, 매개변수 타입을 수정하는 최상위 cv-한정자는 함수 타입을 형성할 때 삭제돼요.
- 변환된 매개변수 타입 목록과 생략 부호나 함수 매개변수 팩(C++11부터)의 유무가 그 함수의 매개변수 타입 목록이 돼요.
void f(char*); // #1
void f(char[]) {} // defines #1
void f(const char*) {} // OK, another overload
void f(char* const) {} // Error: redefines #1
void g(char(*)[2]); // #2
void g(char[3][2]) {} // defines #2
void g(char[3][3]) {} // OK, another overload
void h(int x(const int)); // #3
void h(int (*)(int)) {} // defines #3
후행 한정자 (Trailing qualifiers)
cv나 ref(C++11부터)를 가진 함수 타입(typedef 이름으로 이름이 붙은 타입 포함)은 다음으로만 나타날 수 있어요:
- 비정적 멤버 함수의 함수 타입
- 멤버 포인터가 가리키는 함수 타입
- 함수 typedef 선언·별칭 선언(C++11부터)의 최상위 함수 타입
- 템플릿 타입 매개변수의 기본 인자 안 type-id
- 템플릿 타입 매개변수의 템플릿 인자 type-id
- 리플렉션 연산자의 피연산자(C++26부터)
typedef int FIC(int) const;
FIC f; // Error: does not declare a member function
struct S
{
FIC f; // OK
};
FIC S::*pm = &S::f; // OK
constexpr auto yeti = ^^void(int) const&; // OK (since C++26)
함수 시그니처 (Function signature)
모든 함수는 시그니처를 가져요. 함수의 시그니처는 이름과 매개변수 타입 목록으로 구성돼요. 시그니처는 둘러싼 네임스페이스도 포함하는데, 다음 예외가 있습니다:
- 함수가 멤버 함수라면, 그 시그니처는 둘러싼 네임스페이스 대신 함수가 멤버인 클래스를 포함해요. 그리고 (있으면) 다음 구성 요소도 포함합니다:
cv,ref(C++11부터), 후행 requires 절(C++20부터).- 함수가 후행 requires 절을 가진 비템플릿 friend 함수라면, 그 시그니처는 둘러싼 네임스페이스 대신 둘러싼 클래스를 포함하고 후행 requires 절도 포함해요. (C++20부터)
except와attr(C++11부터)은 함수 시그니처에 포함되지 않아요. 다만noexcept지정은 함수 타입에 영향을 줍니다(C++17부터).
함수 정의 (Function definition)
비멤버 함수 정의는 오직 네임스페이스 스코프에서만 나타날 수 있어요(중첩 함수는 없습니다). 멤버 함수 정의는 클래스 정의의 본문에도 나타날 수 있어요. 문법은 다음과 같습니다:
attr (optional) decl-specifier-seq (optional) declarator virt-specs (optional) contract-specs (optional) function-body (1)
attr (optional) decl-specifier-seq (optional) declarator requires-clause contract-specs (optional) function-body (2) (since C++20)
- (1) 제약이 없는 함수 정의.
- (2) 제약이 있는 함수 정의.
| attr | - | (C++11부터) 속성 목록. declarator 안의 식별자 뒤에 있는 속성(있으면, 이 페이지 위쪽 참고)과 결합됩니다. |
| decl-specifier-seq | - | 지정자가 있는 반환 타입. 선언 문법과 동일 |
| declarator | - | 함수 선언자. 위 함수 선언 문법과 동일(괄호로 감쌀 수 있음) |
| virt-specs | - | (C++11부터) override, final, 또는 그것들의 임의 순서 조합 |
| requires-clause | - | requires 절 |
| contract-specs | - | (C++26부터) 함수 계약 지정자 목록 |
| function-body | - | 함수 몸통(아래 참고) |
function-body는 다음 중 하나예요:
ctor-initializer (optional) compound-statement (1)
function-try-block (2)
= default ; (3) (since C++11)
= delete ; (4) (since C++11)
= delete ( string-literal ); (5) (since C++26)
- (1) 일반 함수 몸통.
- (2) 함수 try 블록.
- (3) 명시적으로 기본화된(explicitly defaulted) 함수 정의.
- (4) 명시적으로 삭제된(explicitly deleted) 함수 정의.
- (5) 오류 메시지가 있는 명시적으로 삭제된 함수 정의.
| ctor-initializer | - | 멤버 이니셜라이저 목록. 생성자에서만 허용 | | compound-statement | - | 함수의 몸통을 이루는 중괄호로 감싼 문장 시퀀스 | | function-try-block | - | 함수 try 블록 | | string-literal | - | 함수가 삭제된 이유를 설명하는 데 쓸 수 있는, 평가되지 않는 문자열 리터럴 |
int max(int a, int b, int c)
{
int m = (a > b) ? a : b;
return (m > c) ? m : c;
}
// decl-specifier-seq is "int"
// declarator is "max(int a, int b, int c)"
// body is { ... }
함수 몸통은 복합문(중괄호 한 쌍으로 감싼 0개 이상의 문장 시퀀스)이며, 함수가 호출되면 실행돼요. 또한 생성자의 함수 몸통은 다음도 포함합니다:
- 생성자의 멤버 이니셜라이저 목록에 식별자가 없는 모든 비정적 데이터 멤버에 대해, 대응하는 멤버 부분 객체를 초기화하는 데 쓰이는 기본 멤버 이니셜라이저 또는(C++11부터) 기본 초기화(default-initialization)
- 생성자의 멤버 이니셜라이저 목록에 타입 이름이 없는 모든 기반 클래스에 대해, 대응하는 기반 클래스 부분 객체를 초기화하는 데 쓰이는 기본 초기화
함수 정의에 virt-specs가 있으면 반드시 멤버 함수를 정의해야 해요. (C++11부터) 함수 정의에 requires-clause가 있으면 반드시 템플릿화된 함수를 정의해야 해요. (C++20부터)
void f() override {} // Error: not a member function
void g() requires (sizeof(int) == 4) {} // Error: not a templated function
함수 정의의 매개변수 타입과 반환 타입은 (cv-한정될 수 있는) 불완전 클래스 타입일 수 없어요. 함수가 삭제된 것으로 정의된 경우는 제외(C++11부터). 완전성 검사는 함수 몸통에서만 이뤄지므로, 멤버 함수가 자신이 정의된 클래스(또는 그것을 둘러싼 클래스)를 반환하는 것은 정의 시점에 그 클래스가 불완전해도 허용돼요(함수 몸통에서는 완전합니다).
함수 정의의 declarator에 선언된 매개변수들은 몸통 안에서 스코프에 있어요. 매개변수가 함수 몸통에서 사용되지 않으면 이름을 붙일 필요가 없어요(추상 declarator를 쓰면 충분합니다):
void print(int a, int) // second parameter is not used
{
std::printf("a = %d\n", a);
}
매개변수에 대한 최상위 cv-한정자는 함수 선언에서 버려지지만, 함수 몸통에서 보이는 매개변수의 타입은 수정해요:
void f(const int n) // declares function of type void(int)
{
// but in the body, the type of "n" is const int
}
기본화된 함수 (Defaulted functions)
함수 정의가 문법 (3)이면 그 함수는 명시적으로 기본화된 것으로 정의돼요. 명시적으로 기본화된 함수는 특수 멤버 함수 또는 비교 연산자 함수여야 하고(C++20부터), 기본 인자가 없어야 해요.
명시적으로 기본화된 특수 멤버 함수 F1은 암시적으로 선언됐을 대응 특수 멤버 함수 F2와 다음과 같이 달라질 수 있어요:
F1과F2는ref와/또는except가 다를 수 있어요.F2가const C&타입의 비객체 매개변수를 가지면,F1의 대응 비객체 매개변수는C&타입일 수 있어요.F2가 "C에 대한 참조" 타입의 암시적 객체 매개변수를 가지면,F1은 (어쩌면 다른) "C에 대한 참조" 타입의 명시적 객체 매개변수를 가진 명시적 객체 멤버 함수일 수 있어요. 이 경우F1의 타입은 추가 매개변수가 있다는 점에서F2의 타입과 달라집니다. (C++23부터)
F1의 타입이 위 규칙이 허용하는 방식 외의 방식으로 F2와 다르다면:
F1이 대입 연산자이고,F1의 반환 타입이F2의 반환 타입과 다르거나F1의 비객체 매개변수 타입이 참조가 아니면, 프로그램은 ill-formed.- 그렇지 않고
F1이 첫 선언에서 명시적으로 기본화되면, 삭제된 것으로 정의돼요. - 그렇지 않으면 프로그램은 ill-formed.
첫 선언에서 명시적으로 기본화된 함수는 암시적으로 inline이고, constexpr 함수가 될 수 있다면 암시적으로 constexpr이에요.
struct S
{
S(int a = 0) = default; // error: default argument
void operator=(const S&) = default; // error: non-matching return type
~S() noexcept(false) = default; // OK, different exception specification
private:
int i;
S(S&); // OK, private copy constructor
};
S::S(S&) = default; // OK, defines copy constructor
명시적으로 기본화된 함수와 암시적으로 선언된 함수를 통틀어 기본화된 함수(defaulted function)라고 해요. 이것들의 실제 정의는 암시적으로 제공되며, 자세한 내용은 각각의 대응 페이지를 참고하세요.
삭제된 함수 (Deleted functions)
함수 정의가 문법 (4)나 (5)(C++26부터)이면 그 함수는 명시적으로 삭제된 것으로 정의돼요. 삭제된 함수의 어떤 사용도(리플렉션 연산자의 피연산자로 쓰는 경우 제외, C++26부터) ill-formed입니다(프로그램이 컴파일되지 않아요). 여기에는 명시적 호출(함수 호출 연산자 사용)과 암시적 호출(삭제된 오버로드 연산자, 특수 멤버 함수, 할당 함수 등에 대한 호출), 삭제된 함수에 대한 포인터나 멤버 포인터를 만드는 것, 심지어 잠재적으로 평가되지 않는 표현식에서 삭제된 함수를 쓰는 것까지 포함돼요.
순수 가상이 아닌 가상 멤버 함수는 암시적으로 odr-사용되더라도 삭제된 것으로 정의될 수 있어요. 삭제된 함수는 삭제된 함수에 의해서만 오버라이드될 수 있고, 삭제되지 않은 함수는 삭제되지 않은 함수에 의해서만 오버라이드될 수 있어요.
string-literal이 있으면, 구현은 삭제 이유를 보여주거나 대안을 제안하는 결과 진단 메시지의 일부로 그 텍스트를 포함하도록 권장돼요. (C++26부터)
함수가 오버로드되어 있으면 오버로드 해석이 먼저 일어나고, 삭제된 함수가 선택됐을 때만 프로그램이 ill-formed이 돼요:
struct T
{
void* operator new(std::size_t) = delete;
void* operator new[](std::size_t) = delete("new[] is deleted"); // since C++26
};
T* p = new T; // Error: attempts to call deleted T::operator new
T* p = new T[5]; // Error: attempts to call deleted T::operator new[],
// emits a diagnostic message "new[] is deleted"
삭제된 함수의 정의는 번역 단위에서 첫 선언이어야 해요. 이전에 선언된 함수는 삭제된 것으로 재선언할 수 없어요:
struct T { T(); };
T::T() = delete; // Error: must be deleted on the first declaration
사용자 제공 함수 (User-provided functions)
함수는 사용자가 선언하고(user-declared) 첫 선언에서 명시적으로 기본화되거나 삭제되지 않으면 사용자 제공(user-provided)이에요. 사용자 제공·명시적으로 기본화된 함수(즉 첫 선언 이후에 명시적으로 기본화된 함수)는 명시적으로 기본화되는 지점에서 정의돼요. 그런 함수가 삭제된 것으로 암시적으로 정의되면 프로그램은 ill-formed입니다. 함수를 첫 선언 이후에 기본화된 것으로 선언하면, 진화하는 코드베이스에 안정적인 바이너리 인터페이스를 제공하면서 효율적인 실행과 간결한 정의를 얻을 수 있어요.
// All special member functions of "trivial" are
// defaulted on their first declarations respectively,
// they are not user-provided
struct trivial
{
trivial() = default;
trivial(const trivial&) = default;
trivial(trivial&&) = default;
trivial& operator=(const trivial&) = default;
trivial& operator=(trivial&&) = default;
~trivial() = default;
};
struct nontrivial
{
nontrivial(); // first declaration
};
// not defaulted on the first declaration,
// it is user-provided and is defined here
nontrivial::nontrivial() = default;
모호성 해결 (Ambiguity resolution)
함수 몸통과 {나 =(C++26부터)로 시작하는 이니셜라이저 사이에 모호함이 생기면, noptr-declarator의 declarator 식별자 타입을 검사해서 해결해요:
- 타입이 함수 타입이면, 모호한 토큰 시퀀스는 함수 몸통으로 취급돼요.
- 그렇지 않으면, 모호한 토큰 시퀀스는 이니셜라이저로 취급됩니다.
using T = void(); // function type
using U = int; // non-function type
T a{}; // defines a function doing nothing
U b{}; // value-initializes an int object
T c = delete("hello"); // defines a function as deleted
U d = delete("hello"); // copy-initializes an int object with
// the result of a delete expression (ill-formed)
함수 계약 지정자 (Function contract specifiers)
함수 선언과 람다 표현식은 함수 계약 지정자 시퀀스를 포함할 수 있어요. 각 지정자는 다음 문법을 가집니다:
pre attr (optional) ( predicate ) (1)
post attr (optional) ( predicate ) (2)
post attr (optional) ( identifier result-attr (optional) : predicate ) (3)
- (1) 사전 조건 어서션(precondition assertion)을 도입.
- (2), (3) 사후 조건 어서션(postcondition assertion)을 도입. (2)는 결과에 바인딩하지 않고, (3)은 결과에 바인딩.
| attr | - | 도입된 계약 어서션에 적용되는 속성 목록 | | predicate | - | 아무 표현식(괄호를 씌우지 않은 콤마 표현식 제외) | | identifier | - | 결과를 가리키는 식별자 | | result-attr | - | 결과 바인딩에 적용되는 속성 목록 |
사전 조건 어서션과 사후 조건 어서션을 통틀어 함수 계약 어서션(function contract assertion)이라고 해요. 함수 계약 어서션은 함수와 연관된 계약 어서션입니다. 함수 계약 어서션의 predicate는 그 predicate를 문맥적으로 bool로 변환한 것이에요.
다음 함수들은 함수 계약 지정자와 함께 선언될 수 없어요: 가상 함수, 삭제된 함수, 첫 선언에서 기본화된 함수.
사전 조건 어서션 (Precondition assertions)
사전 조건 어서션은 함수 진입과 연관돼요:
int divide(int dividend, int divisor)
pre (divisor != 0)
{
return dividend / divisor;
}
double square_root(double num)
pre (num >= 0)
{
return std::sqrt(num);
}
사후 조건 어서션 (Postcondition assertions)
사후 조건 어서션은 함수가 정상적으로 종료하는 것과 연관돼요. 사후 조건 어서션에 identifier가 있으면, 그 함수 계약 지정자는 해당 함수의 결과 바인딩(result binding)의 이름으로 identifier를 도입해요. 결과 바인딩은 그 함수를 호출했을 때 반환되는 객체나 참조를 가리키며, 결과 바인딩의 타입은 연관 함수의 반환 타입이에요.
int absolute_value(int num)
post (r : r >= 0)
{
return std::abs(num);
}
double sine(double num)
post (r : r >= -1.0 && r <= 1.0)
{
if (std::isnan(num) || std::isinf(num))
// exiting via an exception never causes contract violation
throw std::invalid_argument("Invalid argument");
return std::sin(num);
}
사후 조건 어서션에 identifier가 있고 연관 함수의 반환 타입이 (cv-한정될 수 있는) void면 프로그램은 ill-formed이에요:
void f() post (r : r > 0); // Error: no value can be bound to "r"
비템플릿 함수의 선언된 반환 타입에 자리 표시자 타입이 포함되고, identifier가 있는 사후 조건 어서션은 함수 정의에만 나타날 수 있어요:
auto g(auto&) post (r : r >= 0); // OK, "g" is a template
auto h() post (r : r >= 0); // Error: cannot name the return value
auto k() post (r : r >= 0) // OK, "k" is a definition
{
return 0;
}
계약 일관성 (Contract consistency)
함수나 함수 템플릿 func의 재선언 D는 contract-specs가 없거나, D에서 도달 가능한 어떤 첫 선언 F와 같은 contract-specs를 가져야 해요. D와 F가 다른 번역 단위에 있으면, D가 명명된 모듈에 붙어 있을 때만 진단이 요구된다.
한 번역 단위에서 F1이 func의 첫 선언이고 다른 번역 단위에서 F2가 func의 첫 선언이면, F1과 F2는 같은 contract-specs를 지정해야 해요(진단 불요).
두 contract-specs는 같은 순서로 같은 함수 계약 지정자로 구성돼 있으면 같아요. 함수 선언 D1의 함수 계약 지정자 C1은, 다음 조건이 모두 만족되면 선언 D2의 함수 계약 지정자 C2와 같아요:
C1과C2의 predicate는D1과D2위의 함수 정의에 놓이면 일의 정의 규칙(one-definition rule)을 만족할 것(D1과D2가 다른 번역 단위에 있으면, 각 predicate 안에 정의된 대응 엔터티들은 단일 정의를 가진 단일 엔터티인 것처럼 행동) — 다음 재명명(renaming)을 제외:- 선언된 함수의 매개변수의 재명명
- 선언된 함수를 둘러싼 템플릿의 템플릿 매개변수의 재명명
- 결과 바인딩의 재명명(있는 경우)
C1과C2모두 identifier가 있거나, 둘 다 없음.
이 조건이 predicate 안에 포함된 두 람다 표현식의 비교만으로 충족되지 못한다면 진단은 요구되지 않아요.
bool b1, b2;
void f() pre (b1) pre([]{ return b2; }());
void f(); // OK, function contract specifiers omitted
void f() pre (b1) pre([]{ return b2; }()); // Error: closures have different types
void f() pre (b1); // Error: function contract specifiers are different
int g() post (r : b1);
int g() post (b1); // Error: no result binding
namespace N
{
void h() pre (b1);
bool b1;
void h() pre (b1); // Error: function contract specifiers differ
// according to the one-definition rule
}
(C++26부터)
참고 사항 (Notes)
직접 초기화(direct-initialization) 문법을 사용한 변수 선언과 함수 선언 사이에 모호함이 생기면, 컴파일러는 항상 함수 선언을 선택해요. 직접 초기화 문서를 참고하세요.
| 기능 테스트 매크로 | 값 | 표준 | 기능 |
| __cpp_decltype_auto | 201304L | (C++14) | decltype(auto) |
| __cpp_return_type_deduction | 201304L | (C++14) | 일반 함수의 반환 타입 추론 |
| __cpp_explicit_this_parameter | 202110L | (C++23) | 명시적 객체 매개변수(deducing this) |
| __cpp_deleted_function | 202403L | (C++26) | 이유가 있는 삭제된 함수 |
키워드
default, delete, pre, post
예제
기본 인자, 함수 포인터 반환, 예외 지정, 함수 try 블록, 삭제된 함수까지 함수 선언·정의의 다양한 형태를 확인해 볼게요.
#include <iostream>
#include <string>
// simple function with a default argument, returning nothing
void f0(const std::string& arg = "world!")
{
std::cout << "Hello, " << arg << '\n';
}
// the declaration is in namespace (file) scope
// (the definition is provided later)
int f1();
// function returning a pointer to f0, pre-C++11 style
void (*fp03())(const std::string&)
{
return f0;
}
// function returning a pointer to f0, with C++11 trailing return type
auto fp11() -> void(*)(const std::string&)
{
return f0;
}
int main()
{
f0();
fp03()("test!");
fp11()("again!");
int f2(std::string) noexcept; // declaration in function scope
std::cout << "f2(\"bad\"): " << f2("bad") << '\n';
std::cout << "f2(\"42\"): " << f2("42") << '\n';
}
// simple non-member function returning int
int f1()
{
return 007;
}
// function with an exception specification and a function try block
int f2(std::string str) noexcept
try
{
return std::stoi(str);
}
catch (const std::exception& e)
{
std::cerr << "stoi() failed!\n";
return 0;
}
// deleted function, an attempt to call it results in a compilation error
void bar() = delete
# if __cpp_deleted_function
("reason")
# endif
;
가능한 출력:
stoi() failed!
Hello, world!
Hello, test!
Hello, again!
f2("bad"): 0
f2("42"): 42
더 알아보기 (Learn more)
- C documentation for Declaring functions — C 언어에서의 함수 선언
- 멤버 함수 (member functions) — 클래스 멤버 함수
- 함수 (functions) 개요 — 함수의 기본 개념
- 함수 오버로딩 — 같은 이름의 함수 구분