您的位置:首页 > 游戏 > 游戏 > 如何策划网站_外贸建站用的服务器_荥阳seo_网络运营团队

如何策划网站_外贸建站用的服务器_荥阳seo_网络运营团队

2024/12/24 2:24:44 来源:https://blog.csdn.net/qq_55125921/article/details/144332942  浏览:    关键词:如何策划网站_外贸建站用的服务器_荥阳seo_网络运营团队
如何策划网站_外贸建站用的服务器_荥阳seo_网络运营团队

文章目录

      • C++ 函数声明概述
        • 函数声明的语法
        • 特殊情况
        • 示例代码解析
      • 关键点解释
      • 总结
    • 参数列表(Parameter List)详解
        • 详细说明
      • 总结
    • 函数类型与参数类型列表(Function Type and Parameter-Type-List)
        • 参数类型列表(Parameter-Type-List)
        • 确定函数类型
        • 尾置限定符(Trailing Qualifiers)
        • 函数签名(Function Signature)
      • 总结
    • 函数定义(Function Definition)
        • 语法
        • 函数体
        • 示例
        • 成员函数的特殊规则
        • 参数和返回类型的完整性
        • 参数作用域
        • 显式默认函数
        • 显式删除函数
        • 用户提供的函数
      • 总结
    • 模糊解析(Ambiguity Resolution)
        • 模糊解析规则
      • 总结
    • 默认参数(Default Arguments)
        • 语法
        • 示例
        • 规则
      • 总结
    • 可变参数(Variadic Functions)
        • 语法
        • 默认转换(Default Argument Promotions)
        • 访问可变参数
          • 示例代码
        • 重载解析
        • 替代方法
        • 注意事项
      • 总结
    • Lambda 表达式(Lambda Expressions)
        • 语法
        • 组件解释
        • 特性说明符 (`specs`) 详细解释
        • 捕获规则
        • 泛型 lambda (Generic Lambdas)
        • 示例代码
        • 内置变量 `__func__`
      • 总结
    • 闭包类型(Closure Type)
        • 闭包类型的特性
        • 示例代码
        • 悬挂引用 (Dangling References)
          • 示例:悬挂引用的风险
      • 总结
      • 闭包类型的用户定义转换函数
        • 用户定义转换函数的特性
        • 示例代码
      • 闭包类型的构造和赋值
        • 示例代码
      • 闭包类型的成员数据
        • 示例代码
      • Lambda 表达式的限制
        • 示例代码
      • 总结
      • Lambda 捕获机制的示例
        • 1. **捕获默认**
        • 2. **捕获列表**
        • 3. **捕获规则**
        • 4. **重复捕获和参数名冲突**
        • 5. **捕获范围**
        • 6. **初始化器捕获**
        • 7. **隐式捕获**
        • 8. **ODR-使用**
        • 9. **捕获成员和 `this`**
        • 10. **捕获和默认参数**
        • 11. **嵌套 Lambda 捕获**
        • 12. **捕获类型限制**
      • 总结

C++ 函数声明概述

函数声明(Function Declaration)在 C++ 中用于引入函数的名字和类型,而函数定义(Function Definition)则将函数名/类型与函数体关联起来。函数声明可以在任何作用域中出现,包括全局作用域、命名空间作用域、类作用域等。

函数声明的语法

函数声明的基本形式如下:

noptr-declarator ( parameter-list ) cv-qualifier(optional) ref-qualifier(optional) noexcept-specifier(optional) attribute-specifier-seq(optional)

或者使用尾置返回类型(Trailing Return Type)的形式:

noptr-declarator ( parameter-list ) -> trailing-return-type cv-qualifier(optional) ref-qualifier(optional) noexcept-specifier(optional) attribute-specifier-seq(optional)
  • noptr-declarator: 任何有效的声明符,但若以 *, &, 或 && 开头,则必须用括号包围。
  • parameter-list: 可能为空,逗号分隔的参数列表。
  • cv-qualifier: constvolatile 限定符,仅允许在非静态成员函数声明中。
  • ref-qualifier: 引用限定符 &&&,仅允许在非静态成员函数声明中。
  • noexcept-specifier: 异常说明符,可以是动态异常说明或 noexcept 说明。
  • attribute-specifier-seq: 属性说明符序列,应用于函数类型而非函数本身。
  • trailing-return-type: 尾置返回类型,当返回类型依赖于参数名称或复杂时非常有用。
特殊情况
  • 类成员函数:

    • 在类作用域内的函数声明默认为类成员函数,除非使用 friend 关键字。
    • 成员函数可以有 constvolatile 限定符,以及引用限定符 &&&
  • 约束条件 (since C++20):

    • 函数声明可以包含 requires 子句,指定函数的关联约束条件,这些条件必须满足才能被重载解析选中。
  • 返回类型推导 (since C++14):

    • 如果声明符序列包含 auto,则可以省略尾置返回类型,编译器会根据 return 语句中的表达式类型推导返回类型。
    • decltype(auto) 用于更精确的类型推导。
    • 多个 return 语句必须推导出相同类型。
    • 没有 return 语句或 return 语句返回 void 表达式的函数,其返回类型必须是 voiddecltype(auto)
    • 一旦在一个 return 语句中推导了返回类型,后续的 return 语句可以使用该类型。
    • 不能从花括号初始化列表推导返回类型。
  • 虚拟函数和协程 (since C++20):

    • 虚拟函数和协程不能使用返回类型推导。
  • 模板函数:

    • 模板函数可以使用返回类型推导,推导在实例化时进行。
    • 重新声明或特化函数模板时,必须使用相同的返回类型占位符。
  • 显式实例化声明:

    • 显式实例化声明不会实例化使用返回类型推导的函数模板,但会在其他地方需要时实例化。
示例代码解析
// Regular function declaration
int add(int a, int b);// Function with const and volatile qualifiers
struct S {int f() const;int g() volatile;
};// Function with reference qualifiers
struct T {void h() &;  // lvalue reference qualifiervoid i() &&; // rvalue reference qualifier
};// Trailing return type
auto multiply(int a, int b) -> int;// Return type deduction
auto deduce_return_type() { return 42; }  // return type is int
decltype(auto) deduce_exact_type() { return 42; }  // return type is int// Function template with return type deduction
template<typename T>
auto template_func(T t) { return t; }// Redeclaration of a function using the same placeholder
auto f(int num) { return num; }
template auto template_func<int>(int);  // OK: specialize for int// Error cases
// int f(int num);            // Error: no placeholder return type
// decltype(auto) f(int num); // Error: different placeholder
// template char template_func(char); // Error: not a specialization of the primary template// Explicit instantiation declaration
extern template auto template_func(int);  // does not instantiate template_func<int>// Instantiation of template_func<int> when used
int (*p)(int) = template_func;  // instantiates template_func<int> to determine its return type

关键点解释

  1. 普通函数声明:

    • int add(int a, int b); 声明了一个返回 int 的函数 add,接受两个 int 参数。
  2. 成员函数限定符:

    • int f() const; 声明了一个常量成员函数 f,表示该函数不会修改对象的状态。
    • int g() volatile; 声明了一个易变成员函数 g,表示该函数可能会被优化器忽略。
  3. 引用限定符:

    • void h() &; 声明了一个左值引用限定的成员函数 h,只能通过左值调用。
    • void i() &&; 声明了一个右值引用限定的成员函数 i,只能通过右值调用。
  4. 尾置返回类型:

    • auto multiply(int a, int b) -> int; 使用尾置返回类型声明了一个返回 int 的函数 multiply
  5. 返回类型推导:

    • auto deduce_return_type() { return 42; } 编译器推导出返回类型为 int
    • decltype(auto) deduce_exact_type() { return 42; } 编译器推导出返回类型为 int,并且保持原始类型特性。
  6. 模板函数:

    • template<typename T> auto template_func(T t) { return t; } 是一个模板函数,返回类型由 T 推导。
    • template auto template_func<int>(int); 是对模板函数的特化声明,返回类型为 int
  7. 错误示例:

    • 重新声明或特化函数时,必须使用相同的返回类型占位符。
    • 显式实例化声明不会实例化使用返回类型推导的函数模板,但在使用时会实例化。
  8. 显式实例化声明:

    • extern template auto template_func(int); 不会实例化 template_func<int>,但在使用时会实例化。

总结

C++ 函数声明提供了丰富的语法和功能,支持多种类型的限定符、返回类型推导、模板函数等。理解这些规则有助于编写更加灵活和高效的代码,特别是在处理复杂类型和模板编程时。特别是返回类型推导和模板函数的实例化规则,开发者需要特别注意,以确保代码的正确性和可维护性。

参数列表(Parameter List)详解

参数列表决定了调用函数时可以指定的参数。它是由逗号分隔的参数声明列表,每个参数声明具有以下语法形式:

  1. 普通参数声明:
    attr (optional) decl-specifier-seq declarator
    
  2. 显式对象参数声明 (since C++23):
    attr (optional) this decl-specifier-seq declarator
    
  3. 带有默认值的参数声明:
    attr (optional) decl-specifier-seq declarator = initializer
    
  4. 无名参数声明:
    attr (optional) decl-specifier-seq abstract-declarator (optional)
    
  5. 无名显式对象参数声明 (since C++23):
    attr (optional) this decl-specifier-seq abstract-declarator (optional)
    
  6. 带有默认值的无名参数声明:
    attr (optional) decl-specifier-seq abstract-declarator (optional) = initializer
    
  7. 空参数列表:
    void
    
详细说明
  1. 普通参数声明:

    • attr 是可选的属性说明符序列。
    • decl-specifier-seq 是声明说明符序列,包括类型说明符、存储类说明符等。
    • declarator 是声明符,定义参数的名称和类型。

    示例:

    int f(int a, int* p, int (*(*x)(double))[3]);
    
    • int a 是一个整型参数。
    • int* p 是一个指向整型的指针参数。
    • int (*(*x)(double))[3] 是一个复杂的嵌套指针参数,表示一个指向函数的指针,该函数接受一个 double 参数并返回一个指向包含 3 个整数的数组的指针。
  2. 显式对象参数声明 (since C++23):

    • 使用 this 关键字声明显式对象参数,通常用于成员函数中。
    • 显式对象参数必须是第一个参数,并且不能是模板参数包。
    • 成员函数不能是静态的,也不能有 constvolatile 限定符。

    示例:

    struct C {void f(this C& self);     // OKtemplate<typename Self>void g(this Self&& self); // OK for templatesvoid p(this C) const;     // Error: “const” not allowed herestatic void q(this C);    // Error: “static” not allowed herevoid r(int, this C);      // Error: an explicit object parameter can only be the first parameter
    };
    
  3. 带有默认值的参数声明:

    • 可以为参数提供默认值,当调用函数时未提供该参数时使用默认值。

    示例:

    int f(int a = 7, int* p = nullptr, int (*(*x)(double))[3] = nullptr);
    
  4. 无名参数声明:

    • 可以省略参数名称,仅声明类型。

    示例:

    int f(int, int*, int (*(*)(double))[3]);
    
  5. 变参函数(Variadic Functions):

    • 使用 ... 表示变参函数,允许传递任意数量的参数。
    • 变参函数可以与命名参数结合使用,... 必须是最后一个参数。

    示例:

    int printf(const char* fmt, ...); // OK
    
  6. 空参数列表:

    • void 表示函数不接受任何参数,等同于空参数列表。

    示例:

    int f(void); // 等同于 int f();
    
  7. 类型限制:

    • void 类型(即使带有 constvolatile 限定符)不能作为参数类型,但其派生类型(如 void*)可以。
    • 模板中,只有非依赖的 void 类型可以使用。
  8. 占位符参数 (since C++20):

    • 如果参数使用 auto 或概念类型(concept type),则函数声明被视为简化的函数模板声明。

    示例:

    void f1(auto);    // 等同于 template<class T> void f1(T)
    void f2(C1 auto); // 等同于 template<C1 T> void f2(T),如果 C1 是一个概念
    
  9. 参数名称:

    • 参数名称在函数声明中主要用于自文档化目的,在函数定义中是必需的。
    • 参数名称可以省略,但在函数定义中必须提供。
  10. 歧义解析:

    • 当类型名称嵌套在括号中时,可能会出现歧义。编译器会将其解析为指针到函数类型,而不是带有多余括号的标识符。

    示例:

    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])
    
  11. 变参模板中的省略号:

    • 在变参模板中,... 可以出现在多个位置,但它们的效果相同。

    示例:

    template<typename... Args>
    void f(Args..., ...); // 与下面的声明等效template<typename... Args>
    void f(Args... ...);template<typename... Args>
    void f(Args......);
    
  12. 示例代码:
    下面是一个使用变参模板和 std::printf 的示例,展示了如何通过模板传递参数给 printf 函数。

    #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++ 的参数列表提供了丰富的语法和功能,支持多种类型的参数声明,包括普通参数、显式对象参数、带有默认值的参数、变参函数等。理解这些规则有助于编写更加灵活和强大的函数,特别是在处理复杂类型和模板编程时。特别是变参模板和显式对象参数的引入,使得 C++ 的函数声明更加简洁和表达力更强。

函数类型与参数类型列表(Function Type and Parameter-Type-List)

在 C++ 中,函数的类型和参数类型列表是确定函数签名和行为的关键部分。理解这些概念对于编写正确且高效的代码至关重要。以下是关于函数类型和参数类型列表的详细总结。

参数类型列表(Parameter-Type-List)

函数的参数类型列表是根据以下规则确定的:

  1. 参数类型的确定:

    • 每个参数的类型由其自身的参数声明决定。
    • 如果参数类型是“数组类型 T”或“函数类型 T”,则调整为“指向 T 的指针”。
  2. 删除顶层 cv 限定符:

    • 在形成函数类型时,删除任何顶层的 constvolatile 限定符。
  3. 变参函数和函数参数包 (since C++11):

    • 参数类型列表中是否包含省略号 ... 或函数参数包决定了函数是否为变参函数。
  4. 示例:

    void f(char*);         // #1
    void f(char[]) {}      // 定义 #1,char[] 调整为 char*
    void f(const char*) {} // 另一个重载,const char* 是不同的类型
    void f(char* const) {} // 错误:重新定义 #1,char* const 删除顶层 const 后仍是 char*void g(char(*)[2]);   // #2
    void g(char[3][2]) {} // 定义 #2,char[3][2] 调整为 char(*)[2]
    void g(char[3][3]) {} // 另一个重载,char[3][3] 是不同的类型void h(int x(const int)); // #3
    void h(int (*)(int)) {}   // 定义 #3,int x(const int) 调整为 int (*)(int)
    
确定函数类型

在函数声明中,函数类型由以下因素决定:

  1. 普通函数声明 (syntax (1)):

    • 假设 noptr-declarator 是独立声明,给定 qualified-idunqualified-id 的类型为“derived-declarator-type-list T”:
      • 如果异常说明是非抛出的 (noexcept),则函数类型为:
        “derived-declarator-type-list noexcept function of parameter-type-list cv (optional) ref (optional) returning T”
        
      • 否则,函数类型为:
        “derived-declarator-type-list function of parameter-type-list cv (optional) ref (optional) returning T”
        
  2. 尾置返回类型声明 (syntax (2)) (since C++11):

    • 假设 noptr-declarator 是独立声明,给定 qualified-idunqualified-id 的类型为“derived-declarator-type-list T”(此时 T 必须是 auto):
      • 如果异常说明是非抛出的 (noexcept),则函数类型为:
        “derived-declarator-type-list noexcept function of parameter-type-list cv (optional) ref (optional) returning trailing”
        
      • 否则,函数类型为:
        “derived-declarator-type-list function of parameter-type-list cv (optional) ref (optional) returning trailing”
        
  3. 属性应用 (since C++11):

    • 属性说明符(attr)应用于函数类型。

    示例:

    // 函数 "f1" 的类型是
    // “function of int returning void, with attribute noreturn”
    void f1(int a) [[noreturn]];// 函数 "f2" 的类型是
    // “constexpr noexcept function of pointer to int returning int”
    constexpr auto f2(int[] b) noexcept -> int;struct X {// 函数 "f3" 的类型是// “function of no parameter const returning const int”const int f3() const;
    };
    
尾置限定符(Trailing Qualifiers)

带有 cvref 限定符的函数类型可以出现在以下情况中:

  1. 非静态成员函数的函数类型。
  2. 成员指针所指向的函数类型。
  3. 函数类型别名声明或别名模板声明中的顶级函数类型。
  4. 模板类型参数的默认参数中的类型标识符。
  5. 模板参数的类型标识符。

示例:

typedef int FIC(int) const;
FIC f;     // 错误:不声明成员函数struct S {FIC f; // 正确
};FIC S::*pm = &S::f; // 正确
函数签名(Function Signature)

每个函数都有一个唯一的签名,签名由以下部分组成:

  1. 函数名称
  2. 参数类型列表
  3. 命名空间(如果函数是成员函数,则使用类名代替命名空间)。
  4. 成员函数的附加信息 (since C++11):
    • cv 限定符(如 constvolatile)。
    • 引用限定符(如 &&&)。
  5. 尾置约束子句 (since C++20):
    • 如果函数是非模板友元函数且包含尾置约束子句,则签名包含类名和尾置约束子句。
  6. 异常说明和属性 (since C++11):
    • noexcept 说明影响函数类型,但不参与函数签名。
    • 属性(如 [[noreturn]])也不参与函数签名。

示例:

// 函数 "f1" 的签名是
// f1(int)
void f1(int a);// 函数 "f2" 的签名是
// f2(int*)
constexpr auto f2(int[] b) noexcept -> int;struct X {// 函数 "f3" 的签名是// X::f3()const int f3() const;
};

总结

C++ 的函数类型和参数类型列表是通过一系列规则确定的,确保了函数的正确声明和定义。理解这些规则有助于编写更加灵活和高效的代码,特别是在处理复杂类型、模板编程和函数重载时。特别是尾置返回类型、显式对象参数和变参模板的引入,使得 C++ 的函数声明更加简洁和表达力更强。同时,函数签名的概念确保了函数的唯一性和正确的重载解析。

函数定义(Function Definition)

在 C++ 中,函数定义用于实现函数的主体逻辑。根据函数的类型和作用域,函数定义有不同的语法和规则。以下是关于函数定义的详细总结。

语法

函数定义的基本语法如下:

  1. 普通函数定义:

    attr (optional) decl-specifier-seq (optional) declarator virt-specifier-seq (optional) function-body
    
  2. 带约束的函数定义 (since C++20):

    attr (optional) decl-specifier-seq (optional) declarator requires-clause function-body
    
  • attr:可选的属性列表,应用于函数类型。
  • decl-specifier-seq:返回类型的说明符序列,包括类型说明符和其他修饰符。
  • declarator:函数声明符,与函数声明中的语法相同(可以加括号)。
  • virt-specifier-seq:虚函数限定符序列,如 overridefinal(仅限成员函数)。
  • requires-clause:约束子句,用于模板函数(仅限 C++20 及以后版本)。
  • function-body:函数体,包含具体实现。
函数体

函数体可以是以下几种形式之一:

  1. 常规函数体:

    ctor-initializer (optional) compound-statement
    
    • ctor-initializer:构造函数的成员初始化列表(仅限构造函数)。
    • compound-statement:由大括号包围的语句序列,构成函数体。
  2. 函数 try 块:

    function-try-block
    
  3. 显式默认函数定义 (since C++11):

    = default;
    
  4. 显式删除函数定义 (since C++11):

    = delete;
    
  5. 显式删除函数定义并提供错误信息 (since C++26):

    = delete (string-literal);
    
示例
int max(int a, int b, int c)
{int m = (a > b) ? a : b;return (m > c) ? m : c;
}
  • decl-specifier-seqint
  • declaratormax(int a, int b, int c)
  • body{ ... }
成员函数的特殊规则
  1. 虚函数限定符:

    • 如果函数定义包含 virt-specifier-seq,则必须定义为成员函数。
    • 例如:
      void f() override {} // 错误:不是成员函数
      
  2. 约束子句 (since C++20):

    • 如果函数定义包含 requires-clause,则必须定义为模板函数。
    • 例如:
      void g() requires (sizeof(int) == 4) {} // 错误:不是模板函数
      
参数和返回类型的完整性
  • 函数定义中的参数类型和返回类型不能是指向不完整类类型的指针或引用,除非函数被定义为 delete(C++11 及以后版本)。
  • 完整性检查仅在函数体内进行,这允许成员函数返回其所属类(或其外围类),即使在定义时该类是不完整的(在函数体内是完整的)。
参数作用域
  • 在函数定义的声明符中声明的参数在函数体内可见。

  • 如果参数未在函数体内使用,则可以省略参数名称(使用抽象声明符):

    void print(int a, int) // 第二个参数未使用
    {std::printf("a = %d\n", a);
    }
    
  • 即使顶层 cv 限定符在函数声明中被丢弃,它们仍然会影响函数体内参数的类型:

    void f(const int n) // 声明为 void(int)
    {// 但在函数体内,n 的类型是 const int
    }
    
显式默认函数
  • 如果函数定义使用 = default 语法,则该函数被定义为显式默认函数。

  • 显式默认函数必须是特殊成员函数或比较运算符函数(C++20 及以后版本),并且不能有默认参数。

  • 显式默认的特殊成员函数可以与隐式声明的版本有所不同,但差异必须符合特定规则:

    • 可以有不同的引用限定符或异常说明。
    • 如果隐式声明的版本有一个非对象参数类型为 const C&,显式默认版本的相应非对象参数可以是 C&
    • 如果隐式声明的版本有一个隐式对象参数类型为“引用到 C”,显式默认版本可以是一个显式对象成员函数,其显式对象参数类型为“引用到 C”(C++23 及以后版本)。
  • 如果显式默认函数在第一次声明时被定义,默认情况下它是内联的,并且如果它可以是一个 constexpr 函数,则它也是 constexpr 的。

  • 示例:

    struct S {S(int a = 0) = default;             // 错误:有默认参数void operator=(const S&) = default; // 错误:返回类型不匹配~S() noexcept(false) = default;     // 正确:不同的异常说明
    private:int i;S(S&);          // 正确:私有的拷贝构造函数
    };S::S(S&) = default; // 正确:定义拷贝构造函数
    
显式删除函数
  • 如果函数定义使用 = delete 语法,则该函数被定义为显式删除函数。

  • 使用显式删除函数会导致编译错误,包括直接调用、间接调用(如通过重载解析选择删除的函数)、构造指向删除函数的指针或成员指针,甚至在非潜在求值表达式中使用。

  • 删除的函数只能被其他删除的函数覆盖,非删除的函数只能被非删除的函数覆盖。

  • 如果提供了字符串字面量,实现应鼓励将该文本包含在诊断消息中,解释删除的原因或建议替代方案(C++26 及以后版本)。

  • 示例:

    struct T {void* operator new(std::size_t) = delete;void* operator new[](std::size_t) = delete("new[] is deleted"); // C++26
    };T* p = new T;    // 错误:尝试调用删除的 T::operator new
    T* p = new T[5]; // 错误:尝试调用删除的 T::operator new[],发出诊断消息 “new[] is deleted”
    
  • 删除的函数定义必须是翻译单元中的第一个声明,之前声明的函数不能重新声明为删除的:

    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;
    

总结

C++ 的函数定义提供了丰富的语法和功能,支持多种类型的函数实现。理解这些规则有助于编写正确且高效的代码,特别是在处理复杂类型、模板编程、函数重载和特殊成员函数时。特别是显式默认和删除函数的引入,使得 C++ 的函数定义更加灵活和表达力更强。同时,用户提供的函数概念确保了对函数行为的精确控制,适应不断演化的代码库需求。
.

模糊解析(Ambiguity Resolution)

在 C++ 中,当编译器遇到可能解释为函数体或初始化器的代码时,可能会出现模糊性。为了正确解析这些情况,C++ 标准规定了具体的规则来解决这种模糊性。以下是关于模糊解析的详细总结。

模糊解析规则
  1. 函数体与初始化器的模糊:

    • 当编译器遇到以 {= 开头的代码时,可能会出现模糊性。
    • 解析规则如下:
      • 如果声明符标识符的类型是函数类型,则该代码序列被视为函数体。
      • 否则,该代码序列被视为初始化器。
  2. 示例:

    using T = void(); // 函数类型
    using U = int;    // 非函数类型T a{}; // 定义一个什么都不做的函数
    U b{}; // 值初始化一个 int 对象T c = delete("hello"); // 定义一个删除的函数
    U d = delete("hello"); // 使用 delete 表达式的结果复制初始化一个 int 对象(非法)
    
  3. __func__ 预定义变量:

    • 在函数体内,预定义变量 __func__ 被定义为静态常量字符数组,包含函数名称。
    • 该变量具有块作用域和静态存储持续时间。
  4. 示例:

    struct S {S(): s(__func__) {} // OK: 初始化列表是函数体的一部分const char* s;
    };void f(const char* s = __func__); // 错误:参数列表是声明符的一部分
    
  5. 运行示例代码:

    #include <iostream>void Foo() { std::cout << __func__ << ' '; }struct Bar {Bar() { std::cout << __func__ << ' '; }~Bar() { std::cout << __func__ << ' '; }struct Pub { Pub() { std::cout << __func__ << ' '; } };
    };int main() {Foo();Bar bar;Bar::Pub pub;
    }
    

    可能的输出:

    Foo Bar Pub ~Bar
    
  6. 注意事项:

    • 如果在直接初始化语法和函数声明之间存在模糊性,编译器总是选择函数声明;详见直接初始化。
    • 特征测试宏:
      • __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) - 带有原因的删除函数。
  7. 关键字:

    • default
    • delete
  8. 示例代码:

    #include <iostream>
    #include <string>// 简单函数,带默认参数,返回 void
    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 的指针,带有 C++11 尾置返回类型
    auto fp11() -> void(*)(const std::string&) {return f0;
    }// 简单非成员函数,返回 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() failed!\n";return 0;
    }// 删除的函数,尝试调用会导致编译错误
    void bar() = delete
    #if __cpp_deleted_function("reason")
    #endif
    ;int main() {f0();fp03()("test!");fp11()("again!");std::cout << "f2(\"bad\"): " << f2("bad") << '\n';std::cout << "f2(\"42\"): " << f2("42") << '\n';// 尝试调用删除的函数// bar(); // 编译错误
    }
    

    可能的输出:

    Hello, world!
    Hello, test!
    Hello, again!
    stoi() failed!
    f2("bad"): 0
    f2("42"): 42
    

总结

C++ 的模糊解析规则确保了编译器能够正确区分函数体和初始化器,特别是在使用直接初始化语法时。理解这些规则有助于编写无歧义的代码,避免潜在的编译错误。特别是 __func__ 预定义变量的使用,提供了方便的方式来获取当前函数的名称,增强了代码的可读性和调试能力。此外,显式删除函数的功能允许开发者明确禁止某些函数的使用,并提供详细的错误信息,进一步提升了代码的安全性和健壮性。

默认参数(Default Arguments)

在 C++ 中,默认参数允许函数在调用时省略一个或多个尾随参数,从而简化函数调用。默认参数的使用规则和行为如下:

语法

默认参数可以在 函数声明参数列表 中指定,使用 = 操作符来提供初始值。以下是具体的语法形式:

  1. 普通函数声明:

    attr (optional) decl-specifier-seq declarator = initializer
    
  2. 抽象声明符 (用于模板等):

    attr (optional) decl-specifier-seq abstract-declarator (optional) = initializer
    
示例
void point(int x = 3, int y = 4);point(1, 2); // 调用 point(1, 2)
point(1);    // 调用 point(1, 4)
point();     // 调用 point(3, 4)
规则
  1. 尾随参数限制:

    • 默认参数只能应用于尾随参数(即参数列表中最后的参数)。
    • 一旦某个参数有了默认值,其后的所有参数也必须有默认值。
    int x(int = 1, int); // 错误:只有尾随参数可以有默认参数
    
  2. 多次声明:

    • 如果函数在同一作用域中被多次声明,可以在后续声明中添加默认参数。
    • 所有可见的默认参数会在函数调用时合并。
    • 不能为已有默认参数的参数再次提供默认参数,即使值相同。
    void f(int, int);     // #1
    void f(int, int = 7); // #2 OK: 添加默认参数void h() {f(3); // #1 和 #2 可见;调用 f(3, 7)void f(int = 1, int); // 错误:第二个参数已有默认参数
    }
    
  3. 不同翻译单元中的内联函数:

    • 如果内联函数在不同的翻译单元中声明,默认参数集在每个翻译单元结束时必须相同。
  4. 不同翻译单元中的非内联函数:

    • 如果非内联函数在不同的命名空间作用域中声明,默认参数必须相同,但某些默认参数可能在某些翻译单元中不存在。
  5. friend 声明:

    • 如果 friend 声明指定了默认参数,则它必须是 friend 函数定义,并且不允许在翻译单元中进行其他声明。
  6. using 声明:

    • using 声明会传递已知的默认参数集,并且如果稍后在函数的命名空间中添加了更多默认参数,这些默认参数在 using 声明可见的任何地方都可见。
    namespace N {void f(int, int = 1);
    }using N::f;void g() {f(7); // 调用 f(7, 1);f();  // 错误:缺少参数
    }namespace N {void f(int = 2, int);
    }void h() {f();  // 调用 f(2, 1);
    }
    
  7. 类成员函数:

    • 对于非模板类的成员函数,默认参数可以在类体外部定义,并与类体内部声明提供的默认参数合并。
    • 如果这些外部默认参数将成员函数变成默认构造函数或复制/移动构造函数/赋值运算符(这会使调用模棱两可),则程序格式错误。
    • 对于模板类的成员函数,所有默认参数必须在成员函数的初始声明中提供。
    class C {void f(int i = 3);void g(int i, int j = 99);C(int arg); // 非默认构造函数
    };void C::f(int i = 3) {}         // 错误:默认参数已在类作用域中指定
    void C::g(int i = 88, int j) {} // OK: 在此翻译单元中,C::g 可以无参数调用
    C::C(int arg = 1) {}            // 错误:变为默认构造函数
    
  8. 虚函数:

    • 虚函数的覆盖者不会从基类声明中获取默认参数。
    • 当进行虚函数调用时,默认参数是根据对象的静态类型确定的。
    struct Base {virtual void f(int a = 7);
    };struct Derived : Base {void f(int a) override;
    };void m() {Derived d;Base& b = d;b.f(); // OK: 调用 Derived::f(7)d.f(); // 错误:没有默认参数
    }
    
  9. 局部变量和 this 指针:

    • 默认参数中不允许使用局部变量,除非它们不被求值。
    • 默认参数中不允许使用 this 指针。
    void f() {int n = 1;extern void g(int x = n); // 错误:局部变量不能作为默认参数extern void h(int x = sizeof n); // OK: 未求值上下文
    }class A {void f(A* p = this) {} // 错误:this 不允许
    };
    
  10. 非静态类成员:

    • 非静态类成员不允许在默认参数中使用(即使它们没有被求值),除非用于构成指向成员的指针或成员访问表达式。
    class X {int a;int mem1(int i = a); // 错误:非静态成员不能使用static int b;int mem2(int i = b); // OK: 查找 X::b,静态成员int mem3(int X::* i = &X::a); // OK: 非静态成员可以使用int mem4(int i = x.a); // OK: 成员访问表达式
    };
    
  11. 复合赋值运算符:

    • 如果参数名称不存在,可能需要使用空格来避免复合赋值运算符。
    void f1(int*=0);         // 错误:'*=' 是意外的
    void g1(const int&=0);   // 错误:'&=' 是意外的
    void f2(int* = 0);       // OK
    void g2(const int& = 0); // OK
    void h(int&&=0);         // OK 即使没有空格,'&&' 是一个标记
    
  12. 默认参数的求值:

    • 每次调用函数时,都会对默认参数进行求值,前提是相应参数没有传入实参。
    • 除了在不被求值的情况下,函数参数不允许在默认参数中使用。请注意,参数列表中较早出现的参数在作用域中。
    int a;int f(int a, int b = a); // 错误:参数 a 用于默认参数
    int g(int a, int b = sizeof a); // 错误直到解决 CWG 2082// 解决后:未求值上下文是 OK 的
    
  13. 函数类型的一部分:

    • 默认参数不是函数类型的一部分。
    int f(int = 0);void h() {int j = f(1);int k = f(); // 调用 f(0);
    }int (*p1)(int) = &f;
    int (*p2)()    = &f; // 错误:f 的类型是 int(int)
    
  14. 运算符函数:

    • 运算符函数不能有默认参数,除了函数调用运算符。
    class C {int operator[](int i = 0); // 错误int operator()(int x = 0); // OK
    };
    
  15. 显式对象参数 (自 C++23 起):

    • 显式对象参数不能有默认参数。
    struct S { void f(this const S& = S{}); }; // 错误
    

总结

默认参数是 C++ 中非常有用的功能,可以简化函数调用并提高代码的灵活性。然而,使用默认参数时需要注意一些规则和限制,以确保代码的正确性和可维护性。特别是要避免在默认参数中使用局部变量、this 指针和非静态类成员,以及注意不同翻译单元中的默认参数一致性。此外,理解默认参数的求值时机和作用域也是编写高效代码的关键。

可变参数(Variadic Functions)

在 C++ 中,可变参数允许函数接受任意数量的额外参数。这为编写灵活的函数提供了便利,特别是在处理不确定数量的输入时。以下是关于 C++ 中可变参数的详细总结。

语法
  1. 传统可变参数:

    • 函数声明参数列表 之后使用 ... 表示可变参数。
    • 如果参数列表不为空,可以在 ... 之前加上逗号,以保持与 C 语言的兼容性。
    int printx(const char* fmt, ...); // 传统的可变参数函数
    
  2. 空参数列表的可变参数:

    • 可以声明一个没有任何命名参数的可变参数函数。
    int printz(...); // 合法,但参数无法便携地访问
    
  3. 省略号的位置:

    • ... 必须是参数列表中的最后一个元素,不能出现在中间或作为非尾部参数。
    int printy(..., const char* fmt); // 错误:... 不能出现在参数列表中间
    
默认转换(Default Argument Promotions)

当调用可变参数函数时,传递给可变参数部分的每个参数会经历以下默认转换:

  1. std::nullptr_t 转换为 void*:

    • nullptr 会被转换为 void* 指针。
  2. 浮点数提升:

    • float 参数会被提升为 double
  3. 整数提升:

    • boolcharshort 和无作用域枚举类型会被提升为 int 或更宽的整数类型。
  4. 类类型:

    • 非 POD 类类型(直到 C++11)、作用域枚举和具有合格非平凡复制构造函数、移动构造函数或析构函数的类类型,在可能求值的调用中具有实现定义的语义,并有条件地支持。
    • 这些类型在未求值的调用中始终支持。
访问可变参数

为了访问可变参数的值,可以使用 <cstdarg> 头文件提供的宏:

  • va_start:

    • 启用对可变参数的访问。
    • 需要一个 va_list 类型的变量和一个指向最后一个命名参数的引用。
  • va_arg:

    • 访问下一个可变参数。
    • 需要一个 va_list 类型的变量和预期的参数类型。
  • va_copy (C++11):

    • 创建可变参数的副本。
    • 用于需要多次遍历可变参数的情况。
  • va_end:

    • 结束对可变参数的遍历。
    • 释放 va_list 占用的资源。
  • va_list:

    • 保存 va_startva_argva_endva_copy 所需的信息。
示例代码
#include <iostream>
#include <cstdarg>int sum(int count, ...) {va_list args;va_start(args, count);int total = 0;for (int i = 0; i < count; ++i) {total += va_arg(args, int);}va_end(args);return total;
}int main() {std::cout << "Sum: " << sum(3, 1, 2, 3) << std::endl; // 输出: Sum: 6return 0;
}
重载解析

由于可变参数在重载解析中的优先级最低,它们通常用作 SFINAE(Substitution Failure Is Not An Error)中的兜底选项。这使得可变参数函数可以用作其他更具体重载的后备选择。

替代方法
  1. 可变参数模板 (自 C++11 起):

    • 可变参数模板提供了更好的替代方案,因为它们不会对参数类型施加限制,不会执行整数和浮点数提升,并且是类型安全的。
    template<typename... Args>
    void print(Args... args) {(std::cout << ... << args) << '\n';
    }int main() {print("Hello", 42, 3.14); // 输出: Hello423.14return 0;
    }
    
  2. std::initializer_list (自 C++11 起):

    • 如果所有可变参数共享一个公共类型,std::initializer_list 提供了一种便捷的机制来访问这些参数。然而,std::initializer_list 只能提供指向其元素的常量指针,因此无法修改参数。
    void print(const std::initializer_list<int>& list) {for (auto& elem : list) {std::cout << elem << ' ';}std::cout << '\n';
    }int main() {print({1, 2, 3, 4}); // 输出: 1 2 3 4return 0;
    }
    
注意事项
  1. 省略号之前的最后一个参数:

    • 如果省略号之前的最后一个参数是引用类型,或者其类型与默认参数提升后的类型不兼容,则 va_start 宏的行为是未定义的。
  2. 包展开或 lambda 捕获:

    • 如果使用包展开或 lambda 捕获产生的实体作为 va_start 中的最后一个参数,则程序是错误的,无需诊断。
  3. C 语言兼容性:

    • 在 C 语言中,至少需要在省略号参数之前出现一个命名参数。因此,int printz(...); 在 C23 之前是无效的。而在 C++ 中,这种形式是允许的,即使传递给此类函数的参数不可访问,并且通常用作 SFINAE 中的回退重载。
  4. 简化函数模板 (自 C++20 起):

    • 可以使用 auto... 来简化函数模板的声明,使省略号表示可变参数函数而不是可变参数模板。
    void f1(auto...);   // 与 template<class... Ts> void f3(Ts...) 相同
    void f2(auto, ...); // 与 template<class T> void f3(T...) 相同
    

总结

可变参数函数在 C++ 中提供了处理不确定数量参数的能力,广泛应用于标准库和用户定义的函数中。虽然传统的可变参数函数(如 printf)仍然有用,但可变参数模板和 std::initializer_list 通常是更好的选择,因为它们提供了更强的类型安全性和灵活性。了解如何正确使用 <cstdarg> 库以及可变参数的默认转换规则,可以帮助你编写更加健壮和高效的代码。

Lambda 表达式(Lambda Expressions)

从 C++11 开始,C++ 引入了 lambda 表达式,用于构造闭包:一种匿名函数对象,能够捕获其作用域内的变量。lambda 表达式在 C++ 中广泛应用于标准库和用户定义的代码中,提供了简洁且强大的功能。

语法

lambda 表达式的语法可以分为两种主要形式:

  1. 无显式模板参数列表的 lambda 表达式 (可能为非泛型):

    [captures] front-attr (optional) (params) specs (optional) exception (optional)
    back-attr (optional) trailing-type (optional) requires (optional) { body }
    
  2. 带显式模板参数列表的 lambda 表达式 (总是泛型) (自 C++20 起):

    [captures] <tparams> t-requires (optional) front-attr (optional) (params) specs (optional)
    exception (optional) back-attr (optional) trailing-type (optional) requires (optional) { body }
    

此外,还有几种简化形式:

  • 最简单的语法 (直到 C++23):

    [captures] { body }
    
  • 带有 front-attrtrailing-type 的简化形式 (自 C++23 起):

    [captures] front-attr (optional) trailing-type (optional) { body }
    
  • 带有 exceptionback-attr 的简化形式 (自 C++23 起):

    [captures] front-attr (optional) exception back-attr (optional) trailing-type (optional) { body }
    
  • 带有 specsexceptionback-attr 的简化形式 (自 C++23 起):

    [captures] front-attr (optional) specs exception (optional) back-attr (optional) trailing-type (optional) { body }
    
组件解释
  1. 捕获列表 (captures):

    • 逗号分隔的捕获列表,可以选择以捕获默认值开头。
    • 捕获列表可以为空,表示不捕获任何变量。
    • 捕获方式包括:
      • 按值捕获 (=): 捕获所有局部变量的副本。
      • 按引用捕获 (&): 捕获所有局部变量的引用。
      • 混合捕获: 可以同时使用 =&,并在捕获列表中指定个别变量的捕获方式。
      • 个别捕获: 可以单独指定某些变量的捕获方式,例如 [x, &y]
  2. 模板参数列表 (<tparams>) (自 C++20 起):

    • 用于泛型 lambda,允许 lambda 表达式接受任意类型的参数。
    • 模板参数列表必须非空。
  3. 前置属性 (front-attr) (自 C++23 起):

    • 应用于 operator() 的属性说明符序列,例如 [[noreturn]]
  4. 参数列表 (params):

    • operator() 的参数列表。
    • 可以包含显式对象参数 (自 C++23 起)。
  5. 特性说明符 (specs):

    • 一组特性说明符,每个特性最多只能出现一次。
    • 包括 mutableconstexprconstevalstatic
  6. 异常说明 (exception):

    • operator() 提供动态异常说明或 noexcept 说明 (直到 C++20)。
  7. 后置属性 (back-attr):

    • 应用于 operator() 类型的属性说明符序列,例如 [[noreturn]] 不能用于此位置。
  8. 返回类型 (trailing-type):

    • 使用 -> ret 指定返回类型。
  9. 约束 (requires) (自 C++20 起):

    • operator() 添加约束条件。
  10. 函数体 (body):

    • lambda 表达式的函数体。
特性说明符 (specs) 详细解释
特性效果
mutable允许函数体修改按值捕获的对象,并调用它们的非 const 成员函数。不能与显式对象参数一起使用 (自 C++23 起)。
constexpr (自 C++17 起)显式指定 operator() 是一个 constexpr 函数。如果 operator() 满足所有 constexpr 函数的要求,即使没有显式指定 constexpr,它也会是 constexpr
consteval (自 C++20 起)指定 operator() 是一个立即函数。constevalconstexpr 不能同时指定。
static (自 C++23 起)指定 operator() 是一个静态成员函数。staticmutable 不能同时指定。不能与非空捕获列表或显式对象参数一起使用。
捕获规则
  1. 无需捕获的情况:

    • 如果变量是非局部变量或具有静态或线程局部存储持续时间,则可以在不捕获的情况下使用该变量。
    • 如果变量是一个引用,并且已经通过常量表达式初始化,则可以在不捕获的情况下使用该变量。
  2. 读取变量值而无需捕获的情况:

    • 如果变量具有 const 非易失性整数或枚举类型,并且已经通过常量表达式初始化,则可以在不捕获的情况下读取其值。
    • 如果变量是 constexpr 并且没有可变成员,则可以在不捕获的情况下读取其值。
泛型 lambda (Generic Lambdas)
  • 如果参数类型使用 auto 或提供了显式模板参数列表 (自 C++20 起),则 lambda 表达式是泛型 lambda。
  • 泛型 lambda 的 operator() 是一个模板函数。
示例代码
#include <iostream>
#include <vector>
#include <algorithm>int main() {int x = 10;auto lambda = [x]() mutable { return ++x; }; // 按值捕获 x,并允许修改std::cout << lambda() << std::endl; // 输出: 11auto generic_lambda = [](auto a, auto b) { return a + b; }; // 泛型 lambdastd::cout << generic_lambda(1, 2) << std::endl; // 输出: 3std::cout << generic_lambda(1.5, 2.5) << std::endl; // 输出: 4.0std::vector<int> vec = {1, 2, 3, 4, 5};auto print = [](const int& n) { std::cout << n << " "; };std::for_each(vec.begin(), vec.end(), print); // 输出: 1 2 3 4 5std::cout << std::endl;// 带有前置和后置属性的 lambda[[nodiscard]] auto checked_add = [](int a, int b) -> int {return a + b;};// 带有 constexpr 的 lambda (自 C++17 起)constexpr auto add = [](int a, int b) { return a + b; };static_assert(add(2, 3) == 5);// 带有 consteval 的 lambda (自 C++20 起)consteval auto multiply = [](int a, int b) { return a * b; };static_assert(multiply(2, 3) == 6);// 带有 static 的 lambda (自 C++23 起)auto static_lambda = []() static { return 42; };std::cout << static_lambda() << std::endl; // 输出: 42return 0;
}
内置变量 __func__
  • 在 lambda 表达式的函数体内,内置变量 __func__ 会被隐式定义,其行为与普通函数中的 __func__ 相同,即它是一个静态常量字符数组,包含 lambda 表达式的名称(通常是 "operator()")。

总结

lambda 表达式是 C++ 中非常强大且灵活的工具,允许你创建匿名函数对象,并捕获其作用域内的变量。它们广泛应用于各种场景,如标准库算法、回调函数和事件处理等。了解 lambda 表达式的语法和特性,可以帮助你编写更加简洁、高效和易于维护的代码。特别是泛型 lambda 和新引入的特性(如 constexprconstevalstatic),进一步增强了 lambda 表达式的功能和适用范围。

闭包类型(Closure Type)

在 C++ 中,lambda 表达式是一个返回值表达式(prvalue expression),其类型是唯一的、未命名的非联合体(non-union)、非聚合类(non-aggregate class type),称为闭包类型。闭包类型在最小的作用域中声明,该作用域可以是块作用域、类作用域或命名空间作用域,并且用于 ADL(Argument-Dependent Lookup)。

闭包类型的特性
  1. 结构化类型 (Structural Type) (自 C++20 起):

    • 如果捕获列表为空,则闭包类型是结构化类型。
  2. 成员函数 operator():

    • 闭包类型的主要成员是 operator(),它执行 lambda 表达式的主体。
    • operator() 的参数列表由 lambda 表达式的参数列表 params 决定;如果没有提供参数列表,则默认为空。
    • operator() 的返回类型由 trailing-type 指定;如果没有提供 trailing-type,则返回类型会自动推导。
  3. 模板成员函数 operator() (自 C++14 起):

    • 对于泛型 lambda,operator() 是一个模板函数,接受任意类型的参数。
    • 如果参数类型使用 auto 或提供了显式模板参数列表,则会为每个 auto 参数生成一个虚构的模板参数。
  4. 修饰符:

    • mutable: 如果使用了 mutable 关键字,operator() 不会被隐式添加 const 限定符,允许修改按值捕获的对象。
    • constexpr (自 C++17 起): 如果使用了 constexpr 关键字,operator() 是一个常量表达式函数。
    • consteval (自 C++20 起): 如果使用了 consteval 关键字,operator() 是一个立即函数。
    • static (自 C++23 起): 如果使用了 static 关键字,operator() 是一个静态成员函数。
    • explicit 对象参数 (自 C++23 起): 如果 params 包含显式对象参数,则 operator() 是一个显式对象成员函数。
  5. 异常说明:

    • lambda 表达式的异常说明 (exception) 应用于 operator()
  6. 上下文:

    • 在确定名称查找、this 指针的类型和值以及访问非静态类成员时,operator() 的主体被视为在 lambda 表达式定义的上下文中。
示例代码
#include <iostream>
#include <utility>// 泛型 lambda, operator() 是一个模板函数
auto glambda = [](auto a, auto&& b) { return a < b; };
bool b = glambda(3, 3.14); // OK// 带有显式模板参数列表的泛型 lambda
auto glambda_with_tparams = []<class T>(T a, auto&& b) { return a < b; };
bool c = glambda_with_tparams(3, 3.14); // OK// 嵌套的泛型 lambda
auto vglambda = [](auto printer) {return [=](auto&&... ts) { // 泛型 lambda, ts 是参数包printer(std::forward<decltype(ts)>(ts)...);// 无参 lambda (不接受任何参数)return [=]() { printer(ts...); };};
};auto p = vglambda([](auto v1, auto v2, auto v3) {std::cout << v1 << v2 << v3;
});auto q = p(1, 'a', 3.14); // 输出: 1a3.14
q();                      // 输出: 1a3.14// 使用 this 的 lambda
struct X {int x, y;int operator()(int n) { return n + x + y; }void f() {// lambda 的上下文是成员函数 X::f[=]() -> int {return operator()(this->x + y); // X::operator()(this->x + (*this).y)}();}
};int main() {X obj{1, 2};obj.f(); // 输出: 4 (1 + 2 + 1)std::cout << "Comparison result: " << b << std::endl; // 输出: 1 (true)std::cout << "Comparison result with tparams: " << c << std::endl; // 输出: 1 (true)return 0;
}
悬挂引用 (Dangling References)

如果一个非引用实体通过引用被捕获(无论是隐式还是显式),并且在该实体的生命周期结束之后调用了闭包对象的 operator(),则会发生未定义行为。C++ 闭包不会延长通过引用捕获的对象的生命周期。

同样的规则也适用于通过 this 捕获的当前对象。如果 this 捕获的是一个临时对象或局部对象的引用,并且在该对象的生命周期结束后调用了闭包对象的 operator(),也会发生未定义行为。

示例:悬挂引用的风险
#include <iostream>
#include <vector>
#include <memory>std::function<int()> create_lambda() {int local = 10;// 错误:local 是局部变量,lambda 捕获它的引用auto lambda = [&local]() { return local; };return lambda;
}int main() {auto func = create_lambda();std::cout << func() << std::endl; // 未定义行为return 0;
}

在这个例子中,local 是一个局部变量,lambda 通过引用捕获了它。当 create_lambda 函数返回后,local 的生命周期已经结束,因此调用 func() 会导致未定义行为。

为了避免这种情况,应该尽量避免捕获局部变量的引用,或者确保闭包对象在其捕获的实体生命周期内使用。例如,可以通过按值捕获来避免悬挂引用:

std::function<int()> create_lambda() {int local = 10;// 正确:local 按值捕获,lambda 拥有它的副本auto lambda = [local]() { return local; };return lambda;
}int main() {auto func = create_lambda();std::cout << func() << std::endl; // 输出: 10return 0;
}

总结

闭包类型是 C++ 中 lambda 表达式的底层实现机制,它是一个唯一的、未命名的类类型,具有特定的成员函数 operator()。了解闭包类型的特性和行为,特别是 operator() 的修饰符、模板参数、异常说明以及悬挂引用的风险,可以帮助你编写更加安全和高效的代码。泛型 lambda 和新引入的特性(如 constexprconstevalstatic)进一步增强了 lambda 表达式的功能和灵活性。

闭包类型的用户定义转换函数

在 C++ 中,闭包类型(即 lambda 表达式的类型)可以提供一个用户定义的转换函数,用于将闭包对象隐式转换为函数指针或函数对象。这个转换函数只有在 lambda 表达式的捕获列表为空且没有显式对象参数(自 C++23 起)的情况下才会定义。它是一个公共的、constexpr(自 C++17 起)、非虚、非显式、constnoexcept 的成员函数。

用户定义转换函数的特性
  1. 无捕获的非泛型 lambda:

    • C++11 至 C++17:
      using F = ret(*)(params);
      operator F() const noexcept;
      
    • C++17 及之后:
      using F = ret(*)(params);
      constexpr operator F() const noexcept;
      
  2. 无捕获的泛型 lambda (自 C++14 起):

    • C++14 至 C++17:
      template<template-params> using fptr_t = /* see below */;
      template<template-params>
      operator fptr_t<template-params>() const noexcept;
      
    • C++17 及之后:
      template<template-params> using fptr_t = /* see below */;
      template<template-params>
      constexpr operator fptr_t<template-params>() const noexcept;
      
  3. 立即函数 (自 C++20 起):

    • 如果 operator() 是立即函数(consteval),则转换函数也是一个立即函数。
  4. 泛型 lambda 的转换函数模板 (自 C++14 起):

    • 泛型 lambda 的转换函数模板具有与 operator() 相同的虚构模板参数列表。
  5. 返回值:

    • C++14 之前:
      • 返回值是指向具有 C++ 语言链接的函数的指针,调用该函数的效果等同于在默认构造的闭包类型实例上调用 operator()
    • C++14 至 C++23:
      • 对于非泛型 lambda,返回值是指向具有 C++ 语言链接的函数的指针,调用该函数的效果等同于在默认构造的闭包类型实例上调用 operator()
      • 对于泛型 lambda,返回值是指向具有 C++ 语言链接的函数的指针,调用该函数的效果等同于在默认构造的闭包类型实例上调用相应的 operator() specialization。
    • C++23 及之后:
      • 如果 operator() 是静态成员函数,则返回值是指向该 operator() 的指针,具有 C++ 语言链接。
      • 否则,返回值是指向具有 C++ 语言链接的函数的指针,调用该函数的效果等同于:
        • 对于非泛型 lambda,在默认构造的闭包类型实例上调用 operator()
        • 对于泛型 lambda,在默认构造的闭包类型实例上调用相应的 operator() specialization。
  6. constexprnoexcept:

    • 如果 operator()constexpr,则转换函数也是 constexpr
    • 如果 operator() 具有非抛出异常说明,则返回的指针类型是指向 noexcept 函数的指针。
示例代码
#include <iostream>
#include <type_traits>// 无捕获的非泛型 lambda
void f1(int (*)(int)) {}
void f2(char (*)(int)) {}auto non_generic_lambda = [](int x) { return x; };
f1(non_generic_lambda); // OK
f2(non_generic_lambda); // error: not convertible// 无捕获的泛型 lambda
void h(int (*)(int)) {}  // #1
void h(char (*)(int)) {} // #2auto generic_lambda = [](auto a) { return a; };
h(generic_lambda);  // OK: calls #1 since #2 is not convertible// 无捕获的泛型 lambda,返回引用
int value = 42;
int& (*fpi)(int*) = [](auto* a) -> auto& { return *a; }; // OK
std::cout << fpi(&value) << std::endl; // 输出: 42// 检查转换函数是否为 constexpr
auto Fwd = [](int(*fp)(int), auto a) { return fp(a); };
auto C = [](auto a) { return a; };
static_assert(Fwd(C, 3) == 3);  // OKauto NC = [](auto a) { static int s; return a; };
// static_assert(Fwd(NC, 3) == 3); // error: no specialization can be constexpr because of static s// 捕获列表不为空的情况
auto with_capture = [value]() { return value; };
// f1(with_capture); // error: cannot convert to function pointer because it has capturesint main() {return 0;
}

闭包类型的构造和赋值

  1. 默认构造函数:

    • C++20 及之后:
      • 如果捕获列表为空,闭包类型具有默认的默认构造函数。
      • 否则,闭包类型没有默认构造函数(即使使用了捕获默认值 [=][&] 但实际未捕获任何内容)。
    • C++20 之前:
      • 闭包类型没有默认构造函数。
  2. 复制构造函数和移动构造函数:

    • C++20 及之后:
      • 复制构造函数和移动构造函数被声明为默认的,并根据通常的规则隐式定义。
    • C++20 之前:
      • 复制构造函数和移动构造函数是默认的,但不会隐式定义。
  3. 复制赋值运算符:

    • C++20 及之后:
      • 如果捕获列表为空,闭包类型具有默认的复制赋值运算符和移动赋值运算符。
      • 否则,复制赋值运算符被删除(即使使用了捕获默认值 [=][&] 但实际未捕获任何内容)。
    • C++20 之前:
      • 复制赋值运算符被删除,移动赋值运算符未声明。
  4. 析构函数:

    • 析构函数是隐式声明的,默认为默认析构函数。
示例代码
#include <iostream>
#include <utility>struct NonCopyable {NonCopyable() = default;NonCopyable(const NonCopyable&) = delete;NonCopyable(NonCopyable&&) = default;
};// 无捕获的 lambda
auto no_capture = []() { return 42; };
no_capture(); // OK// 捕获局部变量的 lambda
int local = 42;
auto with_capture = [local]() { return local; };
with_capture(); // OK// 捕获可复制对象的 lambda
int copyable = 42;
auto capture_copyable = [copyable]() { return copyable; };
capture_copyable(); // OK// 捕获不可复制对象的 lambda
NonCopyable non_copyable;
auto capture_non_copyable = [&non_copyable]() { return non_copyable; };
capture_non_copyable(); // OK// 尝试复制带有捕获的 lambda
auto copy_with_capture = with_capture; // OK
auto copy_capture_non_copyable = capture_non_copyable; // OK// 尝试赋值带有捕获的 lambda
with_capture = no_capture; // error: no assignment operator for lambdas with capturesint main() {std::cout << "No capture: " << no_capture() << std::endl;std::cout << "With capture: " << with_capture() << std::endl;std::cout << "Capture copyable: " << capture_copyable() << std::endl;std::cout << "Capture non-copyable: " << &capture_non_copyable() << std::endl;return 0;
}

闭包类型的成员数据

  1. 按值捕获:

    • 如果 lambda 表达式通过值捕获实体(使用捕获子句 [=] 或显式捕获不包含字符 &,例如 [a, b, c]),闭包类型会包含未命名的非静态数据成员,这些成员持有所有按值捕获的实体的副本。
    • 这些数据成员按照它们声明的顺序初始化(顺序未指定),直接初始化或根据初始化器的要求进行初始化(可能是复制初始化或直接初始化)。
    • 如果捕获的是数组,数组元素按递增索引顺序直接初始化。
  2. 按引用捕获:

    • 如果 lambda 表达式通过引用捕获实体(使用捕获默认值 [&] 或显式捕获包含字符 &,例如 [&a, &b, &c]),闭包类型可能会声明额外的数据成员,但这些成员必须满足 LiteralType(自 C++17 起)。
    • 捕获的引用类型实体会被捕获为 lvalue 引用(对于函数引用)或对象的副本(对于对象引用)。
示例代码
#include <iostream>struct S {int x;S(int x) : x(x) {}
};int global = 42;void test_captures() {int local = 10;S s(20);// 按值捕获auto by_value = [local, s]() {std::cout << "Local: " << local << ", S.x: " << s.x << std::endl;};// 按引用捕获auto by_reference = [&local, &s]() {std::cout << "Local: " << local << ", S.x: " << s.x << std::endl;};// 修改局部变量和对象local = 15;s.x = 25;// 调用按值捕获的 lambdaby_value(); // 输出: Local: 10, S.x: 20// 调用按引用捕获的 lambdaby_reference(); // 输出: Local: 15, S.x: 25
}int main() {test_captures();return 0;
}

Lambda 表达式的限制

Lambda 表达式不能出现在以下位置:

  • 未求值表达式(如 sizeofdecltype 等)
  • 模板参数
  • 别名声明(如 using 声明)
  • typedef 声明
  • 函数声明中的任何地方,除了函数体和函数的默认参数
示例代码
#include <iostream>// 错误:lambda 表达式不能出现在 sizeof 中
// size_t size = sizeof([]() {});// 正确:lambda 表达式可以在函数体内
void func() {auto lambda = []() { std::cout << "Hello, World!" << std::endl; };lambda();
}int main() {func();return 0;
}

总结

闭包类型的用户定义转换函数允许无捕获的 lambda 表达式隐式转换为函数指针或函数对象,从而提供了更大的灵活性。了解这些转换函数的特性和行为,特别是它们的 constexprnoexcept 属性,可以帮助你编写更加安全和高效的代码。此外,闭包类型的构造和赋值规则也非常重要,特别是在处理带有捕获的 lambda 表达式时。最后,注意 lambda 表达式的使用限制,以避免编译错误。

Lambda 捕获机制的示例

下面通过具体的代码示例来详细说明 C++ Lambda 表达式的捕获机制。

1. 捕获默认
  • 按引用捕获所有变量 (&):

    #include <iostream>void capture_by_reference() {int x = 42;auto lambda = [&]() { std::cout << "x = " << x << std::endl; };x = 100;lambda(); // 输出: x = 100
    }int main() {capture_by_reference();return 0;
    }
    
  • 按值捕获所有变量 (=):

    #include <iostream>void capture_by_value() {int x = 42;auto lambda = [=]() { std::cout << "x = " << x << std::endl; };x = 100;lambda(); // 输出: x = 42
    }int main() {capture_by_value();return 0;
    }
    
  • 捕获 this:

    #include <iostream>class MyClass {
    public:int value = 42;void printValue() {auto lambda = [this]() { std::cout << "value = " << this->value << std::endl; };lambda(); // 输出: value = 42}
    };int main() {MyClass obj;obj.printValue();return 0;
    }
    
2. 捕获列表
  • 捕获单个变量:

    #include <iostream>void capture_single_variable() {int x = 42;int y = 100;auto lambda = [x, &y]() {std::cout << "x = " << x << ", y = " << y << std::endl;y = 200; // 修改 y 的值};lambda(); // 输出: x = 42, y = 100std::cout << "After lambda: y = " << y << std::endl; // 输出: After lambda: y = 200
    }int main() {capture_single_variable();return 0;
    }
    
  • 带有初始化器的捕获:

    #include <iostream>
    #include <string>void initializer_capture() {int x = 42;auto lambda = [n = x + 1, s = std::string("Hello")]() {std::cout << "n = " << n << ", s = " << s << std::endl;};lambda(); // 输出: n = 43, s = Hello
    }int main() {initializer_capture();return 0;
    }
    
3. 捕获规则
  • 使用捕获默认 & 后不能以 & 开始简单捕获:

    #include <iostream>void invalid_capture_with_ampersand() {int x = 42;// 错误:不能在捕获默认 & 之后再使用 & 捕获// auto lambda = [&](int& y) { std::cout << "x = " << x << ", y = " << y << std::endl; };
    }int main() {// invalid_capture_with_ampersand();return 0;
    }
    
  • 使用捕获默认 = 后必须以 & 开始简单捕获:

    #include <iostream>void valid_capture_with_equal() {int x = 42;int y = 100;auto lambda = [=, &y]() {std::cout << "x = " << x << ", y = " << y << std::endl;y = 200; // 修改 y 的值};lambda(); // 输出: x = 42, y = 100std::cout << "After lambda: y = " << y << std::endl; // 输出: After lambda: y = 200
    }int main() {valid_capture_with_equal();return 0;
    }
    
4. 重复捕获和参数名冲突
  • 重复捕获:

    #include <iostream>void duplicate_capture() {int x = 42;// 错误:不能重复捕获同一个变量// auto lambda = [x, x]() { std::cout << "x = " << x << std::endl; };
    }int main() {// duplicate_capture();return 0;
    }
    
  • 参数名冲突:

    #include <iostream>void parameter_name_conflict() {int x = 42;// 错误:捕获的变量名不能与参数名相同// auto lambda = [x](int x) { std::cout << "x = " << x << std::endl; };
    }int main() {// parameter_name_conflict();return 0;
    }
    
5. 捕获范围
  • 捕获的变量必须在可达范围内:
    #include <iostream>void capture_out_of_scope() {// 错误:z 不在 Lambda 表达式的可达范围内// int z = 42;// auto lambda = [&]() { std::cout << "z = " << z << std::endl; };
    }int main() {// capture_out_of_scope();return 0;
    }
    
6. 初始化器捕获
  • 带有初始化器的捕获:
    #include <iostream>
    #include <string>void initializer_capture_example() {int x = 42;auto lambda = [n = x + 1, s = std::string("Hello")]() {std::cout << "n = " << n << ", s = " << s << std::endl;};lambda(); // 输出: n = 43, s = Hello
    }int main() {initializer_capture_example();return 0;
    }
    
7. 隐式捕获
  • 隐式捕获:
    #include <iostream>void implicit_capture() {int x = 42;auto lambda = []() { std::cout << "x = " << x << std::endl; };lambda(); // 隐式捕获 x
    }int main() {implicit_capture();return 0;
    }
    
8. ODR-使用
  • 按值捕获:

    #include <iostream>void odr_use_by_value() {int x = 42;auto lambda = [=]() mutable { x = 100; std::cout << "x = " << x << std::endl; };lambda(); // 修改闭包对象中的 x
    }int main() {odr_use_by_value();return 0;
    }
    
  • 按引用捕获:

    #include <iostream>void odr_use_by_reference() {int x = 42;auto lambda = [&]() { x = 100; std::cout << "x = " << x << std::endl; };lambda(); // 修改原始引用所指向的对象
    }int main() {odr_use_by_reference();return 0;
    }
    
9. 捕获成员和 this
  • 捕获 this:

    #include <iostream>class MyClass {
    public:int value = 42;void printValue() {auto lambda = [this]() { std::cout << "value = " << this->value << std::endl; };lambda(); // 输出: value = 42}
    };int main() {MyClass obj;obj.printValue();return 0;
    }
    
  • 捕获 *this (自 C++17 起):

    #include <iostream>class MyClass {
    public:int value = 42;void printValue() {auto lambda = [*this]() { std::cout << "value = " << value << std::endl; };lambda(); // 输出: value = 42}
    };int main() {MyClass obj;obj.printValue();return 0;
    }
    
10. 捕获和默认参数
  • 默认参数中捕获:

    #include <iostream>void default_param_lambda() {int x = 42;// 错误:Lambda 表达式出现在默认参数中时不能显式或隐式捕获任何东西// void func(int y = [](int a) { return a + x; }(10)) {}
    }int main() {// default_param_lambda();return 0;
    }
    
  • 带有初始化器的捕获:

    #include <iostream>void valid_default_param_lambda() {int x = 42;void func(int y = [n = x + 1]() { return n; }()) {std::cout << "y = " << y << std::endl;}func(); // 输出: y = 43
    }int main() {valid_default_param_lambda();return 0;
    }
    
11. 嵌套 Lambda 捕获
  • 嵌套 Lambda 捕获:
    #include <iostream>void nested_lambda_capture() {int x = 42;auto outer_lambda = [x]() {int y = 100;auto inner_lambda = [x, &y]() {std::cout << "x = " << x << ", y = " << y << std::endl;};inner_lambda(); // 输出: x = 42, y = 100};outer_lambda();
    }int main() {nested_lambda_capture();return 0;
    }
    
12. 捕获类型限制
  • 显式对象参数类型:
    #include <iostream>struct ClosureType {int value = 42;void operator()(int x) const { std::cout << "value = " << value << ", x = " << x << std::endl; }
    };void explicit_object_parameter_type() {ClosureType closure;auto lambda = [](const ClosureType& c, int x) { c(x); };lambda(closure, 100); // 输出: value = 42, x = 100
    }int main() {explicit_object_parameter_type();return 0;
    }
    

总结

通过这些示例,我们可以看到 C++ Lambda 表达式的捕获机制非常灵活且强大。捕获默认、捕获列表、初始化器捕获、隐式捕获、ODR-使用、嵌套 Lambda 捕获等特性使得 Lambda 表达式能够方便地与外部作用域交互,并且提供了丰富的语义和行为控制。理解这些规则和特性,可以帮助你编写更加安全、高效和易于维护的代码。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com