CppCon 2014 学习: An Overview of C++11/14

这是 C++11/14 简化核心语言特性 的列表(Part I):

C++11/14 的核心语言简化特性:

特性 说明
auto 自动类型推导(从右值推导变量类型)
decltype 从表达式中推导类型(可用于返回类型、模板等)
trailing return type 延后返回类型(如:auto func() -> int
nullptr 替代 NULL 的类型安全空指针
range-based for 简化遍历容器:for (auto x : container)
>> in templates 以前 >> 会被当成右移运算符,现在能正确解析嵌套模板:std::vector<std::vector<int>>
static_assert 编译期断言检查,例如:static_assert(sizeof(int) == 4, "unexpected size");
extern template 显式控制模板实例化,减少编译时间
noexcept 指示函数不会抛出异常,有助于优化
variadic templates 支持不定参数模板,如元编程或日志系统
constexpr 编译期常量计算,可用于函数、变量等
template alias (using) 模板类型别名的简化形式,如:using Vec = std::vector<int>;

这是一个用来查找 容器中第一个 null 指针 的模板函数,展示了 C++03 中啰嗦的类型声明问题(wordy declarations)。我们一起来看:

C++03 风格的写法分析:

cpp 复制代码
template<typename Cont>
typename Cont::const_iterator findNull(const Cont &c)
{
    typename Cont::const_iterator it;
    for (it = c.begin(); it != c.end(); ++it)
        if (*it == 0)
            break;
    return it;
}
功能:
  • 接收任意指针类型的容器(如 std::vector<int*>
  • 返回第一个 nullptr 元素的迭代器
  • 如果没有 nullptr,返回 end() 迭代器

问题:C++03 中类型声明冗长

  • typename Cont::const_iterator 写了 3 次
  • 阅读不友好,影响可读性和维护性

C++11 简化版本:使用 auto

cpp 复制代码
template<typename Cont>
auto findNull(const Cont &c) -> typename Cont::const_iterator
{
    for (auto it = c.begin(); it != c.end(); ++it)
        if (*it == nullptr)
            return it;
    return c.end();
}
优点:
  • 使用 auto 减少重复代码
  • 使用 nullptr 替代裸 0,更类型安全
  • 读起来更直观清晰

这个 main() 函数展示了 如何在旧版 C++(如 C++03)中使用前面定义的 findNull 函数。我们逐步分析:

代码说明:

cpp 复制代码
int main() 
{ 
    int a = 1000, b = 2000, c = 3000; 
    vector<int *> vpi;          // 一个 int* 类型的 vector 容器
    vpi.push_back(&a);          // 插入指向 a 的指针
    vpi.push_back(&b); 
    vpi.push_back(&c); 
    vpi.push_back(0);           // 插入一个 null 指针
    vector<int *>::const_iterator cit = findNull(vpi); // 使用 findNull 查找 null 指针
    if (cit == vpi.end()) 
        cout << "no null pointers in vpi" << endl;
    else 
    { 
        vector<int *>::difference_type pos = cit - vpi.begin(); // 计算 null 指针位置
        cout << "null pointer found at pos. " << pos << endl; 
    }
}

关键点理解:

特性 说明
vector<int *> 一个存储指针的容器
findNull(vpi) 调用模板函数查找第一个 nullptr(当时写的是 0
cit == vpi.end() 检查是否没有找到 null 指针
cit - vpi.begin() 计算 null 指针在容器中的下标位置
0 在 C++03 中经常用作 null 指针(C++11 中应改用 nullptr

改进建议(使用 C++11/14):

cpp 复制代码
#include <iostream>
#include <vector>
#include <memory>   // optional, for unique_ptr later
using namespace std;
int main() {
    int a = 1000, b = 2000, c = 3000;
    vector<int*> vpi{ &a, &b, &c, nullptr };
    auto cit = findNull(vpi);  // 更简单的 auto
    if (cit == vpi.end())
        cout << "no null pointers in vpi" << endl;
    else
        cout << "null pointer found at pos. " << (cit - vpi.begin()) << endl;
}

当我们编写一个通用函数模板时(比如 product),其返回类型取决于模板参数 TU,但我们无法事先明确知道返回类型是什么

举例说明:

cpp 复制代码
template<typename T, typename U>
??? product(const T &t, const U &u)
{
    return t * u;
}

你无法提前写出 ???,因为:

  • TU 可能是不同类型;
  • t * u 的结果类型不是固定的,比如:
    • int * double -> double
    • float * long -> float
    • 自定义类型可能有重载 operator*

解决方案(现代 C++):

1. 使用 decltype(C++11)推导返回类型:
cpp 复制代码
template<typename T, typename U>
auto product(const T& t, const U& u) -> decltype(t * u)
{
    return t * u;
}
  • auto ... -> decltype(...)尾置返回类型(trailing return type)语法。
  • 编译器会根据 t * u 推导出返回类型。
2. 使用 auto + decltype(C++14 起):
cpp 复制代码
template<typename T, typename U>
auto product(const T& t, const U& u) {
    return t * u;
}
  • 更简洁,自动推导返回类型(前提是编译器支持 C++14)。

示例:

cpp 复制代码
int main() {
    int a = 3;
    double b = 4.5;
    auto result = product(a, b);  // 返回 double 类型
    std::cout << result << std::endl;  // 输出 13.5
}

这是 C++11 引入的一个关键组合:auto + decltype + 尾置返回类型(Trailing Return Type)

为什么需要这样写?

当你写一个模板函数,而函数的返回类型是两个不同类型变量计算(比如乘法)的结果:

cpp 复制代码
template<typename T, typename U>
auto product(const T &t, const U &u) -> decltype(t * u)
{
    return t * u;
}

解读

  • auto:告诉编译器"我要返回某种类型,但我会在后面说明具体是哪种"。
  • -> decltype(t * u)尾置返回类型 ,通过 decltype 表达式,让编译器根据 t * u 的类型来确定返回类型。
  • decltype(t * u):推导出 t * u 的精确类型(可能是 int, double, MyType 等)。

为什么不能直接用 auto(在 C++11)?

C++11 中,以下写法是不允许的:

cpp 复制代码
//  错误(在 C++11 中,auto 不能用于返回类型推导)
template<typename T, typename U>
auto product(const T &t, const U &u) {
    return t * u;
}

这种写法直到 C++14 才允许。

举个例子

cpp 复制代码
template<typename T, typename U>
auto product(const T& t, const U& u) -> decltype(t * u) {
    return t * u;
}
int main() {
    int a = 4;
    double b = 2.5;
    auto result = product(a, b); // result 是 double 类型
    std::cout << result << std::endl; // 输出 10
}

这是用 C++11 语法重写 findNull 函数模板,利用了 auto尾置返回类型 来简化返回值声明。

细节解析:

cpp 复制代码
template<typename Cont>
auto findNull(const Cont &c) -> decltype(c.begin())
{
    auto it = c.begin();
    for (; it != c.end(); ++it)
        if (*it == 0)
            break;
    return it;
}
  • auto 用于 it 变量,自动推断迭代器类型,省去写具体类型的麻烦。
  • 返回类型用尾置返回类型写法,返回的是 decltype(c.begin()) ------ 即容器的迭代器类型。
  • 遍历容器,寻找第一个指向 nullptr (即0) 的指针,找不到则返回 c.end()

这写法相比旧写法的优势:

  • 返回值类型不用写成 typename Cont::const_iterator,直接用 decltype 自动推断。
  • 函数体内用 auto 简洁声明迭代器。
  • 代码更简洁、易读,减少了模板中类型声明的繁琐。

C++11 引入的非成员函数 begin()end(),它们比容器的成员函数版本更通用,支持普通数组也能用。

重点解析:

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring> // for strlen
bool strLenGT4(const char *s) { return strlen(s) > 4; }
int main()
{
    // STL 容器 vector
    std::vector<int> v {-5, -19, 3, 10, 15, 20, 100};
    auto first3 = std::find(std::begin(v), std::end(v), 3);
    if (first3 != std::end(v))
        std::cout << "First 3 in v = " << *first3 << std::endl;
    // C 风格数组
    const char *names[] {"Huey", "Dewey", "Louie"};
    auto firstGT4 = std::find_if(std::begin(names), std::end(names), strLenGT4);
    if (firstGT4 != std::end(names))
        std::cout << "First long name: " << *firstGT4 << std::endl;
}

解释:

  • std::begin()std::end() 是非成员函数,它们会调用容器的成员函数 begin()end(),但也重载支持普通数组(比如 names),返回数组的起始和终止指针。
  • 这样就可以用标准算法 std::findstd::find_if 一视同仁地处理容器和原生数组。
  • 比起直接用容器成员函数或数组指针,这种写法更通用,代码更简洁且灵活。

你说的是 C++14 对 begin/end 系列函数的扩展,增加了以下几个版本:

  • cbegin()cend():返回容器的 const_iterator,保证不能修改元素。
  • rbegin()rend():返回容器的 reverse_iterator,反向遍历容器。
  • crbegin()crend():返回容器的 const_reverse_iterator,反向且只读遍历。

示例:

cpp 复制代码
#include <iostream>
#include <vector>
template <typename Container>
void process_container(Container &c)
{
    // C++11 风格:获取 const_iterator
    typename Container::const_iterator ci = begin(c);
    // C++14 风格:用 auto 和 cbegin(),更简洁
    auto ci2 = c.cbegin();
    // 反向迭代
    for (auto rit = rbegin(c); rit != rend(c); ++rit)
        std::cout << *rit << " ";
    std::cout << std::endl;
}

解释:

  • cbegin()cend() 是容器的 const 版本,适合只读访问。
  • rbegin()rend() 允许从尾到头遍历容器。
  • crbegin()crend() 结合了 const 和 reverse。
  • 使用这些函数可以让代码更具表达力,减少手动写迭代器类型的麻烦。

这个问题在旧的 C++ 中很常见,NULL 通常定义为 0 或者 (void*)0,导致调用重载函数时出现歧义,特别是在指针和整数参数重载的情况下。

例子分析:

cpp 复制代码
void f(long) { std::cout << "f(long)\n"; }
void f(char *) { std::cout << "f(char *)\n"; }
int main()
{
    f(0L);                // 调用 f(long),明确传递 long 类型 0L
    f(0);                 // 调用 f(long),但对编译器来说 0 可以是 int 也可以是指针的 NULL,导致不确定
    f(static_cast<char*>(0));  // 明确地告诉编译器传递的是 char* 指针,避免二义性
}

旧 C++ 的问题点:

  • 0 是整数常量,不是专门的"空指针",传给指针参数会隐式转换。
  • NULL 通常是 0 的宏定义,造成函数重载时二义性。
  • 需要用 static_cast<char*>(0) 来明确告诉编译器是指针。

C++11 的解决方案:nullptr

C++11 引入了 nullptr ,它是一个专门的空指针字面值,类型为 std::nullptr_t,不会被误认为整数,解决了这个重载歧义问题。

cpp 复制代码
void f(long) { std::cout << "f(long)\n"; }
void f(char *) { std::cout << "f(char *)\n"; }
int main()
{
    f(0L);           // 调用 f(long)
    // f(0);          // 旧代码,会调用 f(long)
    f(nullptr);      // 调用 f(char*), 无歧义
}

总结:

  • 旧 C++ 用 0NULL 表示空指针,容易造成重载二义性。
  • C++11 推荐用 nullptr,更加安全和清晰。

你给的例子准确展示了 nullptr 的用法和它解决的问题:

cpp 复制代码
void f(long) { std::cout << "f(long)\n"; }
void f(char *) { std::cout << "f(char *)\n"; }
int main()
{
    f(0L);       // 调用 f(long),传入 long 类型的 0L,没问题
    f(nullptr);  // 调用 f(char*),因为 nullptr 是专门的空指针类型
    f(0);        // 依然调用 f(long),因为 0 是 int,会被转换成 long,重载不会调用 char*
}

重点总结:

  • 0L:明确是 long 类型,调用 f(long)
  • nullptr:明确是空指针类型,调用 f(char*)
  • 0:整数 0,匹配 longchar* 更合适(指针重载被忽略),仍然调用 f(long)
    所以用 nullptr 能让指针重载更安全,但 0 仍然是整数,有时不容易发现歧义。
    如果想完全避免歧义,推荐在指针参数时总用 nullptr,而不是 0

这是用 C++11 写的 findNull 函数最终版本,改进了以下几点:

  • 返回类型用 decltype(begin(c)),支持所有支持 begin() 的容器,包括原生数组。
  • 使用了非成员函数 begin(c)end(c),更通用。
  • 判断空指针时用 nullptr 替代了旧式的 0,更加明确和安全。
    完整代码示意:
cpp 复制代码
template<typename Cont>
auto findNull(const Cont &c) -> decltype(begin(c))
{
    auto it = begin(c);
    for (; it != end(c); ++it)
        if (*it == nullptr)
            break;
    return it;
}

这样写可以同时支持标准容器和原生数组,且返回的迭代器类型正确无误,推荐用法。

C++14 版本的 findNull 进一步简化了代码,最大亮点是函数返回类型自动推导 ,不再需要像 C++11 那样写复杂的 -> decltype(...)

示例:

cpp 复制代码
template<typename Cont>
auto findNull(const Cont &c)
{
    auto it = begin(c);
    for (; it != end(c); ++it)
        if (*it == nullptr)
            break;
    return it;  // 返回类型由编译器自动推导
}

这样写更简洁,维护起来也更方便。

这部分讲的是 C++14 中函数返回类型推导的两种关键方式:

1. auto 返回类型

  • 采用模板类型推导规则(类似模板参数推导)
  • 剥除 返回表达式的引用(&)和修饰符(const, volatile
  • 也就是说,如果返回的是引用或带 const,用 auto 会变成普通值类型(拷贝)
    举例:
cpp 复制代码
int x = 10;
auto f() {
    return (x);  // x 是 int,但这里返回的是 int(剥除引用)
}

2. decltype(auto) 返回类型

  • 采用 decltype 的推导规则
  • 不会剥除引用或修饰符,返回表达式的"原始类型"
  • 可以准确返回引用、const
    举例:
cpp 复制代码
int x = 10;
decltype(auto) f() {
    return (x);  // 返回 int&,保持引用类型
}

总结:

  • auto 更常用,返回值拷贝或移动
  • decltype(auto) 用于你想完美转发返回类型(尤其是引用和修饰符),防止无意中拷贝

这段代码展示了 在旧版 C++(比如 C++03)中遍历数组和容器的典型写法,它的特点和问题包括:

1. 遍历原生数组

cpp 复制代码
int ai[] = { 10, 20, 100, 200, -500, 999, 333 };
const int size = sizeof ai / sizeof *ai;  // 计算数组大小很麻烦
for (int i = 0; i < size; ++i)
    cout << ai[i] << " ";
cout << endl;
  • 计算数组大小要用 sizeof 除法,容易出错且冗长。
  • 使用索引遍历,写起来不够直观,且易出现越界错误。

2. 遍历 STL 容器(这里是 list<int>

cpp 复制代码
list<int> li(ai, ai + size);  // 通过迭代器区间构造 list
// 遍历修改元素
for (list<int>::iterator it = li.begin(); it != li.end(); ++it)
    *it += 100000;
// 遍历只读访问元素
for (list<int>::const_iterator it = li.begin(); it != li.end(); ++it)
    cout << *it << " ";
  • 迭代器声明冗长,需要写 list<int>::iteratorlist<int>::const_iterator
  • 使用迭代器遍历,代码比较啰嗦。
  • 很容易写成 it != li.end() 出错,比如写成 it < li.end(),特别对新手不友好。

总结问题

  • 计算数组长度麻烦,易错。
  • 迭代器声明长且复杂。
  • 代码冗长,易出现越界或写错条件的错误。

这个地方关注一点就是数组名什么时候不会退化成指针

数组名不会退化成指针的几种典型情况是:

  1. 用在 sizeof 操作符中
    例:sizeof(arr) 计算整个数组大小,不是指针大小。
  2. 用在取地址操作符 &
    例:&arr 取的是整个数组的地址,类型是指向数组的指针。
  3. 用在 decltype 操作符中
    例:decltype(arr) 得到的是数组类型。
  4. 用作初始化列表时
    例:int arr[3] = {1, 2, 3}; 初始化数组时,数组名不退化。
  5. 用作模板参数推导时(如 std::extent
    例:std::extent<decltype(arr)>::value 获取数组长度。
    sizeof & decltype 这些用于数组名时不会退化成指针

这段代码展示了 C++11范围基于循环(range-based for loop) 的优势:

cpp 复制代码
int main() {
    int ai[]{10, 20, 100, 200, -500, 999, 333};
    for (auto i : ai)  // Don't need size
        cout << i << " ";
    cout << endl;
    list<int> li(begin(ai), end(ai));
    for (auto &elt : li)  // Using a ref allows modifying
        elt += 10000;
    for (const auto &elt : li)  // Note: const & not nec-
        cout << elt << " ";     // essarily better for ints
    cout << endl;
    for (auto i : {100, 200, 300, 400}) cout << i << " ";
}
  • 不用手动计算数组大小,不易出错。
  • auto 自动推导类型,简化代码。
  • 使用引用 (auto &) 可以修改容器元素。
  • 用常量引用 (const auto &) 避免拷贝,保护数据,虽然对 int 这种小类型没必要。
  • 还能直接遍历初始化列表 { 100, 200, 300, 400 }

这里说的是 C++ 里模板嵌套时的">>问题":

  • 以前(C++03及之前),写嵌套模板时,两个 > 连着写会被当成右移运算符,必须写成 > >,即中间加空格,例如:

    cpp 复制代码
    std::map<std::string, std::vector<std::string> > dictionary;
  • C++11 改进了语法,允许直接写成 >>,不用空格,更简洁:

    cpp 复制代码
    std::map<std::string, std::vector<std::string>> dictionary;

这样减少了写模板时容易犯的错误(gotcha)。

这里讲的是 C++11 引入的 static_assert,它是 编译时断言 ,和 C 语言的运行时 assert 不同:

  • assert 是宏,检查运行时条件,如果失败就中断程序。
  • static_assert 是语言级别的机制,用于编译期检查 条件,如果条件不满足,编译就会报错,并显示你写的消息。
    格式是:
cpp 复制代码
static_assert(条件, "失败时显示的错误消息");
  • 这个条件必须是编译期可确定的常量表达式
  • 适合用来验证模板参数、类型特性等在编译期就能知道的条件,提前捕获错误。
    比如:
cpp 复制代码
static_assert(sizeof(int) == 4, "int must be 4 bytes");

如果 int 不是 4 字节,编译就会失败,输出对应信息。

这段代码利用了 static_assert 来确保类型转换的安全性:

cpp 复制代码
static_assert(sizeof(int) >= 4, "This app requires ints to be at least 32 bits.");

这行保证了 int 至少是32位(4字节),否则编译会失败。

cpp 复制代码
template<typename R, typename E>
R safe_cast(const E &e)
{
    static_assert(sizeof(R) >= sizeof(E), "Possibly unsafe cast attempt.");
    return static_cast<R>(e);
}

safe_cast 模板函数在编译期通过 static_assert 检查目标类型 R 是否至少和源类型 E 大小相同。

  • 如果不满足,编译时会报错,阻止不安全的类型转换。
    main 中:
cpp 复制代码
long lval = 50;
int ival = safe_cast<int>(lval); // 只有当 long 和 int 大小相等才通过
char cval = safe_cast<char>(lval); // 如果 char 比 long 小,编译会报错

char 通常比 long 小,所以会触发静态断言,编译失败。

这防止了隐式的、潜在不安全的类型缩小转换。

总结:

  • static_assert 可以在编译时帮你捕获类型大小、范围等不符合预期的错误。
  • 这样能让程序更安全、错误更早发现,尤其是在模板编程中非常有用。

问题核心是:

  • 模板代码通常全部写在头文件中("模板包含模型")。
  • 每个使用模板的翻译单元(.cpp文件)都会包含这个头文件,因此都会实例化(生成)模板代码。
  • 这会导致每个翻译单元都有一份相同的模板实例,造成目标文件(object file)代码膨胀(code bloat)。
  • 但链接器会在链接阶段去除多余的重复实例,只保留一份,避免最终可执行文件变得非常大。

C++11 提供了 extern template 来解决模板代码膨胀的问题:

  • 在多个翻译单元中 声明 模板实例为 extern template,告诉编译器"不要在这个翻译单元实例化模板函数",只做语法检查和类定义。

    cpp 复制代码
    extern template class std::vector<Widget>;
  • 在一个翻译单元中 显式实例化 该模板,这里才真正生成对应的模板函数代码:

    cpp 复制代码
    template class std::vector<Widget>;

这样就避免了每个翻译单元重复实例化模板带来的代码膨胀,减少了编译时间和目标文件大小,同时保证链接时有完整的实例代码。

问题是旧C++的动态异常规范(dynamic exception specifications):

  • Java中的异常规范会被强制执行,调用者必须处理声明的异常。
  • 旧C++允许函数用 throw()throw(Type1, Type2, ...) 声明它可能抛出的异常,但调用者不一定必须遵守这些声明。
  • 由于函数模板无法预知可能抛出的异常,动态异常规范难以有效使用。
  • 因此,旧C++标准库中,动态异常规范仅使用了空的 throw(),表示函数不会抛出任何异常,比如:
cpp 复制代码
template<typename T>
class MyContainer {
public:
    // 表明swap不会抛出异常
    void swap(MyContainer &) throw();
};

C++11 用 noexcept 替代了旧的动态异常规范:

  • 旧的动态异常规范(如 throw())不仅难用,还会影响性能。
  • C++11 废弃了动态异常规范,引入了 noexcept 关键字,明确声明函数不会抛出异常。
  • 用法示例:
cpp 复制代码
template<typename T>
class MyContainer {
public:
    void swap(MyContainer &) noexcept;
};
  • 如果一个被声明为 noexcept 的函数抛出异常,程序将立即终止(调用 std::terminate),避免了异常传播带来的额外开销和复杂性。
    总结:noexcept 是更轻量且高效的异常规范方式,推荐在不抛异常的函数上使用。

C++11 的 noexcept 还可以带条件表达式,称为"条件 noexcept"。

它允许根据内部操作是否会抛异常,动态决定外部函数的 noexcept 状态。

示例解析:

cpp 复制代码
void swap(pair& __p)
  noexcept(noexcept(swap(first, __p.first)) && noexcept(swap(second, __p.second)))
{
    using std::swap;
    swap(first, __p.first);
    swap(second, __p.second);
}
  • noexcept(...) 中的条件表达式:
    • noexcept(swap(first, __p.first)) 判断交换 first 成员是否会抛异常。
    • noexcept(swap(second, __p.second)) 判断交换 second 成员是否会抛异常。
  • 只有当两个成员的交换操作都不会抛异常,swap(pair&) 才被标记为 noexcept
    这样写的好处是:
  • 更准确地反映函数的异常保证。
  • 提升性能,因为调用者能根据 noexcept 预测异常行为,做更优化的处理。
    总结:条件 noexcept 结合了 noexcept 运算符,允许编写根据具体实现细节自动调整异常规范的代码。

这是关于"写一个函数来计算任意数量(N个)参数的平均值"的问题。

传统 C 方法:变长参数函数(variadic functions)

c 复制代码
int averInt(int count, ...);
double averDouble(int count, ...);

缺点:

  • 必须为每种类型写不同版本(类型不安全)。
  • 必须手动传入参数个数。
  • C++ 的默认参数不能解决这个问题,因为实际参数数量不确定。
  • 使用变长参数会带来类型安全和易用性的问题。

C++中的解决方案?

  • 过度重载(写很多不同参数数目的函数)会很乱。
  • 模板参数包(variadic templates)是后续更优雅的方案(C++11引入)。
    这就是为什么"写一个能接受任意数量和类型参数的平均函数"在旧C++里比较麻烦。

这是用 C++11 的**变参模板(variadic templates)**来实现可变参数函数的例子,重点是递归展开参数包:

cpp 复制代码
// 终止递归的普通模板函数:当只剩一个参数时,直接返回它
template<typename T>
T sum(T n) {
    return n;
}
// 递归模板,处理多个参数
template<typename T, typename... Args>
T sum(T n, Args... rest) {
    return n + sum(rest...);  // 将第一个参数加上剩余参数的和
}
int main() {
    cout << sum(1,2,3,4,5,6,7);        // 输出整数的和
    cout << sum(3.14, 2.718, 2.23606); // 输出浮点数的和
}

要点:

  • Args... rest 是参数包,sum 函数递归调用自身展开参数包。
  • 递归的基准是只有一个参数时调用第一个 sum,直接返回。
  • 这样,sum 支持任意数量参数和类型(前提是参数间能用 + 操作符相加)。
    用这个sum函数,再写一个average函数就很容易啦!

* 在 template<typename T, typename... Args> T sum(T n, Args... rest) 里,函数的返回类型 T第一个参数的类型

  • 但如果传入的参数类型混合,比如 sum(1, 2.3),第一个参数是 int,第二个是 double
  • 按当前写法返回 int,导致 1 + 2.3 结果被截断为 3(丢失小数部分)。
    你用 C++11 的 auto尾返回类型 试图修正:
cpp 复制代码
template<typename T, typename... Args>
auto sum(T n, Args... rest) -> decltype(n + sum(rest...)) {
    return n + sum(rest...);
}

理论上这可以根据 n + sum(rest...) 表达式推导返回类型,但问题是递归终止条件的声明 没给全(基准函数),导致编译器不知道 sum(rest...) 的返回类型,出现编译错误。

解决方案

你必须同时写出基准版本和递归版本,且保证基准版本返回类型能被尾返回类型推导用到。举例:

cpp 复制代码
// 基准函数,只有一个参数,返回类型就是参数类型
template<typename T>
T sum(T n) {
    return n;
}
// 递归函数,返回类型依赖 n + sum(rest...)
template<typename T, typename... Args>
auto sum(T n, Args... rest) -> decltype(n + sum(rest...)) {
    return n + sum(rest...);
}

这样编译器能在递归展开时正确推导出类型。
总结:

  • 使用尾返回类型 decltype(...) 时,递归调用的基准函数必须先定义好,确保 sum(rest...) 的类型能被正确推导。
  • 否则编译器会抱怨类型不完整。

这是一个非常经典的C++模板元编程的小坑:

  • 在函数模板的尾返回类型(decltype)中递归调用自己,比如 decltype(n + sum(rest...))
  • 编译器在还没读完整个函数声明时,不能知道 sum 的完整类型,导致报错------这是因为函数名在非成员函数的上下文中不允许自引用。
    而成员函数(尤其是静态成员函数)有一个例外,编译器允许它在声明时递归引用自己,因为它可以通过类作用域推导函数名。

解决方案示例:把 sum 变成静态成员函数

cpp 复制代码
struct Sum {
    template<typename T>
    static T sum(T n) {
        return n;
    }
    template<typename T, typename... Args>
    static auto sum(T n, Args... rest) -> decltype(n + sum(rest...)) {
        return n + sum(rest...);
    }
};
int main() {
    std::cout << Sum::sum(1, 2.5, 3) << std::endl; // 正确输出 6.5
}

这样写的好处:

  • sumSum 结构体的静态成员函数,
  • 编译器允许 decltype 中递归调用 sum
  • 解决了递归尾返回类型推导的问题。

你写的 avg 利用之前的 Sum::sum 计算所有参数的和,再用 sizeof...(args) 计算参数个数,完美实现了平均值的计算。

完整示例如下:

cpp 复制代码
#include <iostream>
struct Sum {
    template<typename T>
    static T sum(T n) {
        return n;
    }
    template<typename T, typename... Args>
    static auto sum(T n, Args... rest) -> decltype(n + sum(rest...)) {
        return n + sum(rest...);
    }
};
template<typename... Args>
auto avg(Args... args) -> decltype(Sum::sum(args...)) {
    return Sum::sum(args...) / (sizeof...(args));
}
int main() {
    std::cout << avg(2.2, 3.3, 4.4) << std::endl;  // 输出: 3.3
    std::cout << avg(2, 3.3, 4L) << std::endl;     // 输出: 3.1 (约等)
}
  • 这样写,avgsum 都能自动推导正确返回类型,支持混合参数类型。
  • sizeof...(args) 是C++11提供的编译时参数包大小运算,非常方便。

constexpr 关键字允许函数在编译期求值,如果传入的参数是常量表达式(比如字面量或 constexpr 变量),函数就能在编译时直接计算结果,从而提升性能,尤其适用于数组大小等需要编译时常量的场合。

你给出的例子解读:

cpp 复制代码
const int SIZE = 100;
template<typename T>
constexpr T square(const T& x)
{
    return x * x;
}
Widget warray[SIZE];          // SIZE 是常量,合法
Widget warray2[square(SIZE)]; // square(SIZE) 是constexpr,合法
int val = 50;
int val_squared = square(val);  // val 不是constexpr,运行时计算
Widget warray3[square(val)];    // square(val) 不是编译时常量,数组大小非法,编译错误

总结:

  • constexpr 函数既能编译时求值,也能运行时调用(根据参数是否为常量表达式)。
  • 用于数组大小时,必须传入常量表达式,否则编译失败。

C++11 的 constexpr 函数限制较多:

  • 函数体只能是单个 return 表达式,不能有多条语句。
  • 不支持 if...else 语句,但可以用条件运算符 ?: 代替。
  • 不支持循环,但支持递归。
    C++14 对 constexpr 进行了放宽:
  • 允许函数体内包含多条语句和局部变量。
  • 支持 if...else 和循环等控制结构,只要没有副作用(如修改全局变量或调用非 constexpr 函数)。
  • 仍禁止 gototry/catch、调用非 constexpr 函数等。
    总结:C++14 的 constexpr 更强大、更接近普通函数,但仍保证编译时求值的能力。

Template aliasusing 关键字定义模板别名,替代以前复杂且容易出错的 typedef

  • 例如:

    cpp 复制代码
    template<typename T>
    using setGT = std::set<T, std::greater<T>>;

    这样 setGT<double> 就是 std::set<double, std::greater<double>> 的别名。

  • 也可以用 using 替代普通指针函数类型的 typedef,语法更清晰:

    cpp 复制代码
    typedef void (*voidfunc)();
    using voidfunc = void (*)();
  • 总之,using 语法更现代,更易读,推荐替代传统的 typedef

  • Unicode 字符串字面量
    • u8"..." 表示 UTF-8 编码的字符串(类型是 const char*,C++11 起支持)
    • u"..." 表示 UTF-16 编码的字符串(类型是 const char16_t*
    • U"..." 表示 UTF-32 编码的字符串(类型是 const char32_t*
  • 原始字符串字面量(Raw string literals)
    • R"( ... )" 语法定义,字符串内部不需要转义,大大方便了包含大量反斜杠、引号的字符串。

    • 例子:

      cpp 复制代码
      string s = "backslash: \"\\\", single quote: \"'\"";
      string t = R"(backslash: "\", single quote: "'")";
      // s 和 t 内容相同
    • 还可以使用自定义分隔符,避免字符串中包含的符号干扰原始字符串界定符:

      cpp 复制代码
      string u = R"xyz(And here's how to get )" in!)xyz";

Inline Namespaces 的作用和机制:

  • 目的:方便库或API的版本管理。
  • 原理inline namespace 内的名字会自动提升(hoist)到外层命名空间,这样用户访问时不必写版本号。
  • 效果
    • 用户直接用 bdsoft::Foo 就能访问 v2_0 版本中的 Foo
    • 旧版本 v1_0 和实验版本 v3_0 仍然保留,可以显式访问,如 bdsoft::v1_0::Foo
      示例:
cpp 复制代码
namespace bdsoft {
  namespace v1_0 {
    void func();  // 旧版本
  }
  inline namespace v2_0 {
    void func();  // 默认版本,直接用 bdsoft::func() 访问
  }
  namespace v3_0 {
    void func();  // 实验版本,需要显式命名空间
  }
}

调用:

cpp 复制代码
bdsoft::func();          // 调用 v2_0 版本的 func()
bdsoft::v1_0::func();   // 调用旧版本
bdsoft::v3_0::func();   // 调用实验版本

关于 Attributes(属性)

  • 它们是标准化的、统一的方式来给编译器传递额外信息,替代以前平台相关的 #pragma__attribute____declspec 等非标准写法。
  • 语法是用双中括号 [[attribute]]
  • 例如:
    • [[noreturn]]:告诉编译器该函数不会返回(如调用 exit() 的函数),帮助编译器进行错误检测和优化。
    • [[deprecated]](C++14引入):标记某个实体为已弃用,编译器在使用该实体时会发出警告。
      用法示例:
cpp 复制代码
[[noreturn]] void fatalError() {
    std::exit(1);
}
[[deprecated("Use newFunction() instead")]]
void oldFunction() {
    // ...
}
int main() {
    oldFunction();  // 编译时会有警告
    // fatalError(); // 该函数不会返回
}

[[deprecated]] 还能用于:

函数、类、typedef、枚举、变量、非静态数据成员、模板的完整特化等。

这让代码维护和升级时,可以明确告知使用者某些接口即将被废弃。需要我帮你写更多例子吗?

关于 More Language Features:

  1. Scoped enums(enum classes)
    • 使用 enum class 定义的枚举,枚举值不会直接泄露到外围作用域,避免命名冲突。
    • 需要用 EnumName::Enumerator 访问枚举值。
    • 减少了隐式转换,类型安全更好。
    • 可以显式指定底层类型,如 enum class Color : uint8_t { Red, Green, Blue };
  2. long long
    • 新增的整型,至少64位宽,适合存储较大整数。
  3. alignas / alignof
    • alignas 用于强制指定变量或类型的内存对齐方式。
    • alignof 用于查询类型的对齐要求(字节数)。
      例子:
cpp 复制代码
enum class Color : uint8_t { Red, Green, Blue };
alignas(16) int data[4];  // data 按16字节对齐
std::cout << alignof(double) << "\n";  // 输出 double 的对齐字节数

关于 Yet More Language Features

  1. Generalized Unions(泛化的联合体)
    • 传统 C++ 联合体只能包含没有构造函数、析构函数、赋值运算符的成员。
    • C++11 开始,联合体成员允许拥有构造函数、析构函数和赋值操作符。
    • 但如果联合体成员定义了这些用户自定义的函数,整个联合体对象将不能调用这些函数(编译器会当成 =delete 处理),意味着不能直接使用这些成员的构造、析构、赋值。
  2. Generalized PODs(泛化的POD类型)
    • C++98 中的 POD(Plain Old Data)类型有更严格的定义。
    • C++11 将 POD 类型细分成多个更细致的类别:
      • POD types:符合某些规则的简单结构体或联合体。
      • Trivially copyable types:能用简单的字节复制方式安全复制的类型。
      • Trivial types:构造、析构和复制都是平凡操作的类型。
      • Standard-layout types:内存布局和 C 兼容的类型,可以用于低层操作。
    • 这些分类帮助标准库和编译器更好地优化和保证类型安全。
      简而言之,C++11 让联合体更灵活,且对"简单类型"的分类更细致,更准确。

Yet More Language Features 进一步补充:

  1. Garbage Collection ABI
    • C++11 规范定义了一个"垃圾回收应用二进制接口"(Garbage Collection ABI),
    • 主要是为实现垃圾回收机制的编译器/运行时制定基础规则和接口。
    • 注意 :标准并不要求必须实现垃圾回收,也不包含具体的垃圾回收机制,
      只是为未来支持垃圾回收的实现预留了接口规范。
  2. User-Defined Literals(用户自定义字面量)
    • 允许程序员定义新的字面量后缀,将字面量直接转换成自定义类型对象。

    • 例如,定义一个字面量操作符,可以让下面的写法合法:

      cpp 复制代码
      Binary b = 11010101001011b;
    • 这里 b 是用户自定义的后缀,11010101001011 是字面量(通常是整数或字符串),
      编译器调用对应的字面量操作函数,构造出一个 Binary 类型对象。

    • 这种功能使代码更简洁、可读,且能轻松支持各种自定义字面量格式(比如时间、单位、颜色等)。

C++14 新增的两个很实用的语言特性:

  1. Binary Literals(二进制字面量)
    • 使用 0b0B 作为前缀,可以直接写二进制数字,

    • 比如:

      cpp 复制代码
      auto fifteen = 0b1111;  // 十进制的15
    • 方便直接用二进制表示数据,尤其对底层编程很有用。

  2. 单引号作为数字分隔符(Digit Separator)
    • 可以在数字字面量中用单引号 ' 来分隔数字部分,提升可读性,

    • 例如:

      cpp 复制代码
      auto filler_word = 0xdead'beef;  // 十六进制
      auto aBillion = 1'000'000'000;   // 十进制一亿
    • 这个特性不会改变数字值,只是让数字更易读,尤其是大数字或长数字。

这是 C++11 和之后版本引入的一组支持更好类设计的特性总结:

  1. Generated functions: default / delete
    • 允许程序员显式声明让编译器生成默认的构造函数、析构函数、拷贝/移动函数(= default),或显式禁止某些函数(= delete)。
    • 这样写代码更清晰,控制更精确。
  2. Override control: override / final
    • override 关键字标记派生类中的函数覆盖基类虚函数,编译器帮你检查是否真正覆盖。
    • final 用于阻止虚函数被进一步覆盖,或阻止类被继承。
  3. Delegating constructors(委托构造函数)
    • 一个构造函数可以调用同类的另一个构造函数,避免代码重复。
  4. Inheriting constructors(继承构造函数)
    • 派生类可以继承基类的构造函数,减少写构造函数的麻烦。
  5. In-class initializers(类内成员初始化)
    • 可以直接在类定义中给成员变量赋默认值,简化构造函数代码。
  6. Explicit conversion operators
    • 支持显式定义类型转换操作符,防止隐式类型转换导致的问题。

这个问题是关于"如何禁止对象拷贝"的。

旧的C++解决办法:

  1. 把拷贝构造函数和拷贝赋值运算符声明为私有(private):
    • 这样外部代码无法访问它们,导致无法拷贝。
    • 但这样会带来一些麻烦,比如需要手动声明且不定义(如果不定义,链接会报错),不直观且难以维护。
  2. 继承自专门的不可拷贝基类(如 Boost 的 noncopyable):
    • 该基类把拷贝构造和赋值操作符声明为私有,子类就自动禁用了拷贝。
    • 但引入了继承关系,有时候可能不是最佳设计。
      这两种方案都不够理想 ,C++11后来提供了更好的写法 ------ = delete,可以明确表示禁止拷贝函数。

这是在介绍C++11中两个很重要的功能:= default= delete,用于控制类中函数的生成和禁用。

具体说明:

  • T() = default;
    告诉编译器生成一个默认的构造函数,等同于自动生成的,但写出来后更明确。
  • T(const char *str) : s(str) {}
    自定义的构造函数,初始化字符串成员。
  • T(const T&) = delete;
    显式禁止拷贝构造函数。任何试图用拷贝构造创建对象的操作都会导致编译错误。
  • T &operator=(const T&) = delete;
    显式禁止拷贝赋值操作符,试图赋值也会报错。
    main() 里:
  • T t; 调用默认构造函数,正常。
  • T t2("foo"); 调用带参数构造函数,正常。
  • T t3(t2); 试图拷贝构造,编译错误。
  • t = t2; 试图拷贝赋值,编译错误。
    总结:
    = default 用来显式声明使用默认实现;
    = delete 用来禁止某些函数(比如拷贝构造和赋值),避免错误的使用,写法简洁明了,比旧式的私有声明方法更安全、更直观。

这里说明了 = delete 不仅可以用来禁止某个函数的生成,还能用来限制重载函数的参数类型。

代码分析:

cpp 复制代码
void doSomething(int);             // 允许调用,接受int参数
void doSomething(long) = delete;  // 明确禁止long参数版本
void doSomething(double) = delete; // 明确禁止double参数版本
int main()  
{ 
    doSomething(10);    // 直接传int,调用int版本,正常
    doSomething('c');   // 'c'是char,能隐式转换成int,调用int版本,正常
    doSomething(10L);   // long类型,long版本被delete,报错
    doSomething(2.3);   // double类型,double版本被delete,报错
    doSomething(2.3F);  // float类型,能转换成double但double版本被delete,报错
} 

重点:

  • 通过 = delete 禁止某些重载版本,这样调用这些被禁止版本的代码就会编译错误。
  • 对于能隐式转换到允许的重载(比如char转int),调用依然正常。
  • float 调用时会优先匹配 double 版本(存在提升转换),但由于double版本被delete,调用失败。
    这是一种限制函数调用参数类型的现代方法,代替旧式的SFINAE或者模板特化技巧,代码更直观。

旧式 C++ 中覆盖(overriding)接口设计的问题和潜在的错误风险。具体来说:

问题点

  1. 是否真的覆盖了基类的虚函数?
    • Derived::f(int) 明显是覆盖了 Base::f(int),因为签名相同且基类函数是虚的。
    • 但是 Derived::g() 是不是覆盖了 Base::g() const
      不同点在于 const,所以它实际上不是覆盖,而是重载,可能会导致运行时调用基类的版本而不是派生类的版本,造成隐藏(bug隐患)。
  2. Derived::ff(int) 是否是虚函数?
    • 基类中是 virtual void ff(int),派生类中写成了 void ff(int),没有写 virtual,但它依然是虚函数,因为虚性在继承中保留,但这样写容易迷惑,代码可读性变差。
  3. Derived::h(int) 是不是覆盖基类的虚函数?
    • 基类的 h(int) 不是虚函数,派生类的 h(int) 只是新成员函数,不是覆盖基类虚函数。
    • 调用时可能不会有多态效果。
  4. 如何防止进一步覆盖?
    • 旧式 C++ 没有语法手段阻止派生类进一步重写虚函数,导致设计不安全。

总结

  • 旧式 C++ 覆盖接口容易出错(如错写 const 导致没有覆盖,漏写 virtual 影响代码可读性)
  • 也难以显式防止进一步重写,增加维护难度

现代 C++ 改进(后续章节提到)

  • override 关键字:强制编译器检查是否真的覆盖了基类虚函数,避免遗漏 const 或参数类型错误
  • final 关键字:禁止后续派生类继续覆盖虚函数
    这两个关键字是解决上述问题的利器。

这段讲的是 C++11 引入的 final 关键字用于整个类的修饰

  • class Derived final : public Base { ... };
    这里 Derived 类被声明为 final,表示它是"最终类",不能再被继承。
  • 如果你尝试写 class Further_Derived : public Derived { ... }; 就会报错,编译器阻止了从 Derived 继承。

用途

  • 防止继承,确保类的设计不被改变,增加安全性和性能优化机会(比如虚函数调用的优化)。
  • 明确设计意图:告诉用户这个类不能作为基类使用。

这段代码展示了旧C++中构造函数之间无法直接调用彼此的情况,导致代码重复和潜在错误。

重点说明:

  • FluxCapacitor 有多个构造函数:
    • 默认构造函数 FluxCapacitor(),初始化 capacity 为0,id 自增。
    • double 参数的构造函数,初始化 capacity,然后调用 validate()
    • complex<double> 参数的构造函数,初始化 capacity,然后调用 validate()
    • 拷贝构造函数只初始化了 id,没有正确复制 capacity,这就是 潜在的"silent logic error"(无声的逻辑错误),即拷贝构造函数不复制对象状态,可能导致程序行为异常。
  • 问题
    • 代码中三个构造函数都有重复的初始化和 id 递增逻辑。
    • 拷贝构造函数没有调用其他构造函数,也没有复制 capacity,缺少逻辑,容易出错。
    • 旧C++中,构造函数不能相互调用(即不能写构造函数委托),只能重复写初始化代码。

C++11 解决方案:构造函数委托(Delegating Constructors)

  • C++11允许构造函数调用类中的其他构造函数,避免重复代码,减少错误。
  • 可以改写为:
cpp 复制代码
class FluxCapacitor {
public:
    FluxCapacitor() : FluxCapacitor(0) {}  // 委托给double版本
    FluxCapacitor(double c) : capacity(c), id(nextId++) { validate(); }
    FluxCapacitor(std::complex<double> c) : FluxCapacitor(c.real()) {} // 简单示例
    FluxCapacitor(const FluxCapacitor &f) : capacity(f.capacity), id(nextId++) {}
private:
    std::complex<double> capacity;
    int id;
    static int nextId;
    void validate();
};

这样,默认构造函数和complex<double>构造函数都复用了double版本的初始化逻辑,避免重复和错误。

你给出的 C++11 版本的构造函数委托示例,充分展示了 C++11 委托构造函数的威力:

  • 默认构造函数 FluxCapacitor() 直接调用 FluxCapacitor(0.0),避免了重复代码。
  • double 参数版本又委托给了 complex<double> 版本,将初始化逻辑统一到最后一个构造函数中。
  • 拷贝构造函数用 f.capacity 调用委托构造函数,确保了正确复制状态。
    优点
  • 代码更简洁,逻辑更集中。
  • 不容易忘记初始化成员(如 capacity)。
  • 维护性更好。
    缺点(正如你提到的):
  • 这种委托调用引入了多次构造过程,可能带来性能上的细微开销(多次构造/析构)。
  • 但一般来说,这种性能影响很小,且编译器优化通常能减轻这个问题。

你这段讲的是旧 C++ 和 C++11 在类成员初始化上的区别,特别是**"类内初始化"(in-class member initializers)**的问题。

旧C++的问题:

  • 只有 const static 的整型成员(比如 static const size_t num_cells = 50;)可以在类内初始化。
  • 非静态成员变量(如 capacity)和非整型静态成员(如 nextId)不能在类内初始化,必须在构造函数里初始化。
  • 数组成员(如 Cell FluxCells[num_cells];)可以声明,但如果它依赖非静态constexpr或者变量就比较麻烦。
    示例中:
cpp 复制代码
complex<double> capacity = 100;  // ERROR! 旧C++不支持非静态成员的类内初始化
static int nextId = 0;            // ERROR! 不能在类内初始化非 const static 成员

C++11的改进:

  • 允许非静态成员在类定义内直接初始化 ,如:

    cpp 复制代码
    complex<double> capacity = 100;
  • 但静态成员变量仍需在类外初始化(一般还是写在.cpp文件里):

    cpp 复制代码
    int FluxCapacitor::nextId = 0;

这给代码带来什么好处?

  • 代码更简洁,初始化逻辑更集中。
  • 避免了构造函数里重复写默认值。
  • 让代码更安全,减少遗漏初始化的错误。

你这一页展示了 C++11 对类内成员初始化的扩展改进,下面是详细解释:

cpp 复制代码
class FluxCapacitor {
public:
    static const size_t num_cells = 50;
    FluxCapacitor(complex<double> c) : capacity(c), id(nextId++) {}
    FluxCapacitor() : id(nextId++) {}
    ... 
private : 
    int id;
    complex<double> capacity = 100;
    static int nextId = 0;
    Cell FluxCells[num_cells];
};

C++11 的新特性:In-Class Initializers

  • 非静态成员变量现在可以直接在声明时赋初值。
  • 这简化了构造函数,并让代码更安全。
    示例:
cpp 复制代码
complex<double> capacity = 100;  //  OK in C++11

仍然不允许的:

  • 不能在类内初始化const 的 static 成员
    示例:
cpp 复制代码
static int nextId = 0;  //  ERROR:必须在类外初始化

解决方法是:把初始化放到类外:

cpp 复制代码
int FluxCapacitor::nextId = 0;  //  OK(在类外)

总结各成员状态:

成员 C++98 C++11 是否合法
static const size_t num_cells = 50; OK OK 合法
complex<double> capacity = 100; ERROR OK 合法
static int nextId = 0; ERROR ERROR 非法(需类外)
Cell FluxCells[num_cells]; OK(如果num_cells是const) OK 合法

这页讲的是 C++11 中的新特性:继承构造函数(Inheriting Constructors),下面是具体解析:

问题背景(C++98 的限制)

在 C++98 中:

  • 即使派生类没有自定义构造函数,你也无法继承基类的构造函数
  • 所以你必须手动重复定义每一个构造函数,很容易出错、代码重复多。

C++11 的解决方案:using Base::Base

cpp 复制代码
using FluxCapacitor::FluxCapacitor;

这行代码的作用是:

  • 将基类 FluxCapacitor 的构造函数(除了默认构造函数)直接继承 给派生类 RedBlackFluxCapacitor
  • 你仍然可以添加你自己的构造函数(例如通过 Color 构造的版本)。

示例详解:

cpp 复制代码
class RedBlackFluxCapacitor : public FluxCapacitor 
{ 
public: 
    enum Color { red, black };              // 新增的功能
    using FluxCapacitor::FluxCapacitor;    // 自动继承基类构造函数
    RedBlackFluxCapacitor(Color c) : color(c) {}  // 新构造函数
    void setColor(Color c) { color = c; }
private: 
    Color color { red };   // 成员变量初始化(C++11 中也允许)
};

重要注意事项:

  • 默认构造函数(无参构造)不会被继承,你需要手动写。
  • 如果你重新定义了某个构造函数,它会遮盖继承来的对应构造函数
  • 成员默认初始化(如 Color color { red };)现在也是合法的 C++11 特性。

总结:

特性 C++98 C++11
自动继承构造函数 不支持 using Base::Base
可以添加自定义构造函数 支持 支持
默认构造函数自动继承 不继承 仍需自定义
成员变量默认初始化 不支持 支持

C++11 引入的一个重要改进:显式转换运算符(explicit operator。让我们一一分析它的背景、用法和意义:

旧版 C++ 的问题(C++98):

在 C++98 中:

  • 你可以使用 explicit 来限制构造函数的隐式转换:

    cpp 复制代码
    explicit Rational(int n, int d);
  • 但是不能explicit 限制转换运算符,如 operator double()
    所以这类转换运算符在任何需要 double 的地方都可能被隐式调用,导致意外或不安全的行为。

C++11 的解决方案:

C++11 允许你这样写:

cpp 复制代码
explicit operator double() const;

这表示:

  • 该转换必须显式调用 ,不能在上下文中被自动转换成 double

示例解析:

cpp 复制代码
class Rational {
public:
    // 隐式转换,容易引发问题
    operator double() const;            //  C++98只能这样,危险
    // 显式转换,需要明确调用
    explicit operator double() const;   //  更安全,C++11支持
    // 明确的命名函数,最安全的做法
    double toDouble() const;            //  推荐!
private:
    long num, denom;
};

使用示例对比

假设有如下函数:

cpp 复制代码
void printDouble(double d);
Rational r(3, 4);

不同写法的影响:

写法 是否允许?(若用 explicit 说明
double x = r; 不允许 隐式转换被禁用
double x = static_cast<double>(r); 允许 必须显式转换
printDouble(r); 不允许 隐式转换被禁用
printDouble(static_cast<double>(r)); 允许 明确表示你知道自己在干什么

最佳实践建议:

方法 安全性 可读性 推荐程度
operator double() 不建议
explicit operator double() 中等 建议
toDouble() 命名函数 最高 最高 强烈推荐

你这页是对 C++11/14 中几个 **"重大语言特性"**的提纲式总结,我们逐项解析:

1. Initialization(初始化)

包括三个子特性:

a. 初始化列表(Initializer Lists)

  • 允许用 {} 统一初始化容器或用户自定义类型。

  • 示例:

    cpp 复制代码
    std::vector<int> v = {1, 2, 3, 4};

b. 统一初始化语法(Uniform Initialization)

  • {} 初始化任何对象,统一语法:

    cpp 复制代码
    int x{5};
    MyClass obj{arg1, arg2};

c. 防止窄化(Prevention of Narrowing)

  • 窄化(narrowing)是把大类型的值赋给小类型,可能丢失精度:

    cpp 复制代码
    int x1 = 3.14;     // OK,旧 C++
    int x2{3.14};      //  错误,C++11 检查

2. Lambdas(λ表达式)

  • 内联定义匿名函数,适用于算法、回调、函数对象等:

    cpp 复制代码
    auto add = [](int a, int b) { return a + b; };
    std::cout << add(2, 3); // 输出 5

3. Rvalue References & Move Semantics(右值引用与移动语义)

核心是提高性能,避免不必要的复制:

a. Rvalue Reference (T&&)

  • 表示绑定到右值(临时对象)上,用于资源的"偷取"。

b. Move Semantics(移动语义)

  • 让类支持将资源"移走"而不是复制。

    cpp 复制代码
    std::vector<int> a = {1,2,3};
    std::vector<int> b = std::move(a);  // 移动资源而非复制

4. Universal References(万能引用)

  • 出现在模板中,形如 T&&,可以绑定左值或右值。

    cpp 复制代码
    template<typename T>
    void func(T&& arg);  // 这是万能引用

5. Perfect Forwarding(完美转发)

  • 保留传入参数的左/右值性质,用 std::forward 实现:

    cpp 复制代码
    template<typename T>
    void wrapper(T&& arg) {
      func(std::forward<T>(arg));  // 不丢失引用信息
    }

总结图示:

特性 作用
初始化列表 简化容器/对象初始化
统一初始化 统一风格 + 防窄化
Lambda 简洁定义函数对象
Rvalue Ref 绑定右值
Move Semantics 资源转移,提高性能
Universal Ref 模板中通用绑定引用
Perfect Forwarding 保持参数的"原样性"

旧版 C++(C++03 及更早)在初始化聚合类型(aggregates)方面的局限性,并通过几个例子展示:

聚合初始化是什么?

"聚合类型"指的是没有构造函数、继承、私有成员 等复杂特性的简单类或结构体(如 struct Point)。这种类型允许用大括号 {} 初始化成员。

示例逐条解释:

cpp 复制代码
int vals[] = { 10, 100, 50, 37, -5, 999 };

合法。数组的初始化在老 C++ 中就可以这样做。

cpp 复制代码
struct Point { int x; int y; };
Point p1 = {100, 100};

合法 。简单结构体(聚合)成员可用 {} 初始化。

cpp 复制代码
vector<int> v = { 5, 29, 37 };

非法 (在 C++03 中)。标准容器不支持这种"列表初始化",这是 C++11 引入 initializer_list 之后才支持的

cpp 复制代码
const int valsize = sizeof vals / sizeof *vals;
vector<int> v2(vals, vals + valsize);

在 C++03 中,这是你能用的唯一方式去初始化 vector(用指针范围)。

问题总结:

在旧 C++ 中:

类型 是否支持 {} 初始化?
数组
struct(聚合)
STL 容器(如 vector
自定义类(有构造函数)

解决:C++11 引入"统一初始化"

C++11 提供统一的 {} 初始化支持所有这些情况,包括 STL 容器和用户类,从而让你能这么写:

cpp 复制代码
std::vector<int> v = {5, 29, 37};  // OK in C++11
MyClass m = {arg1, arg2};         // OK if构造函数匹配 initializer_list

C++11 中引入的 std::initializer_list ,它极大扩展了初始化语法的功能,使用户自定义类型(如 vector)也可以使用花括号 {} 来初始化。

关键点总结:

1. 统一初始化语法
cpp 复制代码
vector<int> v = { 5, 29, 37 };
vector<int> v2 { 5, 29, 37 };

这两种写法在 C++11 中是等效的,第二种甚至不需要 =

2. 不只是初始化,还能赋值:
cpp 复制代码
v2 = { 10, 20, 30, 40, 50 };

这是因为 vector 提供了一个 operator=(std::initializer_list<T>),可以让你用花括号重新赋值。

实现细节:STL 容器如何支持花括号初始化?

示例中提到 vector 的简化实现:

cpp 复制代码
template<typename T>
class vector {
public:
    vector(std::initializer_list<T>);          // 构造函数
    vector& operator=(std::initializer_list<T>); // 赋值运算符
    ...
};

只要你的类提供这些函数,你的类也能支持类似用法:

自定义类型的示例:

cpp 复制代码
class MyContainer {
public:
    MyContainer(std::initializer_list<int> list) {
        for (int x : list) {
            data.push_back(x);
        }
    }
    MyContainer& operator=(std::initializer_list<int> list) {
        data.clear();
        for (int x : list) {
            data.push_back(x);
        }
        return *this;
    }
private:
    std::vector<int> data;
};
MyContainer mc = {1, 2, 3};  //  自动调用构造函数
mc = {4, 5};                 //  自动调用赋值运算符

小结

特性 描述
initializer_list 构造函数 支持 {} 初始化对象
operator= 重载 允许用 {} 进行赋值
不再局限于数组/struct 用户自定义类也可支持

这段讲解展示了 std::initializer_list 的更广泛用途 ------ 在 C++11 中,花括号 {} 初始化不仅适用于构造对象,还能广泛用于:

关键用法汇总:

1. 初始化容器:
cpp 复制代码
vector<int> v {10, 20, 30};
2. 插入多个元素:
cpp 复制代码
v.insert(end(v), {40, 50, 60});

std::vector::insert 接受 initializer_list,所以可以插入一组值。

3. 范围-based for 循环中直接使用:
cpp 复制代码
for (auto x : {1, 2, 3, 4, 5})
    cout << x << " ";

临时的 initializer_list<int> 被隐式构造出来并迭代。

4. 作为 return 值:
cpp 复制代码
return {100, 200, 300, 400, 500};

因为 vector<int>initializer_list 构造函数,返回值的 {} 会自动被解释为构造一个 vector<int>

运行输出:

cpp 复制代码
1 2 3 4 5 
100 200 300 400 500 

小结

用法 示例
构造函数初始化 vector<int> v {1, 2, 3};
赋值 v = {4, 5, 6};
插入操作 v.insert(end(v), {7, 8});
for 循环 for (auto x : {9, 10})
返回值 return {11, 12};
这些都归功于 std::initializer_list<T> 的强大支持。

这段讲解的是一个 C++11 initializer_list 重载优先级 的"陷阱" ------ 即构造函数如果同时存在:

  • 一个接受普通参数(如 vector<int>(int, int)
  • 一个接受 std::initializer_list<T>(如 vector<int>(initializer_list<int>)
    编译器会优先选择 initializer_list 版本 ,当使用大括号 {} 时。

举例解释:

常规构造(圆括号):
cpp 复制代码
vector<int> v(10, 20);

这调用的是:

cpp 复制代码
vector(int count, const T& value)

效果v 有 10 个元素,每个都是 20

initializer_list 构造(花括号):
cpp 复制代码
vector<int> v2{10, 20};

这调用的是:

cpp 复制代码
vector(initializer_list<T>)

效果v2 有两个元素,分别是 1020

为什么危险?

同样的数字组合,不同的括号,语义完全不同!

很容易在维护或阅读代码时误解含义。

建议:

"避免写与 initializer_list 构造函数模糊冲突的重载。"

换句话说:

如果你的类中有构造函数可能与 initializer_list 混淆,不妨只支持一种方式或清晰注释意图,防止出错。

实际开发中推荐做法:

  • 尽量使用圆括号 () 来明确调用意图,尤其是有数量 + 值参数的容器类。
  • 如果自定义类中使用 initializer_list 构造器,避免其他构造函数有相同参数个数/类型的组合
  • 使用 explicit 降低隐式转换误用的风险。
    需要我给你写个示例类,展示如何安全使用 initializer_list 和其他构造函数吗?

这段代码讲的是 旧版 C++ 的初始化语法混乱和容易引起误解的问题,尤其是在函数声明 vs. 变量定义、自动类型转换,以及构造/默认初始化方面。

分析逐行含义:

cpp 复制代码
int *pi1 = new int(10); //  分配并初始化为 10
cpp 复制代码
int *pi2 = new int; //  默认构造,但未初始化(可能是垃圾值)
cpp 复制代码
int *pi3 = new int(); //  初始化为 0(值初始化)
cpp 复制代码
int v1(10); //  定义 int,值为 10
cpp 复制代码
int v2(); //  编译器认为是"函数声明":返回 int 的函数 v2,不是变量!

这是著名的 Most Vexing Parse 问题。

更糟的歧义示例:

cpp 复制代码
int foo(bar); //  是定义了个变量 foo,类型是 bar 吗?还是声明了函数 foo(bar)?
  • 编译器解释为:foo 是一个函数,接受一个 bar 类型参数,返回 int
  • 这不是你可能想的"用 bar 初始化 foo"。

令人困惑的隐式类型转换:

cpp 复制代码
int i(5.5); //  合法!但会截断为 5(隐式转换 + 截断)
cpp 复制代码
double x = 10e19;
int j(x); //  合法!但会丢失大量精度,甚至溢出

C++11 之后的改进

通过 统一初始化语法(Uniform Initialization)

cpp 复制代码
int i{5.5};  //  编译错误(防 narrowing)
int j{x};    //  编译错误(x 可能超出 int 范围)
int k{};     //  值初始化为 0

这种方式避免了上面那些隐式转换和语义歧义。

建议:

  • 使用大括号 {} 来初始化变量,防止 Most Vexing Parse 和隐式窄化。
  • 避免让变量声明看起来像函数声明。
  • 对于新代码,尽量使用统一初始化风格(int x{10};)以提高可读性和安全性。

你提到的这一部分揭示了 C++11 引入的一项重大行为变更初始化时禁止缩窄(narrowing conversion),这是对旧版 C++ 的重要"修复",但也可能造成"破坏性变更"(breaking change)。

核心点:

合法(所有版本):
cpp 复制代码
S s1 = {5, 10};   // 传统聚合初始化
S s2{5, 10};      // C++11 的统一初始化
非法(缩窄 conversion):
cpp 复制代码
S s3{5, 10.5};    //  double → int,C++11 禁止缩窄
S s4 = {5, 10.5}; //  在 C++11 里也错,但在旧 C++ 中被接受(意外!)
int a[] = {1, 2, 3.3}; //  double → int,C++11 报错,旧版接受(意外!)

什么是 Narrowing Conversion?

缩窄指的是将 更大/更高精度的数据类型 (如 double, long)转换为 更小或低精度的数据类型 (如 int, float),有可能导致:

  • 数据截断(如 3.3 → 3
  • 数据丢失(精度或溢出)
  • 未定义行为(某些极端场景)

C++11 的改变:

使用统一初始化 {} 时:

如果存在缩窄转换(narrowing conversion),编译器会报错

这包括:

  • double → int
  • long → short
  • float → char
  • int → unsigned(当值为负)
  • 等等......
    这是为了避免静默错误,提升代码安全性。

总结建议:

场景 Old C++ C++11+ 建议
S s = {5, 10.5}; (自动截断) (编译错误) 避免依赖自动截断
int a[] = {1, 2, 3.3}; (截断) (编译错误) 确保类型精确匹配
S s{5, 10}; (旧) (新) (推荐) 使用统一初始化
使用 {} + narrowing 有风险 明确禁止 这是 breaking change

你说的是 C++11 中 auto 和统一初始化 {} 结合使用时的一个坑(gotcha):

关键点:

  • auto a(10);
    这里 a 被推断为 int,符合预期。
  • auto b{10};
    这里 b 不是 int,而是 std::initializer_list<int> 类型!
    这是因为统一初始化列表 {} 会优先被识别为 std::initializer_list 类型。

输出示例:

cpp 复制代码
a has type: i                      // i = int
b has type: St16initializer_listIiE  // 这是 std::initializer_list<int> 的编译器内部名

这带来的问题:

  • 有时你想写 auto x{value};,结果变量类型不是你想要的,而是变成了 initializer_list,会影响后续代码的行为。

建议:

  • 如果想要普通的类型推断,用圆括号或等号初始化

    cpp 复制代码
    auto a = 10;
    auto a(10);
  • 如果你写 {},要小心它可能变成 std::initializer_list,特别是对于单个元素。

你说的是用函数指针(比如 isPos)作为算法的谓词时,编译器通常无法进行内联优化,导致性能不如直接用函数对象或 lambda。

关键点:

  • inline bool isPos(int n) { return n > 0; }
    这个函数是 inline 的,但当用作函数指针传给 find_if通常不会被内联
    因为编译器调用函数指针时需要间接调用,不确定实际调用目标。
  • 结果就是,算法中每次调用 isPos 都是间接调用,效率较低。

旧时的解决方法:

  • 用标准库的函数对象适配器,如 bind2nd(greater<int>(), 0),不过这些写法复杂且难读。

现代 C++ 更好的解决办法:

  • 用 lambda 代替函数指针
cpp 复制代码
auto firstPos = find_if(begin(v), end(v), [](int n){ return n > 0; });
  • 这样编译器可以直接内联 lambda 体,性能更好,代码也更简洁。
    总结就是:用函数指针做谓词性能差,lambda 是现代更好的写法。

用函数对象(比如 IsPos 结构体重载 operator())确实能让编译器内联调用,提高性能,避免函数指针带来的间接调用开销。

不过它的问题是:为了一个简单的判断逻辑,必须额外定义一个结构体(或类),代码显得繁琐且不够直观。

现代写法推荐

用 lambda 表达式代替函数对象,代码既简洁又高效:

cpp 复制代码
vector<int> v {-5, -19, 3, 10, 15, 20, 100};
auto firstPos = find_if(begin(v), end(v), [](int n){ return n > 0; });
if (firstPos != end(v))
    cout << "First positive value in v is: " << *firstPos << endl;

这样不仅性能可以优化(编译器会内联 lambda),代码也更加清晰、简洁。

  • Lambda 表达式就是一种匿名函数对象,你可以在需要的地方直接写逻辑,不用额外写命名的结构体或函数。
  • 这样让代码更紧凑,逻辑局部化,特别是在使用 STL 算法时,方便又强大。
  • Herb Sutter 说的"Lambdas make STL algorithms roughly 100x more usable"这句话点出了它的革命性,让 C++ 标准库算法的使用体验极大提升。
    你刚才的代码示例也很经典:
cpp 复制代码
auto firstPos = find_if(begin(v), end(v), [](int n){ return n > 0; });

写得简洁明了,性能还好,绝对是现代 C++ 的推荐写法。

这一部分讲得很关键:

  • Lambda 的 捕获列表 [] 用来"捕获"外部作用域中的变量,捕获后这些变量可以在 lambda 体内使用。
  • 捕获的 lambda 实际上是一个匿名的函数对象,称为 closure(闭包)。
  • 例子中:
cpp 复制代码
[target, epsilon](double val) { return fabs(target - val) < epsilon; }

这里,targetepsilon 是从外部作用域捕获进来的,它们被"打包"进了 lambda,方便后续调用时使用。

  • 这样就实现了局部灵活的函数逻辑,同时还能用 STL 算法(这里是 partitionfor_each)来操作容器。

捕获模式非常灵活,这里总结一下:

  • 按值捕获 [variable1, variable2]:把变量的当前值拷贝进闭包,后续 lambda 里的修改不会影响外部变量。
  • 按引用捕获 [&variable1, &variable2]:捕获的是变量的引用,lambda 内修改会影响外部变量。
  • 默认按值捕获 [=]:闭包会把所有外部变量都按值捕获。
  • 默认按引用捕获 [&]:闭包会把所有外部变量都按引用捕获。
  • 混合捕获 [=, &variable1]:默认按值捕获,variable1 按引用捕获(反之亦然,[&, variable1] 表示默认引用捕获,variable1 按值捕获)。
    这种设计让你能灵活控制闭包内部对外部变量的访问和修改权限。

捕获限制 :lambda 的捕获只能针对非静态的局部变量(包括函数参数)。

  • 成员变量不能直接捕获 ,但在成员函数中可以通过捕获 this 指针来访问成员变量。
  • 捕获 this 后,lambda 可以访问(甚至修改)当前对象的成员。
  • 如果不想捕获 this,也可以先把成员变量值拷贝到局部变量,再捕获那个局部变量。
    例如:
cpp 复制代码
class MyClass {
    int x = 10;
public:
    void func() {
        int y = 20;
        auto lambda1 = [this]() { std::cout << x << std::endl; };  // 捕获 this,可以访问成员 x
        auto lambda2 = [y]() { std::cout << y << std::endl; };    // 捕获局部变量 y
        lambda1();
        lambda2();
    }
};

默认捕获模式([=][&])虽然方便,但会带来隐患

  • 可能无意识地捕获了不该捕获的变量,导致代码难以理解和维护。
  • 会使代码容易出现悬挂引用(dangling references),尤其是当捕获引用的变量生命周期结束后,lambda 还在使用它。
  • 还可能修改作用域之外的全局或静态变量,增加副作用风险。
  • 因此,Scott Meyers 建议明确写出需要捕获的变量,避免使用默认捕获 ,这样代码更清晰,副作用更容易控制。
    写成显式捕获,比如 [x, &y],胜过简单地写 [=][&]

Lambdas as "Local Functions"

cpp 复制代码
int main() {
    double target = 4.9;
    double epsilon = .3;
    bool withinEpsilonBAD(double val)  // ERROR!
    {
        return fabs(target - val) < epsilon;
    };
    auto withinEpsilon = [=](double val)  // OK!
    { return fabs(target - val) < epsilon; };
    cout << (withinEpsilon(5.1) ? "Yes!" : "No!"); 
}
  • C++不支持在函数内部直接定义另一个函数(即"局部函数"),
  • lambda表达式可以充当"局部函数"的角色,
  • 你可以用lambda在函数体内部定义匿名函数,甚至捕获外部变量(这里用的是值捕获 [=]),
  • 这样写既简洁又灵活,还避免了代码冗余。
    所以,想在一个函数内部写"局部函数",C++11以后的lambda就是完美替代方案。

C++14 Generic Lambdas

  • C++11的lambda参数类型必须显式写出(比如 const shared_ptr<string> &p1),
  • C++14引入了泛型lambda ,允许使用 auto 作为参数类型,简化代码且更通用,
  • 泛型lambda可以接受各种类型,只要能调用其中的方法或操作符就行。
    比如:
cpp 复制代码
sort(begin(vps), end(vps), [](const auto& p1, const auto& p2) { return *p1 < *p2; });
auto getsize = [](auto const& c) { return c.size(); };

这种写法,代码更简洁且适应面更广。

总结一下Lambda的常见用途:

  • STL算法中 ,用作谓词(predicates),比如 find_if, count_if,或者排序比较函数 sort 等。
  • **智能指针(unique_ptr, shared_ptr)**的自定义删除器(custom deleters),让资源释放更灵活。
  • 线程编程中,条件变量(condition variables)用来快速定义等待条件的谓词。
  • 回调函数(callbacks) ,尤其是在事件驱动或者异步编程中,写起来更简单,避免了额外定义函数。
    Lambda让代码更简洁、局部化且易维护。

在旧C++里,对象复制(copying)有时会发生多余且不必要的情况,特别是返回值或算术运算符重载时,可能导致性能浪费。

例如:

cpp 复制代码
Big bt = makeBig(); // 可能涉及复制构造
Big sum = x + y;    // operator+ 返回 Big 对象,可能会有额外的复制

虽然编译器可能通过 返回值优化 (RVO)命名返回值优化 (NRVO) 来消除部分复制,但不是所有情况下都能避免,特别是在复杂的表达式或函数调用链中。

总结一下旧C++解决方案的脆弱性:

  • 函数为了避免多余复制,可能改成返回引用,但这涉及对象生命周期管理,容易导致悬空引用。
  • 返回裸指针虽然可行,但增加内存泄漏和bug风险。
  • 返回智能指针 虽然安全,但引入更多语法复杂度和运行时开销。
    新的思路是:
    如果我们知道返回的对象是临时对象 (右值),那么它的资源可以被"移动"而非复制,从而避免昂贵的复制操作。
    这引出了**右值引用(rvalue reference)**的新类型引用。

Ancient Terms, Modern Meanings

  • Lvalues :可以取地址的表达式,通常代表有持久存储的对象,比如变量、引用,或者解引用的指针(*ptr),即使没有名字,也算lvalue。
  • Rvalues :不能取地址的表达式,通常是临时对象、字面量常量等,比如 42x + y 的结果,临时生成的值。
    这是理解现代C++中移动语义和右值引用的基础。接下来可以讲讲右值引用是怎么定义的,以及它如何帮助避免多余复制?

C++11 Rvalue References

  • int &&右值引用 ,只能绑定到右值(临时对象或字面量等)。
  • int &&rri = 10; 合法,因为 10 是右值。
  • int &&rri2 = i; 不合法,因为 i 是左值,不能绑定给右值引用。
  • int &ri2 = fn(); 不合法,如果 fn() 返回的是右值,不能直接绑定给普通左值引用。
  • 但是 const int &ri3 = fn(); 是合法的,常量左值引用可以绑定到右值上。
  • int &&rri4 = fn(); 右值引用可以绑定右值返回值。
    右值引用引入的目的就是让我们能区分临时对象(右值)和具名对象(左值),从而实现高效的资源转移(移动语义)。

Copy vs. Move Operations

  • 传统 C++ 有 拷贝构造函数拷贝赋值运算符

    cpp 复制代码
    T::T(const T&);
    T& operator=(const T&);

    它们负责复制对象的内容。

  • C++11 新增了 移动构造函数移动赋值运算符

    cpp 复制代码
    T::T(T&&);
    T& operator=(T&&);

    它们的作用是"偷走"参数对象的资源,把资源转移给当前对象,参数对象则变成"空壳",方便高效处理临时对象。

  • 移动操作通常被声明为 noexcept,表示不会抛异常,更安全。

现代 C++ 类设计中的"六大法宝"------六个经典函数:

  1. 默认构造函数 Big()
  2. 析构函数 ~Big()
  3. 拷贝构造函数 Big(const Big&)
  4. 拷贝赋值运算符 Big& operator=(const Big&)
  5. 移动构造函数 Big(Big&&)
  6. 移动赋值运算符 Big& operator=(Big&&)
  • 其中第 5 和第 6 是 C++11 引入的新成员,用来实现移动语义,避免不必要的拷贝,提高性能。
  • 类内部成员 Blob b; 代表一个资源管理类型(比如动态内存或文件句柄),需要特殊处理,移动操作通常"窃取"其资源,避免复制成本。
    这6个函数构成了管理类资源生命周期和复制/移动的核心,理解并正确实现它们是高效且安全的 C++ 类设计关键。
cpp 复制代码
int main() {
    Big x, y;  // Note: below, "created" really
    Big a;     //     means "not just moved"
    a = makeBig();     // 1 Big created *
    Big b(x + y);      // 1 Big created *
    a = x + y;         // 1 Big created *
    a = munge(x + y);  // 2 Bigs created *
    std::swap(x, y);   // 0 Bigs created!
}

这段代码展示了移动操作(move constructor 和 move assignment operator)在实际使用中的表现:

  • makeBig() 返回一个临时的 Big 对象,这个临时对象通过移动构造或移动赋值将资源转移到变量 a 中,而不是进行昂贵的拷贝。
  • x + y 返回一个临时的 Big,同样利用移动操作避免拷贝,直接把结果资源移动到新对象或赋值目标中。
  • munge(x + y) 返回一个临时对象,在赋值时也使用移动操作;这里因为涉及两个临时对象,所以"创建了2个Big"。
  • std::swap(x,y); 内部利用移动操作交换资源,没有创建新的 Big 对象,性能极好。
    总结:
    通过移动语义,临时对象的资源可以直接"偷走",不必复制,从而显著提升效率。这是现代 C++ 管理大对象和资源的关键技术。

旧版的 std::swap 实现是基于拷贝构造和拷贝赋值的:

cpp 复制代码
template<typename T>
void swap(T &x, T &y)
{
    T tmp(x);    // 调用拷贝构造
    x = y;       // 拷贝赋值
    y = tmp;     // 拷贝赋值
}

问题是 ,即使类型 T 支持移动语义(move constructor 和 move assignment),这个版本的 swap 依然不会用到移动操作,而是照旧用拷贝,导致性能损失。

强制使用移动语义的 C++11 版本 swap

利用 std::move 可以显式地将对象转换成右值,从而强制调用移动构造和移动赋值:

cpp 复制代码
template<typename T>
void swap(T &x, T &y)
{
    T tmp(std::move(x));   // 用移动构造代替拷贝构造
    x = std::move(y);     // 用移动赋值代替拷贝赋值
    y = std::move(tmp);   // 用移动赋值代替拷贝赋值
}

这样,swap 就能利用类型的移动操作,提高性能。

小结:

  • 旧版 swap 用的是拷贝操作,不会自动用移动操作
  • C++11 使用 std::move 强制调用移动操作,提升效率
  • 这是理解和使用移动语义时非常重要的点
    std::move 本身是个非常轻量的"零开销"函数,它不执行任何数据移动或复制操作 ,它的唯一作用就是将传入的对象强制转换为一个右值引用 (rvalue reference)。
    换句话说,std::move 只是告诉编译器:

"请把这个对象当作一个即将被销毁的临时对象来看待,可以放心使用移动语义。"

这允许编译器调用移动构造函数或移动赋值运算符,从而避免不必要的拷贝,提高效率。

重点补充

  • std::move 不会移动数据,移动是由对应类型的移动构造函数或移动赋值运算符完成的。
  • 它只是一个转换操作,将左值转换成右值引用。
  • 如果类型没有实现移动操作,则仍会退回到拷贝操作。
  • 这样写的 swap 函数签名不变,但内部使用了移动语义,更高效。
    总结一句话:
    std::move 是一个零成本的类型转换辅助函数,用于启用移动语义。

这段代码很典型地展示了 C++11 的移动构造函数和移动赋值操作符的写法,细节上也很关键:

cpp 复制代码
class Big {
public:
    ... 
    Big(Big &&rhs)
        :  // 移动构造函数
          b(std::move(rhs.b)),  // 将 rhs.b 转换为右值引用,调用 Blob 的移动构造函数
          x(rhs.x)              // 直接拷贝基本类型成员
    {}
    Big &operator=(Big &&rhs)  // 移动赋值操作符
    {
        b = std::move(rhs.b);  // 需要用 std::move 将 rhs.b 转为右值引用,触发移动赋值
        x = rhs.x;             // rhs 本身是左值,所以这里直接赋值基本类型成员
        return *this;
    }
private:
    Blob b;
    double x;
};
  • 移动构造函数Big(Big&& rhs)
    通过 std::move(rhs.b)rhs.b 转换成右值引用,调用 Blob 类型的移动构造函数,从而"偷走"资源。
    这里 rhs.x 是基本类型,直接拷贝即可。
  • 移动赋值操作符Big& operator=(Big&& rhs)
    在赋值中,rhs 虽然是右值引用类型,但它本身是一个命名变量(lvalue ),因此需要用 std::move(rhs.b) 明确将其转换为右值引用,触发移动赋值。
    基本类型 x 依然直接赋值即可。
    总结:
  • 移动操作里,成员对象(这里是 Blob b)的移动语义至关重要,Big 的移动操作实际上依赖于 Blob 的正确实现。
  • std::move 是必不可少的,否则移动语义无法触发。
  • 基本数据类型直接拷贝,因为它们本身就是小型值类型,不需要"移动"。
cpp 复制代码
class Blob { 
public: 
    ... 
    // 移动构造函数
    Blob(Blob &&rhs) {   
        raw_ptr = rhs.raw_ptr;  // "偷取"指针资源
        rhs.raw_ptr = nullptr;  // 清空源对象的指针,避免悬挂指针
    } 
    // 移动赋值操作符
    Blob &operator=(Blob &&rhs) {  
        if (this != &rhs) {       // 防止自赋值
            delete [] raw_ptr;    // 释放当前对象持有的资源
            raw_ptr = rhs.raw_ptr; // "偷取"指针资源
            rhs.raw_ptr = nullptr; // 清空源对象的指针,避免悬挂指针
        } 
        return *this;            // 返回当前对象的引用
    } 
private: 
    char *raw_ptr;  // 原始指针,管理动态分配的资源
};

这段代码展示了 Blob 类的移动构造函数和移动赋值操作符,它们负责"偷取"资源指针,避免不必要的深拷贝。具体来说:

  • 移动构造函数中,将 rhs(右值参数)内部的指针直接拿过来,然后把 rhs.raw_ptr 置空,防止两者指向同一块内存。
  • 移动赋值操作符中,先判断自赋值,然后释放当前资源,再"偷取" rhs 的指针,最后将 rhs.raw_ptr 置空。

**万能引用(Universal References)**的概念,Scott Meyers提出的:

  • 当你写 auto&& x = ...,这个 && 并不总是表示"右值引用"。
  • 它取决于初始化表达式的值类别:
    • 如果初始化的是右值(如 3.1415),那么 x 是一个右值引用。
    • 如果初始化的是左值(如变量 pi),那么 y 实际上是一个左值引用。
      再看模板例子:
cpp 复制代码
template<typename T>
void f(T&& val);

这里的 T&& 是万能引用(也叫转发引用),它既能绑定到左值,也能绑定到右值:

  • f(3.14); 调用时,T 被推断为 double,函数参数类型是 double&&(右值引用)
  • f(x); 传递的是右值,T 推断为 double,参数是 double&&
  • f(pi); 传递的是左值,T 推断为 double&,参数是 double& &&,根据引用折叠规则简化成 double&

C++模板类型推导时,引用折叠规则(reference collapsing rules),即当你写出两个引用连续出现时,编译器会根据规则"折叠"为一种引用类型。

具体规则是:

组合 折叠结果
T& & T&
T&& & T& ← 左值引用传染性
T& && T& ← 左值引用传染性
T&& && T&&
这就是为什么模板参数推导中:
cpp 复制代码
template<typename T>
void f(T&& val);
  • 调用 f(3.14); 时,T 推断为 double,所以 T&&double&& &&,折叠后为 double&& (右值引用)
  • 调用 f(pi); 时,T 推断为 double&(因为 pi 是左值),所以 T&&double& &&,折叠后为 double& (左值引用)
    这使得万能引用既可以绑定左值也可以绑定右值,非常灵活。

你展示的这个例子说明了当类有多个成员(尤其是昂贵的拷贝资源)时,构造函数数量可能会指数级增长,以覆盖所有可能的拷贝/移动组合。

示例中,Big类包含两个成员:

  • Blob b; (可能是资源管理类,拷贝和移动都很昂贵)
  • string s; (同样拷贝和移动)
    你为了支持不同的构造情形,写了四个构造函数,分别处理不同的拷贝/移动参数组合:
cpp 复制代码
Big(const Blob &b2, const string &str)  // 两个都拷贝
Big(Blob &&b2, string &&str)            // 两个都移动
Big(const Blob &b2, string &&str)       // 第一个拷贝,第二个移动
Big(Blob &&b2, const string &str)       // 第一个移动,第二个拷贝

如果成员再多,这种构造函数的数量将成指数增长,代码维护和扩展非常痛苦。

这个问题的核心:

  • 对多个成员,每个成员都可能有拷贝和移动的参数版本
  • 构造函数为了支持不同参数组合,需要写很多重载

解决思路(C++11及以后):

  1. 使用完美转发(perfect forwarding)和模板构造函数
    用模板构造函数统一处理所有组合,结合std::forward完美转发参数,避免写大量重载。
  2. 成员的统一构造方式
    在成员的构造调用中使用std::forward传递参数,避免多余的拷贝。
    示例简化:
cpp 复制代码
class Big {
public:
    template<typename B, typename S>
    Big(B&& b2, S&& str)
        : b(std::forward<B>(b2)), s(std::forward<S>(str)) {}
private:
    Blob b;
    std::string s;
};

这样,构造函数模板会根据传入的参数类型,自动决定调用拷贝还是移动构造,避免了大量的显式重载。

这段代码展示了**完美转发(perfect forwarding)**的经典用法:

cpp 复制代码
class Big {
public:
    template<typename T1, typename T2>
    Big(T1&& b2, T2&& str) 
        : b(std::forward<T1>(b2)),    // 保留传入参数是左值还是右值
          s(std::forward<T2>(str))    // 同上
    {}
private:
    Blob b;
    std::string s;
};

核心点:

  • T1&&T2&&通用引用(universal references),能接受左值和右值参数。
  • std::forward<T>(arg) 会根据模板参数 T 的推断情况,决定把 arg 当作左值还是右值转发。
  • 这样,成员对象 bs 会根据传进来的参数的实际类型,调用它们的拷贝构造或移动构造。
  • 这种写法避免了大量的构造函数重载,且效率更高。

原则:只有当移动比复制更高效时,才应该实现移动构造函数和移动赋值运算符。

  • 大多数 C++11 标准库组件都支持移动操作,比如 std::vector, std::string 等,能在内部用移动来提升性能。
  • 有些类型只支持移动(比如 std::unique_ptr),不支持复制,这样避免了潜在的资源管理问题。

"五法则"(The Rule of 5)

随着 C++11 引入移动语义,传统的"三法则"(Rule of 3)扩展为"五法则":

如果一个类需要自定义以下任意一个:

  1. 析构函数 ~T()
  2. 拷贝构造函数 T(const T&)
  3. 拷贝赋值运算符 T& operator=(const T&)
  4. 移动构造函数 T(T&&)
  5. 移动赋值运算符 T& operator=(T&&)
    那通常你应该同时定义这五个,保证资源正确管理和性能最优。

你说的内容是关于 C++11 中著名的 "五法则"(Rule of 5) ,它是对传统 "三法则"(Rule of 3) 的扩展:

Rule of 5 要点总结:

  • 传统的 Rule of 3 包括:
    1. 拷贝构造函数
    2. 拷贝赋值运算符
    3. 析构函数
  • C++11 引入移动语义后,扩展成 Rule of 5,还包含:
    4. 移动构造函数
    5. 移动赋值运算符

具体规则:

  • 如果你显式声明了其中任意一个(哪怕用 = default= delete):
    • 你应该显式声明这五个函数,保证资源管理的正确性和一致性。
  • 如果声明了其中任意一个,编译器不会自动生成移动操作(移动构造和移动赋值)。
  • 但是,编译器仍可能隐式生成拷贝操作。
  • 需要注意的是,C++11 标准中这部分行为已经被标记为废弃,未来可能会调整。

结论:

良好的 C++11 风格是,显式管理这五个特殊成员函数,避免编译器的隐式生成带来的意外行为。

C++14 引入的广义捕获(Generalized Lambda Capture) 特性,主要解决了 C++11 中 lambda 捕获的限制。

主要内容总结:

  • C++11 Lambda 捕获只能按值或按引用捕获已有变量 ,无法捕获移动语义的对象(比如 unique_ptr),导致无法方便地捕获"移动-only"的类型。
  • C++14 的广义捕获允许在捕获列表里初始化捕获变量,即不仅捕获已有变量,还能用任意表达式初始化,支持移动语义。
  • 捕获的变量是lambda内部新创建的变量,和外部变量是两个独立的对象。

你给的例子说明:

cpp 复制代码
auto ptr = std::make_unique<int>(10);  
auto lambda = [ptr = std::move(ptr)] { return *ptr; };
  • 这里,ptr = std::move(ptr) 表示在 lambda 捕获列表中声明了一个新变量 ptr,用外部的 ptr 通过移动构造初始化。
  • 这样,lambda 拥有了对 unique_ptr<int> 的独立所有权。
  • 该 lambda 返回所指向的值,即 *ptr
相关推荐
Fanxt_Ja2 小时前
【JVM】三色标记法原理
java·开发语言·jvm·算法
萌新小码农‍2 小时前
Spring框架学习day7--SpringWeb学习(概念与搭建配置)
学习·spring·状态模式
蓝婷儿2 小时前
6个月Python学习计划 Day 15 - 函数式编程、高阶函数、生成器/迭代器
开发语言·python·学习
行云流水剑2 小时前
【学习记录】深入解析 AI 交互中的五大核心概念:Prompt、Agent、MCP、Function Calling 与 Tools
人工智能·学习·交互
love530love2 小时前
【笔记】在 MSYS2(MINGW64)中正确安装 Rust
运维·开发语言·人工智能·windows·笔记·python·rust
一弓虽2 小时前
zookeeper 学习
分布式·学习·zookeeper
苗老大2 小时前
MMRL: Multi-Modal Representation Learning for Vision-Language Models(多模态表示学习)
人工智能·学习·语言模型
南郁2 小时前
007-nlohmann/json 项目应用-C++开源库108杰
c++·开源·json·nlohmann·现代c++·d2school·108杰
slandarer3 小时前
MATLAB | 绘图复刻(十九)| 轻松拿捏 Nature Communications 绘图
开发语言·matlab