函数声明

来自cppreference.com
< cpp‎ | language

函数声明引入函数名和它的类型。函数定义将函数名/类型与函数体关联。

函数声明

函数声明可以在任何作用域出现。在类作用域中的函数声明引入成员函数(除非使用 friend 说明符),细节见成员函数友元函数

被声明的函数类型由返回类型(return type)(由声明语法声明说明符序列 提供)和函数声明符(声明符)组成。

非指针声明符 ( 形参列表 ) cv限定符 (可选) 引用限定符 (可选) 异常说明 (可选) 属性 (可选) (1)
非指针声明符 ( 形参列表 ) cv限定符 (可选) 引用限定符 (可选) 异常说明 (可选) 属性 (可选)
-> 尾随返回类型
(2) (C++11 起)

声明符 语法的其他形式见声明页面)

1) 常规函数声明语法
2) 尾随返回类型声明:尾随返回类型只能在最外层函数声明符中使用。此时 声明说明符序列 必须包含关键词 auto
非指针声明符 - 任何合法的 声明符,但如果它以 *&&& 开始,那么它必须被括号环绕。
形参列表 - 函数形参的逗号分隔列表,可以为空(细节见下文)
属性 - (C++11 起) 以逗号分隔且可以为空的属性列表。这些属性应用于函数类型,而非函数自身。在声明符中标识符之后出现的属性与有可能在声明开端出现的属性合并,如果存在。
cv限定符 - const/volatile 限定,只能在非静态成员函数中使用
引用限定符 - (C++11 起) 引用限定,只能在非静态成员函数中使用
异常说明 -

动态异常说明

(C++11 前)

动态异常说明noexcept 说明之一

(C++11 起)
(C++17 前)

noexcept 说明

(C++17 起)

注意,异常说明不是函数类型的一部分。 (C++17 前)

尾随返回类型 - (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 起)

与任何声明相同,ret func(params) 所声明的函数 func 的类型是 ret(params)(但适用下文所述的形参类型重编):参见 类型的命名

返回类型推导

如果函数声明的 声明说明符序列 包含关键词 auto,那么尾随返回类型可以省略,且编译器将从返回语句中所用的表达式的类型推导出它。如果返回类型没有使用 decltype(auto),那么推导遵循模板实参推导的规则进行:

int x = 1;
auto f() { return x; }        // 返回类型是 int
const auto& f() { return x; } // 返回类型是 const int&

如果返回类型是 decltype(auto),那么返回类型是将返回语句中所用的表达式包裹到 decltype 中时所得到的类型:

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 的返回类型已知
}

如果返回语句使用花括号初始化器列表(braced-init-list),那么就不能推导:

auto func () { return {1, 2, 3}; } // 错误

虚函数协程 (C++20 起)不能使用返回类型推导:

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)
属性 (可选) 声明说明符序列 抽象声明符 (可选) (3)
属性 (可选) 声明说明符序列 抽象声明符 (可选) = 初始化器 (4)
void (5)
1) 声明一个具名(形式)参数。声明说明符序列声明符 的含义见声明页面。
int f(int a, int *p, int (*(*x)(double))[3]);
2) 声明一个带有默认值的具名(形式)参数。
int f(int a = 7, int *p = nullptr, int (*(*x)(double))[3] = nullptr);
3) 声明一个无名形参。
int f(int, int *, int (*(*)(double))[3]);
4) 声明一个具有默认值的无名形参。
int f(int = 7, int * = nullptr, int (*(*)(double))[3] = nullptr);
5) 指示函数不接受形参,它是空参数列表的确切同义词:int f(void);int f(); 声明同一函数。注意类型 void(可以有 cv 限定)不能在其他情况下用于参数列表:int f(void, int);int f(const void); 是错误的(但可以使用它的衍生类型,如 void*)。在模板中,只能使用非待决的 void(当以 T = void 实例化时,采用单个 T 类型的形参的函数不会成为无形参函数)。

省略号 ... 可以在形参列表末尾出现;这会声明一个变参函数

int printf(const char* fmt ...);

为了与 C89 兼容,当形参列表含有至少一个形参时,省略号前可以出现一个逗号:

int printf(const char* fmt, ...); // OK,同上

尽管 声明说明符序列 蕴含了可以存在类型说明符之外的说明符,但其他可用的说明符只有 register auto (C++11 前),而且它没有任何效果。

(C++17 前)


如果任何函数形参使用了占位符(placeholder)auto概念(concept)类型),那么函数声明转变为简写函数模板声明:

void f1(auto);    // 同 template<class T> void f(T)
void f2(C1 auto); // 如果 C1 是概念,同 template<C1 T> void f7(T)
(C++20 起)

在函数声明中声明的形参名通常只用作以自身为文档。它们在函数定义中被使用(但仍不强制)。

形参列表中的每个函数形参的类型根据下列规则确定:

1) 首先,以如同在任何声明中的方式,组合声明说明符序列和声明符以确定它的类型。
2) 如果类型是“T 的数组”或“T 的未知边界数组”,那么它会被替换成类型“指向 T 的指针”
3) 如果类型是函数类型 F,那么它被替换成类型“指向 F 的指针”
4) 从形参类型中丢弃顶层 cv 限定符(此调整只影响函数类型,但不改动形参的性质:int f(const int p, decltype(p)*);int f(int, const int*); 声明同一函数)

因为这些规则,下列函数声明确切地声明同一函数:

int f(char s[3]);
int f(char[]);
int f(char* s);
int f(char* const);
int f(char* volatile s);

下列声明也确切地声明同一函数:

int f(int());
int f(int (*g)());

当类型名称被圆括号包围时(包括 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 起)

函数定义

非成员函数的定义只能在命名空间作用域中出现(不存在嵌套函数)。成员函数的定义也可以在类定义的体内出现。它们拥有下列语法:

属性 (可选) 声明说明符序列 (可选) 声明符 虚声明符序列 (可选) 函数体

其中 函数体 是下列之一:

构造函数初始化器 (可选) 复合语句 (1)
函数-try-块 (2)
= delete ; (3) (C++11 起)
= default ; (4) (C++11 起)
1) 常规函数体
2) 函数 try 块(这是包装在 try/catch 块内的常规函数体)
3) 显式弃置的函数定义
4) 显式预置的函数定义,只能用于特殊成员函数比较运算符函数 (C++20 起)
属性 - (C++11 起) 以逗号分隔且可以为空的属性列表。这些属性与可能出现在 声明符 中标识符之后的属性结合(见本页顶部)。
声明说明符序列 - 带有说明符的返回类型,与声明文法相同
声明符 - 函数声明符,与上述函数声明文法相同(可以被圆括号包围)。和函数声明一样,它可以后随 requires-子句 (C++20 起)
虚说明符序列 - (C++11 起) overridefinal,或它们任意顺序的组合(只能用于非静态成员函数)
构造函数初始化器 - 成员初始化器列表,只能用于构造函数
复合语句 - 花括号环绕的语句序列,它们构成函数体
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)”
// 函数体是 { ... }

函数体是一条复合语句(由一对花括号环绕的零或多条语句),它们在函数调用时被执行。

函数的各个形参类型和返回类型不能是(可有 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
}

弃置函数

如果使用特殊语法 = delete ;取代函数体,那么该函数被定义为弃置的(deleted)。任何弃置函数的使用都是非良构的(程序无法编译)。这包含调用,包括显式(以函数调用运算符)及隐式(对弃置的重载运算符、特殊成员函数、分配函数等的调用),构成指向弃置函数的指针或成员指针,甚至是在不潜在求值的表达式中使用弃置函数。但是可以隐式 ODR 使用刚好被弃置的非纯虚成员函数。

如果函数被重载,那么首先进行重载决议,且只有在选择了弃置函数时程序才非良构:

struct sometype
{
    void* operator new(std::size_t) = delete;
    void* operator new[](std::size_t) = delete;
};
sometype* p = new sometype; // 错误:尝试调用弃置的 sometype::operator new

函数的弃置定义必须是翻译单元中的首条声明:已经声明过的函数不能声明为弃置的:

struct sometype { sometype(); };
sometype::sometype() = 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;

__func__

在函数体内,如同以如下方式定义了函数局部的预定义变量 __func__

static const char __func__[] = "函数名";

此变量具有块作用域及静态存储期:

struct S
{
    S(): s(__func__) {} // OK:初始化器列表是函数体的一部分
    const char* s;
};
void f(const char* s = __func__); // 错误:形参列表是声明符的一部分
(C++11 起)

注解

在使用直接初始化语法的变量声明和函数声明之间有歧义的情况下,编译器选择函数声明;见直接初始化页面。

示例

#include <iostream>
#include <string>
 
// 命名空间(文件)作用域中的声明
// (定义在后面提供)
int f1();
 
// 拥有默认实参的简单函数,不返回内容
void f0(const std::string& arg = "world")
{
    std::cout << "Hello, " << arg << '\n';
}
 
// 返回指向 f0 的指针的函数
auto fp11() -> void(*)(const std::string&)
{
    return f0;
}
 
// 返回指向 f0 的指针的函数,C++11 前的风格
void (*fp03())(const std::string&)
{
    return f0;
}
 
int main()
{
    f0();
    fp11()("test");
    fp03()("again");
    int f2(std::string) noexcept; // 函数作用域中的声明
    std::cout << "f2(\"bad\"):" << f2("bad") << '\n';
    std::cout << "f2(\"42\"):" << f2("42") << '\n';
}
 
// 简单的非成员函数,返回 int
int f1()
{
    return 42;
}
 
// 拥有异常说明和函数 try 块的函数
int f2(std::string str) noexcept try
{ 
    return std::stoi(str);
}
catch (const std::exception& e)
{
    std::cerr << "stoi() 失败!\n";
    return 0;
}

输出:

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 显式预置或弃置的函数定义不能带有 overridefinal 说明符 可以带有这些说明符
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 2145 C++98 函数定义中 声明符 不能被圆括号包围 可以包围
CWG 2259 C++11 圆括号包围类型名称时产生的歧义的解决规则未覆盖 lambda 表达式 已覆盖
CWG 2430 C++98 CWG 问题 1824 的解决方案导致在类定义中的
函数定义中不能将该类作为返回类型形参类型
只能在函数体中进行完整性检查

参阅