← All notes

C++11\14特性

Table of contents

C++11特性

1. 自动类型推导 (auto)

C++11 引入了 auto 关键字,允许编译器根据表达式的类型自动推导变量的类型。它可以简化代码,特别是在处理复杂的类型时。

auto x = 5;      // 编译器推导 x 为 int
auto y = 3.14;   // 编译器推导 y 为 double
auto ptr = new int(10);  // 推导为 int*

推导规则看CppLearning.pdf。

2. 范围 for 循环(Range-based for loop)

C++11 引入了范围 for 循环,简化了对容器或数组元素的迭代操作。

std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto& v : vec) {
    std::cout << v << std::endl;
}

3. 智能指针 (shared_ptr, unique_ptr, weak_ptr)

智能指针本质是一个封装了一个原始C++指针的类模板,为了确保动态内存的安全性而产生的。 C++11 提供了新的智能指针,用于更好地管理动态内存:

  • std::unique_ptr:独占所有权。
  • std::shared_ptr:共享所有权,使用引用计数来管理对象的生命周期。
  • std::weak_ptr:用于打破 shared_ptr 之间的循环引用。
std::shared_ptr<int> p1 = std::make_shared<int>(10);
std::unique_ptr<int> p2 = std::make_unique<int>(20);

在 C++ 的早期,auto_ptr 是标准库中唯一提供的智能指针,用于自动管理动态分配的内存。然而,auto_ptr 有一些设计缺陷,尤其是所有权的转移行为不直观、不安全,因此在 C++11 引入了新的智能指针类型,如 unique_ptrshared_ptrweak_ptr,并逐渐淘汰了 auto_ptr

接下来,我们详细讨论从 auto_ptr 到 C++11 的智能指针演变。

1. auto_ptr(C++98)

auto_ptr 是 C++98 标准中的智能指针,用来管理动态分配的对象。它的主要目的是在对象离开作用域时自动释放动态分配的内存,避免内存泄漏。

1.1 基本用法
#include <memory>

int main() {
    std::auto_ptr<int> p1(new int(10));  // 动态分配内存并绑定到 auto_ptr
    std::auto_ptr<int> p2 = p1;          // 转移所有权,p1 不再持有对象

    std::cout << *p2 << std::endl;       // 输出 10
    // p1 现在是空指针,持有 nullptr
}
1.2 auto_ptr 的问题
  • 所有权转移auto_ptr 在拷贝和赋值时会转移资源的所有权。例如,auto_ptr p2 = p1; 会将资源从 p1 转移到 p2,而 p1 被置为空。这种所有权转移是不直观的,容易导致悬空指针或双重释放的问题。
  • 不能用于容器:由于 auto_ptr 拷贝时会改变源对象的状态,它无法安全地用于 STL 容器(如 std::vector)。容器通常会在元素的插入和删除过程中进行拷贝操作,使用 auto_ptr 会导致意外的行为。
  • 已经废弃:由于这些问题,auto_ptr 在 C++11 被标记为废弃,并在 C++17 中完全移除。

2. unique_ptr(C++11)

unique_ptr 是 C++11 引入的智能指针,旨在替代 auto_ptr。它也是一种独占所有权的智能指针,但相比 auto_ptrunique_ptr 更加安全和高效。

2.1 特点
  • 独占所有权unique_ptr 独占所管理的资源,不能被拷贝,但可以通过移动语义(move semantics)转移所有权。
  • 轻量高效:相比于 shared_ptrunique_ptr 没有额外的引用计数开销。
  • 自定义删除器unique_ptr 支持自定义删除器,可以管理复杂资源(如文件句柄、网络连接等)。
2.2 基本用法
#include <memory>

int main() {
    std::unique_ptr<int> p1(new int(20));       // 创建 unique_ptr,管理动态分配的内存
    std::unique_ptr<int> p2 = std::move(p1);    // 使用 std::move 转移所有权

    std::cout << *p2 << std::endl;              // 输出 20
    // p1 现在是空的,不能再使用它
}
2.3 auto_ptr 的区别
  • 不能拷贝:与 auto_ptr 不同,unique_ptr 不能通过复制构造或赋值操作来拷贝。必须使用 std::move 将所有权从一个 unique_ptr 转移到另一个。这避免了 auto_ptr 的潜在所有权混乱问题。

  • 自定义删除器unique_ptr 可以接受自定义删除器来释放资源,适用于更复杂的资源管理场景。

std::unique_ptr<FILE, decltype(&fclose)> filePtr(fopen("test.txt", "w"), fclose);
2.4 应用场景
  • 用于确保资源在离开作用域时被正确释放,避免内存泄漏。
  • unique_ptr 适用于独占资源的场景,如管理文件、网络连接或设备句柄等。

3. shared_ptr(C++11)

shared_ptr 是一种支持共享所有权的智能指针,多个 shared_ptr 可以共同管理同一个对象。对象的生命周期会延长,直到最后一个 shared_ptr 释放它。

3.1 特点
  • 引用计数shared_ptr 内部维护一个引用计数,当引用计数为 0 时,所管理的对象会被自动销毁。
  • 拷贝安全shared_ptr 可以被拷贝,拷贝时引用计数增加。
  • 线程安全:共享同一个控制块的不同 shared_ptr 实例可以被多个线程并发操作;具体边界见 3.7。
3.2 基本用法
#include <memory>

int main() {
    std::shared_ptr<int> p1 = std::make_shared<int>(30);   // 推荐的创建方式
    std::shared_ptr<int> p2 = p1;                          // 拷贝 p1,引用计数增加

    std::cout << *p1 << std::endl;                         // 输出 30
    std::cout << "Use count: " << p1.use_count() << std::endl;  // 引用计数输出 2
}
3.3 shared_ptrunique_ptr 的区别
  • 共享所有权shared_ptr 的设计目的是让多个智能指针共享同一个对象。当不再需要对象时,引用计数归零,自动销毁对象。相比之下,unique_ptr 独占对象,不能共享。

  • 性能开销:由于 shared_ptr 需要维护引用计数,它比 unique_ptr 有更大的内存和性能开销。如果没有必要的共享行为,应该优先使用 unique_ptr

3.4 自定义删除器

unique_ptr 类似,shared_ptr 也支持自定义删除器:

std::shared_ptr<FILE> filePtr(fopen("test.txt", "w"), fclose);
3.5 应用场景
  • 当多个对象需要共享同一个资源时,可以使用 shared_ptr。例如,一个对象在多个模块中被使用,所有模块都应该控制其生命周期。
  • 当对象的强引用计数降为0时,shared_ptr会释放该对象的内存,但控制块不会立即释放,因为仍然有weak_ptr可能在观察它。控制块会在弱引用计数也降为0时释放,这样可以确保weak_ptr可以安全地判断对象是否有效。
3.6 shared_ptr 的析构过程

shared_ptr 的具体数据结构和函数实现由标准库决定,C++ 标准只规定它的行为。常见实现中,一个 shared_ptr 通常保存两个关键状态:

  • 存储指针get() 返回、operator* 解引用的指针;
  • 控制块指针:指向控制块。控制块保存强引用计数、弱引用计数、删除器以及分配器等信息。

存储指针不一定就是最终被删除的指针,例如别名构造(aliasing constructor)可以让二者不同。因此,最后释放资源时调用的是控制块中保存的删除器,而不是简单地对 get() 执行 delete

下面是一个忽略继承、异常处理和平台优化后的伪实现。许多实现会让弱引用计数额外包含一个“强引用仍存在”的隐式弱引用:

struct ControlBlock {
    std::atomic<long> strong_count; // shared_ptr 的数量
    std::atomic<long> weak_count;   // weak_ptr 的数量 + 隐式弱引用

    void dispose(); // 调用删除器,销毁被管理对象
    void destroy(); // 释放控制块自身
};

void release_shared(ControlBlock* cb) noexcept {
    if (cb == nullptr) {
        return;
    }

    // 原子减一;只有观察到 1 -> 0 的线程负责销毁对象
    if (cb->strong_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
        cb->dispose();

        // 对象销毁后,释放控制块持有的隐式弱引用
        if (cb->weak_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
            cb->destroy();
        }
    }
}

template <typename T>
shared_ptr<T>::~shared_ptr() noexcept {
    release_shared(control_block_);
}

因此,一个非空 shared_ptr 析构时大致经历以下步骤:

  1. 原子地将控制块中的强引用计数减一。
  2. 如果减一后仍大于 0,说明还有其他 shared_ptr 拥有对象,本次析构到此结束。
  3. 如果当前线程使强引用计数从 1 变为 0,它就调用控制块中的删除器,销毁被管理对象。删除器可能是 delete,也可能是用户提供的自定义删除器。
  4. 对象销毁后再减少隐式弱引用;只有弱引用计数也归零时,才释放控制块。

std::make_shared 通常会把对象和控制块放在同一次内存分配中,但二者的生命周期仍然分开:强引用归零时先执行对象的析构函数,弱引用归零时才真正回收包含控制块的那块内存。这也是对象已经销毁后,weak_ptr::expired()weak_ptr::lock() 仍能安全判断对象状态的原因。

注意:上面的代码用于解释原理,并不是标准规定或某个标准库源码的逐行复制。不同实现可能使用专门的原子指令、内存屏障或其他计数布局。

3.7 为什么说 shared_ptr 是线程安全的

shared_ptr 的线程安全来自共享控制块中的计数操作具有原子性和必要的同步保证。多个线程各自持有不同的 shared_ptr 实例,但这些实例共享同一个控制块时,可以并发地执行拷贝、赋值和析构:

  • 拷贝 shared_ptr 时,强引用计数被原子地增加;
  • 析构或 reset() 时,强引用计数被原子地减少;
  • 对于最后一次释放,原子的“减一并返回旧值”操作保证只有一个线程能观察到 1 -> 0,因此被管理对象只会被销毁一次;
  • 控制块同样会在最后一个弱引用消失后只释放一次。

不过,“shared_ptr 是线程安全的”有严格的边界:

  1. 不同的 shared_ptr 实例可以在不同线程中并发修改,即使它们管理的是同一个对象。
  2. 同一个 shared_ptr 实例不能在没有同步的情况下被多个线程并发修改。例如,一个线程执行 reset(),另一个线程同时对同一个变量赋值,会产生数据竞争。C++11/14 中可使用互斥锁,或者始终通过 std::atomic_loadstd::atomic_store 等针对 shared_ptr 的自由函数访问它;std::atomic<std::shared_ptr<T>> 是 C++20 才提供的。
  3. 被管理对象本身不因此变得线程安全。多个线程通过各自的 shared_ptr 同时修改对象,仍需要对象自己的锁、原子变量或其他同步机制。
  4. use_count() 只是在调用瞬间取得的计数快照。其他线程可能马上改变计数,因此不要用它来判断“随后是否可以独占访问对象”。

可以把结论概括为:控制块的生命周期管理是线程安全的;同一个智能指针变量的并发写入和所指对象的并发访问,不由 shared_ptr 自动保护。

4. weak_ptr(C++11)

weak_ptr 是一种非拥有(non-owning)的智能指针,它与 shared_ptr 配合使用,用于解决共享对象的循环引用问题。weak_ptr 不影响引用计数。

4.1 特点
  • 弱引用weak_ptr 只是指向对象,并不影响对象的生命周期,不能直接解引用。
  • 循环引用解决:在使用 shared_ptr 时,如果两个对象相互持有对方的 shared_ptr,会导致循环引用,无法释放内存。使用 weak_ptr 可以打破这种循环。
4.2 基本用法
#include <memory>

int main() {
    std::shared_ptr<int> sp = std::make_shared<int>(40);
    std::weak_ptr<int> wp = sp;  // 创建 weak_ptr,不增加引用计数

    if (auto temp = wp.lock()) {  // 使用 lock() 生成 shared_ptr
        std::cout << *temp << std::endl;  // 输出 40
    } else {
        std::cout << "Resource no longer available." << std::endl;
    }
}
4.3 应用场景
  • weak_ptr 通常用于避免循环引用。例如在双向链表、图结构或依赖关系复杂的系统中,shared_ptr 的循环引用问题可以通过 weak_ptr 解决。

shared_ptr 只保证共享控制块的生命周期管理是线程安全的,并不保证被管理对象本身是线程安全的。

4. Lambda 表达式

C++11 引入了 lambda 表达式,使得可以在代码中编写匿名函数,特别适合于函数作为参数的场景,如回调、事件处理等。

auto add = [](int a, int b) { return a + b; };
std::cout << add(1, 2) << std::endl;  // 输出 3

捕获外部变量:

int x = 5;
auto func = [x](int a) { return a + x; };
std::cout << func(3) << std::endl;  // 输出 8

匿名函数本质上是一个对象,在其定义的过程中会创建出一个栈对象,内部通过重载()符号实现函数调用的外表。

优点:使用匿名函数,可以免去函数的声明和定义。这样匿名函数仅在调用函数的时候才会创建函数对象,而调用结束后立即释放,所以匿名函数比非匿名函数更节省空间。

5. 移动语义与右值引用 (rvalue references)

C++11 引入了移动语义和右值引用,允许开发者通过转移资源所有权来避免不必要的拷贝,极大地提升了性能,尤其是对于临时对象的处理。

  • 右值引用通过 T&& 定义,可以捕获右值。对于仍有名字的左值,可以使用 std::move() 将它显式转换为可被移动的右值。
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1);  // v1 的资源被转移给 v2,避免了拷贝

5.1 std::move 到底做了什么

std::move 定义在 <utility> 中。它本身既不搬运内存,也不保证一定调用移动构造函数;它只是把表达式无条件转换为右值(更准确地说是 xvalue),从而允许编译器选择接收方的移动构造函数或移动赋值运算符。

下面是一个简化后的 C++11 风格实现:

template <typename T>
typename std::remove_reference<T>::type&& move(T&& value) noexcept {
    using ReturnType = typename std::remove_reference<T>::type&&;
    return static_cast<ReturnType>(value);
}

因此,真正执行资源转移的是目标类型的移动构造函数或移动赋值运算符,而不是 std::move 本身:

std::string source = "a long string";
std::string target = std::move(source);
// std::move(source) 只负责转换值类别;
// std::string 的移动构造函数负责接管 source 的内部资源。

5.2 std::move 的常见使用场景

1. 转移只能移动、不能拷贝的资源所有权

std::unique_ptrstd::threadstd::futurestd::promisestd::unique_lock 等类型通常禁止拷贝,只允许移动:

#include <memory>

auto p1 = std::make_unique<int>(42);
auto p2 = std::move(p1);

// p1 已经不再拥有对象,p2 接管所有权
assert(p1 == nullptr);

shared_ptr 虽然可以拷贝,但移动时可以直接转移当前智能指针持有的所有权,通常不需要增加再减少引用计数:

auto p1 = std::make_shared<int>(42);
auto p2 = std::move(p1);

assert(!p1);  // 移动后的 shared_ptr 为空
assert(p2);
2. 把大型对象交给另一个对象或容器

对于 std::stringstd::vector 等持有动态资源的对象,确定不再需要原对象内容时,可以移动以避免深拷贝:

std::vector<std::string> names;
std::string name = "Alice";

names.push_back(std::move(name));
// name 仍然是合法对象,但其值处于“有效但未指定”的状态

只有在后续不再依赖原值时才应这样做。如果还要继续读取 name 的原内容,就不应该移动它。

3. 实现类的移动构造函数和移动赋值运算符

右值引用参数虽然类型是 T&&,但只要它有名字,它在函数体中就是左值。因此移动其成员时仍然需要 std::move

class Buffer {
public:
    Buffer(Buffer&& other) noexcept
        : data_(std::move(other.data_)) {}

    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            data_ = std::move(other.data_);
        }
        return *this;
    }

private:
    std::vector<char> data_;
};

移动构造函数尽量声明为 noexcept。例如,std::vector 扩容时通常只有在元素的移动构造函数不会抛出异常时,才会优先移动旧元素,否则可能为了异常安全而选择拷贝。

4. 把对象交给明确接管所有权的函数

如果函数参数按值接收资源,调用方可以用 std::move 明确表示“调用之后我不再需要这个对象”:

void consume(std::unique_ptr<int> value) {
    // consume 独占 value
}

auto p = std::make_unique<int>(42);
consume(std::move(p));

普通对象也经常采用“按值接收,再移动到成员”的写法:

class User {
public:
    explicit User(std::string name)
        : name_(std::move(name)) {}

private:
    std::string name_;
};

这样传入右值时可以一路移动;传入左值时只在进入函数时拷贝一次,随后再把参数移动到成员中。

5. 在 C++14 Lambda 中转移捕获资源

C++14 的初始化捕获可以把只能移动的对象交给 Lambda:

auto p = std::make_unique<int>(42);

auto task = [value = std::move(p)] {
    std::cout << *value << std::endl;
};

task();

这种写法常用于异步任务、回调和线程函数,因为资源的生命周期会跟随 Lambda 对象。

5.3 使用 std::move 时的注意事项

  1. 移动后的对象仍然可以析构和重新赋值,但不要依赖其原值。大多数标准库类型在移动后处于“有效但未指定”的状态;可以对它赋新值、调用 clear() 或让它正常析构。某些类型会提供更强的保证,例如移动后的 unique_ptrshared_ptr 明确为空。

  2. 不要随意对 const 对象使用 std::move移动操作通常需要修改源对象,而 std::move 不会去掉 const,所以这种情况往往仍然调用拷贝构造函数:

    const std::string source = "hello";
    std::string target = std::move(source); // 通常仍然是拷贝
    
  3. 返回局部变量时通常不要写 std::move编译器可以执行返回值优化;即使没有消除构造,局部变量在返回时也可以被隐式移动。显式添加 std::move 反而可能阻止 NRVO:

    std::string make_name() {
        std::string name = "Alice";
        return name;             // 推荐:允许 NRVO 或隐式移动
        // return std::move(name); // 通常没有必要
    }
    
  4. 泛型转发函数通常应该使用 std::forward,而不是无条件使用 std::movestd::move 会把传入的左值也转换成右值;std::forward 则会保留调用者传入参数的原始值类别。

  5. 类型没有移动操作时,写了 std::move 也可能发生拷贝。右值同样可以绑定到 const T&,因此当移动构造函数不存在或不可用时,编译器可能选择拷贝构造函数。

另外,<algorithm> 中还有一个接收三个迭代器的 std::move(first, last, destination) 算法,用于逐个移动一个区间的元素。它与 <utility> 中接收单个对象、负责转换值类别的 std::move(value) 作用不同。

完美转发(Perfect Forwarding)是 C++11 引入的一个特性,允许函数将其接收到的参数(包括其值类型、引用类型和右值或左值属性)完美地转发给另一个函数。这种转发可以保留参数的所有特性,从而避免不必要的拷贝或引用失效问题。

完美转发的核心是右值引用(rvalue references)和模板参数推导,特别是结合了 std::forward 的使用,使得我们可以将函数接收到的参数以最优的方式传递给其他函数。

完美转发依赖于以下几个概念:

  1. 右值引用(Rvalue Reference)
    • 右值引用通过 T&& 语法定义,用来接受右值对象(临时对象),并允许开发者高效地使用和转移这些对象的资源。
    • 对于模板参数中的 T&&,它既能接受右值引用,也能接受左值引用,这是完美转发的基础。
  2. 万能引用(Universal Reference)
    • T&& 作为模板参数出现时,它被称为万能引用,可以同时接受左值引用和右值引用。编译器根据传递的参数类型推导 T 的类型,从而决定是左值引用还是右值引用。
  3. std::forward
    • std::forward 是一个帮助函数,用来在函数内部将参数以原始的类型(左值或右值)转发给另一个函数。它根据模板参数的类型决定是否执行移动语义(move semantics)。
#include <iostream>
#include <utility>  // for std::forward

void process(int& x) {
    std::cout << "Lvalue reference: " << x << std::endl;
}

void process(int&& x) {
    std::cout << "Rvalue reference: " << x << std::endl;
}

template <typename T>
void forwarder(T&& arg) {
    process(std::forward<T>(arg));  // 保持 arg 的值类别
}

int main() {
    int a = 42;
    forwarder(a);        // 转发左值,调用 process(int&)
    forwarder(100);      // 转发右值,调用 process(int&&)
}
Lvalue reference: 42
Rvalue reference: 100

如果没有使用 std::forward,完美转发就无法正常工作。例如:

template <typename T>
void forwarder(T&& arg) {
    process(arg);  // 没有使用 std::forward
}

int main() {
    int a = 42;
    forwarder(a);        // 错误:a 是左值,传递给 process 时应为左值引用
    forwarder(100);      // 错误:100 是右值,但被转发为左值
}

在这段代码中,process(arg) 传递的是 arg,无论 arg 是左值还是右值,都会被视为左值传递。结果是,即使你传递的是右值,它在传递过程中也会丧失右值属性。

std::forward 根据模板参数的类型来决定是否转发为右值:

template<typename T>
T&& forward(typename std::remove_reference<T>::type& t) noexcept {
    return static_cast<T&&>(t);
}
  • 如果 T 是左值引用类型,std::forward<T>(t) 会返回 t 的左值引用。
  • 如果 T 是右值引用类型,std::forward<T>(t) 会将 t 转换为右值引用。

6. 完美转发的注意事项

  1. 引用折叠
    • 在完美转发中,C++11 引入了引用折叠规则,帮助解决模板参数推导过程中多重引用的问题。具体规则如下:
      • T& &T& &&T&& & 都会折叠为 T&(左值引用)。
      • 只有 T&& && 会折叠为 T&&(右值引用)。
  2. 性能问题
    • 完美转发的设计目的是优化性能,减少不必要的拷贝和对象构造。然而,完美转发可能引入额外的复杂性,尤其是在处理移动语义时。
  3. std::forwardstd::move 的区别
    • std::move 总是将其参数转换为右值引用,而 std::forward 会根据参数类型决定是否保留其值类别。因此,std::forward 更适合用于模板参数转发。

6. nullptr

C++11 引入了 nullptr,它取代了传统的 NULL,成为一个类型安全的空指针常量。

int* ptr = nullptr;  // 代替 NULL

7. constexpr

C++11 引入了 constexpr,它允许在编译时计算常量表达式,提高编译期的优化。

constexpr int square(int x) { return x * x; }
constexpr int result = square(5);  // 在编译时计算

8. 委托构造函数(Delegating constructors)

C++11 允许一个构造函数调用另一个构造函数,简化了构造函数的实现。

class MyClass {
public:
    MyClass(int a) : MyClass(a, 0) {}  // 调用另一个构造函数
    MyClass(int a, int b) : x(a), y(b) {}

private:
    int x, y;
};

9. 显式转换运算符(Explicit conversion operators)

C++11 允许将类型转换运算符标记为 explicit,避免隐式转换带来的错误。

class MyClass {
public:
    explicit operator bool() const { return true; }
};

MyClass obj;
if (obj) {  // 只有通过显示的转换,才能将 obj 转换为 bool
    // ...
}

10. std::thread 和多线程支持

C++11 引入了标准库的多线程支持,包括 std::threadstd::mutexstd::lock_guard 等,可以直接使用标准库进行线程创建和同步。

#include <thread>
void threadFunc() {
    std::cout << "Hello from thread" << std::endl;
}

int main() {
    std::thread t(threadFunc);  // 创建一个线程
    t.join();                   // 等待线程结束
}

11. 变参模板(Variadic templates)

C++11 支持变参模板,允许模板接受任意数量的模板参数。

template<typename... Args>
void print(Args... args) {
    (std::cout << ... << args) << std::endl;  // C++17 fold expression
}
print(1, 2.5, "Hello");  // 输出:1 2.5 Hello

12. std::tuple

C++11 引入了 std::tuple,它是一个固定大小的异构集合,允许存储不同类型的对象。

#include <tuple>

std::tuple<int, double, std::string> t(1, 2.5, "Hello");
std::cout << std::get<0>(t) << std::endl;  // 输出 1

13. enum class

C++11 引入了强类型枚举 enum class,它比传统的 enum 更安全,因为它不会隐式转换为整型。

enum class Color { Red, Green, Blue };
Color c = Color::Red;

14. std::array

C++11 提供了 std::array,它是 C++ STL 的静态数组,具有固定大小,并且提供了与 STL 容器相同的接口。

#include <array>

std::array<int, 3> arr = {1, 2, 3};
std::cout << arr[1] << std::endl;  // 输出 2

15. 初始化列表(Initializer Lists)

C++11 引入了初始化列表语法,允许通过大括号 {} 直接初始化对象。

std::vector<int> vec = {1, 2, 3, 4};  // 使用初始化列表

16. decltype

C++11 提供了 decltype,用于推导表达式的类型,类似于 auto,但 decltype 可以用于复杂表达式的类型推导。

int x = 5;
decltype(x) y = 10;  // y 的类型与 x 相同,推导为 int

17. 静态断言(static_assert

C++11 引入了 static_assert,用于在编译时进行条件检查,如果条件为假,编译器会抛出错误。

static_assert(sizeof(int) == 4, "int size is not 4 bytes");

18. 后置返回类型(Trailing Return Types)

C++11 支持在函数定义中使用后置返回类型,特别适用于复杂的返回类型或函数模板。

auto add(int a, int b) -> int {
    return a + b;
}

19. noexcept

C++11 引入了 noexcept,用于标记函数不抛出异常,有助于编译器优化。

void func() noexcept {
    // 函数不抛出异常
}

20. 右值引用与移动构造函数

C++11 提供了右值引用和移动语义,用于避免不必要的对象拷贝,提高性能。移动构造函数和移动赋值运算符可以通过转移资源来避免深拷贝。

class MyClass {
public:
    MyClass(MyClass&& other) noexcept {
        // 实现移动构造函数
    }
    MyClass& operator=(MyClass&& other) noexcept {
        // 实现移动赋值运算符
    }
};

总结

C++11 是 C++ 语言的一次重大更新,带来了许多极大增强语言能力的特性。这些特性不仅简化了编写 C++ 代码的过程,还提供了强大的工具用于提高性能和安全性,使得 C++ 在现代开发中更加高效和实用。