函数声明
函数声明引入函数名和它的类型。函数定义将函数名/类型与函数体关联起来。
函数声明
函数声明可以在任何作用域出现。类作用域中的函数声明引入成员函数(除非使用 friend 说明符),细节见成员函数和友元函数。
非指针声明符 ( 形参列表 ) cv限定符 (可选) 引用限定符 (可选) 异常说明 (可选) 属性 (可选)
|
(1) | ||||||||
非指针声明符 ( 形参列表 ) cv限定符 (可选) 引用限定符 (可选) 异常说明 (可选) 属性 (可选)-> 尾随返回类型
|
(2) | (C++11 起) | |||||||
(声明符 语法的其他形式见声明页面)
非指针声明符 | - | 任何合法的声明符,但如果它以 * 、& 或 && 开始,那么它必须被括号环绕。
| ||||||
形参列表 | - | 函数形参的逗号分隔列表,可以为空(细节见下文) | ||||||
属性 | - | (C++11 起) 属性的列表。这些属性应用于函数的类型,而非函数自身。声明符中标识符之后出现的属性与声明开端出现的属性合并到一起,如果存在。 | ||||||
cv限定符 | - | const/volatile 限定,只能在非静态成员函数中使用 | ||||||
引用限定符 | - | (C++11 起) 引用限定,只能在非静态成员函数中使用 | ||||||
异常说明 | - |
| ||||||
尾随返回类型 | - | (C++11 起) 尾随返回类型,当返回类型取决于实参名时,例如 template<class T, class U> auto add(T t, U u) -> decltype(t + u);,或当返回类型复杂时,例如在 auto fpif(int)->int(*)(int) 中,尾随返回类型很有用 |
如声明页面所示,声明符可以后随 requires 子句,它声明与该函数关联的约束,而重载决议所要选择的函数必须满足该制约。(例如 void f1(int a) requires true;)注意,关联的制约是函数签名的一部分,但不是函数类型的一部分。 |
(C++20 起) |
只要声明说明符序列允许,函数声明符就可以和其他声明符混合:
// 声明一个 int、一个 int*、一个函数,及一个函数指针 int a = 1, *p = NULL, f(), (*pf)(double); // 声明说明符序列 是 int // 声明符 f() 声明(但不定义)一个不接受实参并返回 int 的函数 struct S { virtual int f(char) const, g(int) &&; // 声明两个非静态成员函数 virtual int f(char), x; // 编译时错误:(声明说明符序列中的)virtual // 只能声明非静态成员函数 };
以 volatile 限定的对象类型作为形参类型或返回类型是被弃用的。 |
(C++20 起) |
函数的返回类型不能是函数类型或数组类型(但可以是到它们的指针或引用)。
与任何声明相同,声明前出现的属性和声明符中直接跟在标识符之后的属性都会应用到所声明或定义的实体(在这个例子中,应用到函数): [[noreturn]] void f [[noreturn]] (); // OK:两个属性都应用到函数 f 然而,(按上述语法)在声明符后出现的属性会应用到函数类型而非函数自身: void f() [[noreturn]]; // 错误:此属性对函数自身没有影响 |
(C++11 起) |
返回类型推导如果函数声明的声明说明符序列 包含关键词 auto,那么尾随返回类型可以省略,且编译器将从返回语句中所用的表达式的类型推导出它。如果返回类型没有使用 decltype(auto),那么推导遵循模板实参推导的规则进行: int x = 1; auto f() { return x; } // 返回类型是 int const auto& f() { return x; } // 返回类型是 const int& 如果返回类型是 decltype(auto),那么返回类型是将返回语句中所用的表达式包裹到 int x = 1; decltype(auto) f() { return x; } // 返回类型是 int,同 decltype(x) decltype(auto) f() { return (x); } // 返回类型是 int&,同 decltype((x)) (注意:“const decltype(auto)&”是错误的,decltype(auto) 必须独自使用) 如果有多条返回语句,那么它们必须推导出相同的类型: auto f(bool val) { if (val) return 123; // 推导出返回类型 int else return 3.14f; // 错误:推导出返回类型 float } 如果没有返回语句或返回语句的实参是 void 表达式,那么所声明的返回类型,必须要么是 decltype(auto),此时推导返回类型是 void,要么是(可有 cv 限定的)auto,此时推导的返回类型是(具有相同 cv 限定的)void。 auto f() {} // 返回 void auto g() { return f(); } // 返回 void auto* x() {} // 错误: 不能从 void 推导 auto* 一旦在函数中见到一条返回语句,那么从该语句推导的返回类型就可以用于函数的剩余部分,包括其他返回语句: auto sum(int i) { if (i == 1) return i; // sum 的返回类型是 int else return sum(i - 1) + i; // OK,sum 的返回类型已知 } 如果返回语句使用花括号包围的初始化器列表,那么就不能推导: auto func() { return {1, 2, 3}; } // 错误 struct F { virtual auto f() { return 2; } // 错误 }; 除了用户定义转换函数以外的函数模板可以使用返回类型推导。即使返回语句中的表达式并非待决,推导也在实例化时发生。这种实例化并不处于 SFINAE 的目的的立即语境中。 template<class T> auto f(T t) { return t; } typedef decltype(f(1)) fint_t; // 实例化 f<int> 以推导返回类型 template<class T> auto f(T* t) { return *t; } void g() { int (*p)(int*) = &f; } // 实例化两个 f 以确定返回类型, // 选择第二个模板重载 使用返回类型推导的函数或函数模板的重声明或特化必须使用同一返回类型占位符: auto f(int num) { return num; } // int f(int num); // 错误:返回类型未使用占位符 // decltype(auto) f(int num); // 错误:占位符不同 template<typename T> auto g(T t) { return t; } template auto g(int); // OK:返回类型是 int // template char g(char); // 错误:不是主模板 g 的特化 反过来也一样:不使用返回类型推导的函数或函数模板的重声明或特化不能使用返回类型占位符: int f(int num); // auto f(int num) { return num; } // 错误:不是 f 的重声明 template<typename T> T g(T t) { return t; } template int g(int); // OK:特化 T 为 int // template auto g(char); // 错误:不是主模板 g 的特化 显式实例化声明本身并不会实例化使用返回类型推导的函数模板: template<typename T> auto f(T t) { return t; } extern template auto f(int); // 不会实例化 f<int> int (*p)(int) = f; // 实例化 f<int> 以确定它的返回类型, // 但仍需要在程序的别处出现显式实例化的定义 |
(C++14 起) |
形参列表
形参列表决定调用函数时所能指定的实参。它是形参声明 的逗号分隔列表,其中每一项拥有下列语法:
属性 (可选) 声明说明符序列 声明符 | (1) | ||||||||
属性 (可选) |
(2) | (C++23 起) | |||||||
属性 (可选) 声明说明符序列 声明符 = 初始化器
|
(3) | ||||||||
属性 (可选) 声明说明符序列 抽象声明符 (可选) | (4) | ||||||||
属性 (可选) |
(5) | (C++23 起) | |||||||
属性 (可选) 声明说明符序列 抽象声明符 (可选) = 初始化器
|
(6) | ||||||||
void
|
(7) | ||||||||
省略号 ...
可以在形参列表末尾出现;这会声明一个变参函数:
int printf(const char* fmt ...);
为了与 C89 兼容,当形参列表含有至少一个形参时,省略号前可以出现一个逗号:
int printf(const char* fmt, ...); // OK,同上
尽管声明说明符序列 暗示可以存在类型说明符之外的说明符,但可用的其他说明符只有 register 和 auto (C++11 前),而且它没有任何效果。 |
(C++17 前) |
如果任何函数形参使用了占位符(auto 或 概念类型),那么函数声明转变为简写函数模板声明: void f1(auto); // 同 template<class T> void f(T) void f2(C1 auto); // 如果 C1 是概念,同 template<C1 T> void f7(T) |
(C++20 起) |
带说明符 this 的形参声明(形式 (2)/(5))声明的是显式对象形参。 显式对象形参不能是函数形参包,并且只能作为以下声明中的形参列表的首个形参: 有显式对象形参的成员函数具有以下限制: struct C { void f(this C& self); // OK template<typename Self> void g(this Self&& self); // 模板页 OK void p(this C) const; // 错误:此处不允许使用 “const” static void q(this C); // 错误:此处不允许使用 “static” void r(int, this C); // 错误:显式对象形参只能是首个形参 }; // void func(this C& self); // 错误:非成员函数不能有显式对象形参 |
(C++23 起) |
函数声明中声明的形参名通常只用作以自身为文档。它们在函数定义中被使用(但仍不强制)。
当类型名称被圆括号包围时(包括 lambda 表达式) (C++11 起)会产生歧义。此时可以解析成类型是函数指针的形参的声明以及声明符 中的标识符被额外的圆括号包围的形参的声明。解决方案是将该类型名称视为简单类型说明符(此时它即是函数指针类型):
class C {}; void f(int(C)) {} // void f(int(*fp)(C param)) {} // 不是 void f(int C) {} void g(int *(C[10])); // void g(int *(*fp)(C param[10])); // 不是 void g(int *C[10]);
形参类型不能是含有到未知边界数组的引用或指针的类型,含有这种类型的多级指针/数组,或含有指向以这些类型为形参的函数的指针。
指示可变实参的省略号前不需要有逗号,即使它跟随指示形参包展开的省略号,所以下列函数模板是严格相同的: template<typename... Args> void f(Args..., ...); template<typename... Args> void f(Args... ...); template<typename... Args> void f(Args......); 使用这种声明的例子之一是 std::is_function 的可能的实现。 运行此代码 #include <cstdio> template<typename... Variadic, typename... Args> constexpr void invoke(auto (*fun)(Variadic......), Args... args) { fun(args...); } int main() { invoke(std::printf, "%dm•%dm•%dm = %d%s%c", 2, 3, 7, 2 * 3 * 7, "m³", '\n'); } 输出: 2m•3m•7m = 42m³ |
(C++11 起) |
函数类型
形参类型列表
函数的形参类型列表 按以下方式确定:
- 每个形参(包括形参包) (C++11 起)的类型通过形参自身的声明确定。
- 在确定每个形参的类型后,类型是 “
T
的数组”或某个函数类型T
的形参会调整为具有类型“指向T
的指针”。 - 在产生形参类型的列表后,在组成函数类型时会移除所有修改了形参类型的顶层 cv 限定符。
- 转换后的形参类型的列表,加上是否有出现省略号或者形参包 (C++11 起)的特征共同组成了函数的形参类型列表。
void f(char*); // #1 void f(char[]) {} // 定义了 #1 void f(const char*) {} // OK,另一重载 void f(char* const) {} // 错误:重定义了 #1 void g(char(*)[2]); // #2 void g(char[3][2]) {} // 定义了 #2 void g(char[3][3]) {} // OK,另一重载 void h(int x(const int)); // #3 void h(int (*)(int)) {} // 定义了 #3
确定函数类型
在语法 (1) 中,在假设非指针声明符 是独立声明的情况下,给定非指针声明符 中有限定标识 或无限定标识 的类型为“T
派生的声明符类型列表”:
|
(C++17 起) |
- 否则, (C++17 起)声明的函数的类型是
“返回T
的 cv限定符 (可选) 引用限定符 (可选) (C++11 起)接受形参类型列表的函数 派生的声明符类型列表”。
在语法 (2) 中,在假设非指针声明符 是独立声明的情况下,给定非指针声明符 中有限定标识 或无限定标识 的类型为“ |
(C++11 起) |
|
(C++17 起) |
存在属性 时,它会应用到函数类型。 |
(C++11 起) |
// f1 的类型是“返回 void 的接受 int 的函数,带有属性 noreturn” void f1(int a) [[noreturn]]; // f2 的类型是“返回 int 的接受指向 int 的指针的 constexpr noexcept 函数” constexpr auto f2(int[] b) noexcept -> int; struct X { // f3 的类型是“返回 const int 的 const 的不接受参数的函数” const int f3() const; };
尾随限定符
有 cv限定符 或引用限定符 (C++11 起)的函数类型(包括以 typedef
名命名的类型)只能作为以下类型出现:
- 非静态成员函数的类型。
- 成员指针指代的函数类型。
- 函数 typedef 声明或别名声明 (C++11 起)的顶层函数类型。
- 模板类型形参的默认实参中的类型标识。
- 模板类型形参对应的模板实参的类型标识。
typedef int FIC(int) const; FIC f; // 错误:声明的不是成员函数 struct S { FIC f; // OK }; FIC S::*pm = &S::f; // OK
函数签名
每个函数都有一个签名。
函数签名包含函数的名字和形参类型列表。除以下情况外,函数签名也包含函数的外围命名空间:
- 如果函数是成员函数,那么它的签名会包含以该函数为成员的类而非它的外围命名空间。它的签名也包含以下组分(如果存在):
- cv限定符
|
(C++11 起) |
|
(C++20 起) |
异常说明 和属性 (C++11 起)不是函数签名的一部分,尽管 noexcept 说明会影响函数类型 (C++17 起)。
函数定义
非成员函数的定义只能在命名空间作用域中出现(不存在嵌套函数)。成员函数的定义也可以在类定义的体内出现。它们拥有下列语法:
属性 (可选) 声明说明符序列 (可选) 声明符 虚声明符序列 (可选) 函数体 | (1) | ||||||||
属性 (可选) 声明说明符序列 (可选) 声明符 requires子句 函数体 | (2) | (C++20 起) | |||||||
属性 | - | (C++11 起) 属性的列表。这些属性与可能出现在声明符 中标识符之后的属性(见本页顶部)合并到一起。 |
声明说明符序列 | - | 带有说明符的返回类型,与声明文法相同 |
声明符 | - | 函数声明符,与上述函数声明语法相同(可以被圆括号包围) |
虚说明符序列 | - | (C++11 起) override 、final ,或它们任意顺序的组合(只能用于非静态成员函数)
|
requires子句 | - | requires 子句 |
函数体 | - | 函数体(见下文) |
函数体 是下列之一:
构造函数初始化器 (可选) 复合语句 | (1) | ||||||||
函数try块 | (2) | ||||||||
= default ;
|
(3) | (C++11 起) | |||||||
= delete ;
|
(4) | (C++11 起) | |||||||
= delete ( 字符串字面量 );
|
(5) | (C++26 起) | |||||||
构造函数初始化器 | - | 成员初始化器列表,只能用于构造函数 |
复合语句 | - | 花括号环绕的语句序列,它们构成函数体 |
函数try块 | - | 函数 try 块 |
字符串字面量 | - | 不求值字符串字面量,可以用于解释函数为何被弃置的理由 |
int max(int a, int b, int c) { int m = (a > b) ? a : b; return (m > c) ? m : c; } // 声明说明符序列是“int” // 声明符是“max(int a, int b, int c)” // 函数体是 { ... }
函数体是一条复合语句(由一对花括号环绕的零或多条语句),它们在函数调用时被执行。构造函数的函数体还包括以下内容:
- 对于所有标识符未在该构造函数的成员初始化器列表中出现的非静态数据成员,用于初始化对应成员子对象的默认成员初始化式或 (C++11 起)默认初始化。
- 对于所有类型名未在该构造函数的成员初始化器列表中出现的基类,用于初始化对应基类子对象的默认初始化。
如果函数定义包含了虚声明符序列,那么定义的必须是成员函数。 |
(C++11 起) |
如果函数定义包含了 requires子句,那么定义的必须是模板化函数。 |
(C++20 起) |
void f() override {} // 错误:不是成员函数 void g() requires (sizeof(int) == 4) {} // 错误:不是模板化函数
函数的各个形参类型和返回类型不能是(可有 cv 限定的)不完整的类类型,除非函数已显式定义为被弃置 (C++11 起)。完整性检查只会在函数体中进行,因此成员函数可以返回在其中定义它们的类(或它的外围类),尽管在定义点它可能不完整(它在函数体内完整)。
在函数定义的声明符 中声明的形参在函数体内处于作用域中。如果某个形参没有在函数体中使用,那么它不需要具名(只需要使用抽象声明符):
void print(int a, int) // 没有使用第二个形参 { std::printf("a = %d\n", a); }
尽管形参上的顶层 cv 限定符在函数声明中被忽略,它们仍然会修饰形参的类型,这在函数体中可见:
void f(const int n) // 声明 void(int) 类型的函数 { // 但在体内,n 的类型是 const int }
预置函数如果函数定义具有语法 (3),那么该函数被定义为显式预置的。 显式预置的函数必须是特殊成员函数或比较运算符函数 (C++20 起),并且不能有默认实参。 显式预置的特殊成员函数
|
(C++11 起) |
|
(C++23 起) |
如果
在首个声明被显式预置的函数隐式内联,并且在它可以是 constexpr 函数的情况下隐式具有 constexpr。 struct S { S(int a = 0) = default; // 错误:有默认实参 void operator=(const S&) = default; // 错误:返回类型不匹配 ~S() noexcept(false) = default; // OK,异常说明可以不同 private: int i; S(S&); // OK,私有构造函数 }; S::S(S&) = default; // OK,定义了构造函数 显式预置的函数和隐式声明的函数统称为预置 函数。它们的实际定义会隐式提供,详情见这些函数各自对应的页面。 弃置函数如果函数定义具有语法 (4) 或 (5) (C++26 起),那么该函数被定义为显式弃置的。 任何弃置函数的使用都是非良构的(程序无法编译)。这包含调用,包括显式(以函数调用运算符)及隐式(对弃置的重载运算符、特殊成员函数、分配函数等的调用),构成指向弃置函数的指针或成员指针,甚至是在不潜在求值的表达式中使用弃置函数。 非纯虚成员函数可以被定义为弃置,即便它被隐式 ODR 使用。弃置函数只能被弃置函数覆写,而非弃置函数只能被非弃置函数覆写。 |
(C++11 起) |
如果出现字符串字面量,那么鼓励实现将此文本作为其给出的诊断消息的一部分,用以展示弃置的理由或者给出替代建议。 |
(C++26 起) |
如果函数被重载,那么首先进行重载决议,且只有在选择了弃置函数时程序才非良构: struct T { void* operator new(std::size_t) = delete; void* operator new[](std::size_t) = delete("new[] 已被弃置"); // C++26 起 }; T* p = new T; // 错误:尝试调用弃置的 T::operator new T* p = new T[5]; // 错误:尝试调用弃置的 T::operator new[],产生诊断消息“new[] 已被弃置” 函数的弃置定义必须是翻译单元中的首条声明:已经声明过的函数不能声明为弃置的: struct T { T(); }; T::T() = delete; // 错误:必须在首条声明弃置 由用户提供的函数如果一个函数由用户声明且没有在它的首个声明被显式预置或显式弃置,那么它由用户提供。由用户提供的显式预置的函数(即在它的首个声明后被显式预置)在它被显式预置的地方定义;如果该函数被隐式定义为弃置的,那么程序非良构。需要为不断变化的代码库提供稳定的二进制接口的情况下,在函数的首个声明后再定义为预置可以保证执行效率,也能提供简明的定义。 // trivial 的所有特殊成员函数都分别在它们的首个声明处被显式预置, // 因此它们都不由用户提供 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(); // 首个声明 }; // 没有在首个声明处被显式预置, // 因此该函数由用户提供并在此定义 nontrivial::nontrivial() = default; 解决歧义在以
using T = void(); // 函数类型 using U = int; // 非函数类型 T a{}; // 定义一个什么也不做的函数 U b{}; // 值初始化一个 int 对象 T c = delete("hello"); // 定义一个被弃置的函数 U d = delete("hello"); // 以 delete 表达式的结果复制初始化一个 int 对象(非良构)
__func__在函数体内,如同以如下方式定义了函数局部的预定义变量 __func__: static const char __func__[] = "函数名"; 此变量具有块作用域及静态存储期: struct S { S(): s(__func__) {} // OK:初始化器列表是函数体的一部分 const char* s; }; void f(const char* s = __func__); // 错误:形参列表是声明符的一部分 |
(C++11 起) |
注解
在使用直接初始化语法的变量声明和函数声明之间有歧义的情况下,编译器选择函数声明;见直接初始化页面。
功能特性测试宏 | 值 | 标准 | 功能特性 |
---|---|---|---|
__cpp_decltype_auto |
201304L | (C++14) | decltype(auto)
|
__cpp_return_type_deduction |
201304L | (C++14) | 普通函数的返回类型推导 |
__cpp_explicit_this_parameter |
202110L | (C++23) | 显式对象形参(推导 this) |
__cpp_deleted_function |
202403L | (C++26) | 带有理由的弃置函数 |
关键词
示例
#include <iostream> #include <string> // 拥有默认实参的简单函数,不返回内容 void f0(const std::string& arg = "world!") { std::cout << "Hello, " << arg << '\n'; } // 命名空间(文件)作用域中的声明 // (定义在后面提供) int f1(); // 返回指向 f0 的指针的函数,C++11 前的风格 void (*fp03())(const std::string&) { return f0; } // 返回指向 f0 的指针的函数 auto fp11() -> void(*)(const std::string&) { return f0; } int main() { f0(); fp03()("test!"); fp11()("again!"); int f2(std::string) noexcept; // 函数作用域中的声明 std::cout << "f2(\"bad\"):" << f2("bad") << '\n'; std::cout << "f2(\"42\"):" << f2("42") << '\n'; } // 简单的非成员函数,返回 int int f1() { return 007; } // 拥有异常说明和函数 try 块的函数 int f2(std::string str) noexcept try { return std::stoi(str); } catch (const std::exception& e) { std::cerr << "stoi() 失败!\n"; return 0; } // 弃置函数,尝试调用它会造成编译错误 void bar() = delete # if __cpp_deleted_function ("理由") # endif ;
可能的输出:
stoi() 失败! Hello, world! Hello, test! Hello, again! f2("bad"):0 f2("42"):42
缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
缺陷报告 | 应用于 | 出版时的行为 | 正确行为 |
---|---|---|---|
CWG 135 | C++98 | 类内的成员函数定义的形参和返回值不能是类本身,因为它还不完整 | 允许此类定义 |
CWG 332 | C++98 | 函数形参可以是有 cv 限定的 void 类型 | 已禁止 |
CWG 393 | C++98 | 含有到未知边界数组的指针/引用的类型不能作为形参 | 允许这些类型 |
CWG 452 | C++98 | 成员初始化器列表不是函数体的一部分 | 是函数体的一部分 |
CWG 577 | C++98 | 待决 void 类型可以用来声明无形参函数 | 只有非待决的 void 可以 |
CWG 1327 | C++11 | 显式预置或弃置的函数定义不能带有 override 或 final 说明符 | 可以带有这些说明符 |
CWG 1355 | C++11 | 只有特殊成员函数能由用户提供 | 拓展到所有函数 |
CWG 1394 | C++11 | 弃置函数不能有不完整类型的形参或返回不完整类型 | 允许这些地方有不完整的返回类型 |
CWG 1824 | C++98 | 函数定义的返回类型和形参类型的完整性检查也会在函数定义的语境外进行 | 只能在函数定义的语境中检查 |
CWG 1877 | C++14 | return; 在返回类型推导中被视为 return void(); | 此时直接将返回类型推导成 void |
CWG 2015 | C++11 | 虚弃置函数的隐式 ODR 使用非良构 | 将此类 ODR 使用从使用禁止豁免 |
CWG 2044 | C++14 | 返回 void 但声明的返回类型是 decltype(auto) 的函数的返回类型推导会失败 |
更新推导规则以处理这种情况 |
CWG 2081 | C++14 | 即使函数的最初声明没有使用返回类型推导,它的重声明也可以使用 | 不能使用 |
CWG 2144 | C++11 | {} 在同一个地方既可以是函数体也可以是初始化器 | 通过声明符标识符的类型来区分 |
CWG 2145 | C++98 | 函数定义中声明符 不能被圆括号包围 | 可以包围 |
CWG 2259 | C++11 | 圆括号包围类型名称时产生的歧义的解决规则未覆盖 lambda 表达式 | 已覆盖 |
CWG 2430 | C++98 | CWG 问题 1824 的解决方案导致在类定义中的 函数定义中不能将该类作为返回类型形参类型 |
只能在函数体中检查 |
CWG 2760 | C++98 | 构造函数的函数体不包括未在常规函数体中指定的初始化 | 也包括这些初始化 |
CWG 2831 | C++20 | 带有 requires子句 的函数定义可以定义非模板化函数 | 已禁止 |
CWG 2846 | C++23 | 显式对象成员函数不能有类外定义 | 可以有 |