把博客园图标替换成自己的图标
把博客园图标替换成自己的图标end

AIGC标识 C++的拓展语法(goto、lambda表达式、reserve()、iota()、move()、lower_bound()、upper_bound())

goto的作用

记住:

happy:
...
goto happy:

反着也行。

通常用于过多循环判错解的情况:

for ...
    for ...
        for ...
            if (!check()) goto wrong;
            ...
wrong:
puts("NO");

只能在同一个函数使用,即使你用lambda表达式也不行。不能跳过变量的初始化。

lambda表达式

定义

形如:[capture](parameters) -> return_type { body }

  • [capture]:捕获列表,用于捕获当前作用域中的变量。
  • (parameters):参数列表,与普通函数相同。
  • -> return_type:返回类型(可省略,由编译器推导)。
  • { body }:函数体。

用法

最简单的用法:

#include <iostream>
int main() {
    auto greet = []() {
        std::cout << "Hello from lambda!" << std::endl;
    };
    greet(); // 调用
    return 0;
}

带参数:

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

std::cout << add(3, 4) << std::endl; // 输出: 7

如果返回类型可以推导可以省略 ->int,但确保可以被编译器自动推导。

按值捕获:

int x = 10;
auto func = [x]() { // 按值捕获
    std::cout << "x = " << x << std::endl;
};
func(); // 输出: x = 10
写法 含义
[] 不捕获任何变量
[x] 按值捕获 x
[&x] 按引用捕获 x
[x, &y] 混合捕获
[=] 按值捕获所有局部变量
[&] 按引用捕获所有局部变量

⚠️ 注意:[=] 和 [&] 是“全量捕获”,适用于简单情况,但可能带来性能或生命周期问题。

如果用捕获(不引用)不想使外部被修改需要加上 mutable

int x = 10;
auto f = [x]() mutable {
    x += 5;           // 修改的是副本,不影响外部
    return x;
};
std::cout << f() << std::endl; // 15
std::cout << x << std::endl;   // 10(外部不变)

注意,如果要 -> 请记住:

[捕获](参数) mutable -> 返回类型 { 函数体 }

修改被捕获变量(需使用引用)

int count = 0;
auto inc = [&count]() { // 按引用捕获
    ++count;
};

inc(); inc();
std::cout << count << std::endl; // 输出: 2

作为STL算法的参数(一般也用于sort中的cmp)

#include <vector>
#include <algorithm>
#include <iostream>

int main() {
    std::vector<int> nums = {1, 2, 3, 4, 5};

    // 找出大于3的元素
    auto it = std::find_if(nums.begin(), nums.end(), [](int n) {
        return n > 3;
    });

    if (it != nums.end())
        std::cout << *it << std::endl; // 输出: 4

    return 0;
}

1. mutable 关键字——修改按值捕获的副本(前面讲过)

按值捕获的变量在 lambda 内部是只读的(类似 const)。如果想修改其副本而不影响外部,需要加上 mutable

int x = 10;
auto f = [x]() mutable {
    x += 5;           // 修改的是副本
    return x;
};
std::cout << f() << std::endl; // 15
std::cout << x << std::endl;   // 10(外部不变)

2. 捕获初始化(C++14 起)

可以在捕获列表中定义并初始化新变量,常用于移动语义:

std::unique_ptr<int> p = std::make_unique<int>(42);
auto f = [p = std::move(p)]() {  // 将 p 移动进 lambda
    return *p;
};
// 原 p 已被移走,不能再使用

3. 泛型 Lambda(C++14 起)

参数可以用 auto,自动生成模板化的函数对象:

auto add = [](auto a, auto b) { return a + b; };
std::cout << add(3, 4) << std::endl;       // int
std::cout << add(3.14, 2.72) << std::endl; // double

4. 生命周期与悬挂引用(重要警告)

按引用捕获局部变量后,如果 lambda 的生命周期超过了变量的生命周期(比如被存储为回调),将产生悬挂引用,导致未定义行为:

std::function<int()> getFunc() {
    int x = 42;
    return [&x]() { return x; }; // ❌ 危险:x 被销毁,lambda 引用失效
}

解决方法

  • 按值捕获 [=][x]
  • 若必须按引用,确保 lambda 在变量销毁前使用(如在同一作用域内立即调用)。

5. std::function vs auto 的性能考量

  • auto 存储具体类型,无类型擦除开销,编译器可以内联,性能最好。
  • std::function 可以存储任意可调用对象,但会引入运行时开销(类型擦除、虚函数调用)。

建议:绝大多数情况用 auto,除非你需要将 lambda 存入容器或作为多态接口。

6. 混合默认捕获与显式捕获

你可以组合使用默认捕获和显式捕获,注意顺序:

[=, &y]   // 默认按值捕获所有,但 y 按引用
[&, x]    // 默认按引用捕获所有,但 x 按值

7. constexpr Lambda(C++17 起)

如果 lambda 满足编译期求值要求,可以用于 constexpr 上下文:

constexpr auto square = [](int n) { return n * n; };
static_assert(square(5) == 25);

8. 捕获 this 的注意事项

在类成员函数中使用 lambda 时,如果要用到成员变量,需要捕获 this

class Foo {
    int val = 10;
public:
    void bar() {
        auto f = [this]() { return val; }; // 显式捕获 this
        // 或者 [=] 在 C++20 前也会隐式捕获 this(有风险),建议显式写
    }
};

最佳实践:避免使用 [=] 隐式捕获 this(C++20 已修正),明确写出 [this][&]


总结小贴士

  • goto:在简单多层跳出时可用,但更推荐封装函数;注意不能跳越初始化。
  • Lambda:掌握基础捕获后,多练习 mutable、泛型、捕获初始化等现代特性,注意生命周期问题,优先用 auto 而非 std::function 以提升性能。

reserve(),iota(),move()

1. reserve() —— 容器的“内存预约”(后面要使用 push_back() 等)

  • 作用:为容器(如 vectorstring)预先分配足够的底层内存,只改变 capacity(容量),不改变 size(大小)
  • 好处:减少动态扩容带来的内存重新分配和元素拷贝/移动开销,提升性能。
#include <vector>
std::vector<int> v;
v.reserve(100);        // 预分配 100 个 int 的内存
std::cout << v.size() << ", " << v.capacity(); // 输出: 0, 100

for (int i = 0; i < 100; ++i) {
    v.push_back(i);    // 期间不会再触发扩容,效率很高
}
对比 reserve(n) resize(n)
改变 size ❌ 不改 ✅ 改成 n
改变 capacity ✅ 至少为 n ✅ 可能扩容
默认初始化元素 ❌ 不创建元素 ✅ 会创建或删除元素

⚠️ 注意reserve() 只适用于支持容量概念的容器(vectorstringdeque 不完全保证),对 listmap 等节点式容器无效(它们不需要预留连续内存)。


2. iota() —— 连续序列填充器

  • 头文件<numeric>
  • 作用:用从起始值开始的连续递增序列填充一个区间(左闭右开)。
  • 本质*first = value; ++value; 循环执行。
#include <numeric>
#include <vector>
#include <iostream>

std::vector<int> arr(5);
std::iota(arr.begin(), arr.end(), 0);  // arr 变成 {0, 1, 2, 3, 4}

// 也可以不从 0 开始
std::iota(arr.begin(), arr.end(), 10); // arr 变成 {10, 11, 12, 13, 14}

常用场景

  • 快速生成测试数据。
  • 配合 reserve() 和下标赋值,避免 push_back 的重复检查。
  • 用于离散化、并查集初始化等算法(如 parent[i] = i)。

对比手动 for 循环

// 传统写法
for (int i = 0; i < n; ++i) arr[i] = i;
// iota 更简洁、语义清晰,且内部高度优化
std::iota(arr.begin(), arr.end(), 0);

3. move() —— 小心!它有两个“分身”

(1) std::move(类型转换,头文件 <utility>

  • 作用:将左值强制转换为右值引用,暗示“这个对象可以转移资源”,从而触发移动构造函数或移动赋值运算符。
  • 目的:实现资源所有权的转移,避免深拷贝。
#include <utility>
#include <vector>
#include <string>

std::string str1 = "hello";
std::string str2 = std::move(str1); // str1 的资源被“偷”给了 str2
// 此时 str1 处于“有效但未指定”的状态(通常为空),再次访问需小心
std::cout << str2; // 输出 "hello"

⚠️ 致命误区std::move 本身不移动任何东西,它只是一个类型转换!真正的移动发生在构造函数或赋值运算符中。

(2) std::move(范围搬运算法,头文件 <algorithm>

  • 作用:将一段范围内的元素移动到另一段范围(类似于 std::copy,但调用的是移动赋值)。
  • 返回值:指向目标区间末尾的迭代器。
#include <algorithm>
#include <vector>

std::vector<int> src = {1, 2, 3};
std::vector<int> dst(3);
std::move(src.begin(), src.end(), dst.begin()); // dst 变成 {1, 2, 3}
// 注意:src 中的元素被移走后,仍处于有效但未指定状态(通常是原始值,但不保证)

注意区分

  • std::move(转换)通常用于单个对象。
  • std::move(算法)用于容器之间的批量搬运。

移动后的对象(重要警告!)

被移动后的对象(无论是单个还是容器元素)都处于 “有效但未指定” 的状态:

  • 你可以对它调用不依赖当前值的操作,比如 clear()size()、析构。
  • 不要直接读取它的值(除非你确定它被重置为了什么,比如标准库保证 std::string 移后为空,但自定义类不一定)。

安全示范

std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1);
// v1 的大小可能变为 0,但不保证
v1.clear();      // ✅ 安全,clear() 不依赖内容
// std::cout << v1[0]; // ❌ 危险!不要这样做

三者的巧妙组合(实战小彩蛋)

这三个工具经常可以配合使用,写出高效的初始化代码:

#include <vector>
#include <numeric>
#include <utility>

std::vector<int> makeData(int n) {
    std::vector<int> v;
    v.reserve(n);              // 1. 预留空间,避免扩容
    // 这里假设你要生成 0~n-1,可以:
    // 方法一:直接 resize + iota
    // v.resize(n);
    // std::iota(v.begin(), v.end(), 0);
    
    // 方法二:如果不想先初始化两次,可以用移动语义从临时数组转移(略复杂)
    return v; // 返回时自动触发移动(RVO/移动语义),效率极高
}

总结速查表

函数 头文件 核心用途 易错点
reserve() <vector> 预分配内存,优化性能 别和 resize() 混淆,它不创建元素
iota() <numeric> 填充递增序列 记得包含 <numeric>,别写成 itoa(那是 C 库函数)
std::move(转换) <utility> 启用移动语义,避免拷贝 移动后对象不可直接使用原值
std::move(算法) <algorithm> 批量移动范围元素 目标区间大小要足够,移动后源区间状态不确定

两个问题

第一个问题:为什么 cout << v1[0] 不对?

先回顾我之前的示例代码:

std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1);
// std::cout << v1[0]; // ❌ 为什么危险?

根本原因:被移动后的对象处于“有效但未指定”的状态。

具体到 std::vector,大多数标准库(如 GCC 的 libstdc++ 和 MSVC)在实现移动构造函数时,内部是这样做的:

  1. v2 的内部指针指向 v1 的堆内存。
  2. v1 的内部指针置为 nullptr,并把 v1sizecapacity 置为 0

所以当你执行 v1[0] 时,相当于解引用一个空指针(nullptr[0]),这属于未定义行为(UB),程序大概率会直接崩溃(段错误)。

但是要注意区分

  • 对于 std::vector,移完后访问元素肯定错,因为实现会把原容器掏空。
  • 对于 std::string,标准库通常保证移完后字符串为空(""),此时 str[0] 是安全的(返回 '\0')。
  • 对于自定义类,移完后到底剩什么完全取决于你怎么写移动构造函数。

安全原则:永远不要假设被移动后对象的值,你可以安全地调用 size()clear() 或析构它,但绝对不要直接读取其内部元素,除非你查阅了该类型的标准库保证文档。


第二个问题:std::move 算法是什么?

你注意到了,std::move 有两个完全不同的身份:

1. 类型转换(头文件 <utility>

就是刚才用的 std::move(v1),它只是一个“演员”,把左值变成右值引用,触发移动构造/赋值。

2. 范围搬运算法(头文件 <algorithm>

这是你问的“算法”,它的声明是:

template<class InputIt, class OutputIt>
OutputIt move(InputIt first, InputIt last, OutputIt d_first);
  • 作用:将 [first, last) 区间内的元素,一个一个地移动到从 d_first 开始的目标区间。
  • 内部原理:循环中执行 *d_first = std::move(*first)(注意这里调用的是 std::move 类型转换,从而触发元素的移动赋值运算符)。

代码示例

#include <algorithm>
#include <vector>
#include <string>
#include <iostream>

int main() {
    std::vector<std::string> src = {"apple", "banana", "cherry"};
    std::vector<std::string> dst(3);  // 必须提前分配好空间!

    // 算法版 move:把 src 里的字符串“偷”到 dst 中
    std::move(src.begin(), src.end(), dst.begin());

    // 输出 dst:apple, banana, cherry
    for (const auto& s : dst) std::cout << s << ", ";
    
    // src 中的字符串现在处于“未指定”状态(通常是空字符串 "")
    for (const auto& s : src) std::cout << "[" << s << "] "; // 可能输出 [][][] 
}

std::copy 算法的区别

  • std::copy:保留 src 原封不动(深拷贝)。
  • std::move(算法):把 src 的元素资源偷走,src 失去原值(浅拷贝指针 + 置空对方)。

⚠️ 算法版 std::move 的两大致命陷阱

陷阱 1:目标空间必须足够大(但是搞到函数参数中不用,直接fun(move(ls)))
std::copy 一样,std::move 算法不会自动扩容。你必须确保 dstsize 大于等于 src 的元素个数。

std::vector<int> src = {1,2,3};
std::vector<int> dst; 
std::move(src.begin(), src.end(), dst.begin()); // ❌ 崩溃!dst 是空的

正确做法:要么 dst.resize(3),要么用 std::back_inserter(但 back_inserter 调用的是 push_back,同样会触发移动,可行):

std::move(src.begin(), src.end(), std::back_inserter(dst)); // ✅ 自动扩容

陷阱 2:源和目标不能重叠(范围重叠)
如果源区间和目标区间在同一个容器中且重叠,std::move 算法结果是未定义的(可能用后面覆盖前面,乱掉)。如果你想把元素移动到重叠的右侧位置,请使用 std::move_backward(和 copy_backward 同理)。

std::vector<int> v = {1, 2, 3, 4, 5};
// 想把前 3 个移动到后 3 个位置(覆盖 3,4,5)
// std::move(v.begin(), v.begin()+3, v.begin()+2); // ❌ 危险!重叠了
std::move_backward(v.begin(), v.begin()+3, v.end()); // ✅ 从后往前移动,安全
// v 可能变成 {1, 2, 1, 2, 3}

总结两个 move 的口诀

写法 头文件 功能 用途
std::move(obj) <utility> 类型转换(变左值为右值) 单个对象转移所有权
std::move(begin, end, out) <algorithm> 批量搬运元素(调用移动赋值) 容器间或容器内的批量转移

使用(后缀数组)

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <stdlib.h>
#include <vector>
#define int long long
#define uint unsigned long long
#define N 300005
using namespace std;
char s[N];
uint seed = 231;
uint h[N],Pow[N];
int rk[N],sa[N],height[N];
signed main(){
	scanf("%s",s + 1);
	int n = strlen(s + 1);
	Pow[0] = 1;
	s[n + 1] = '$';
	for (int i = 1;i <= n;i ++) Pow[i] = Pow[i - 1] * seed;
	for (int i = 1;i <= n;i ++) h[i] = h[i - 1] * seed + s[i];
	for (int i = 1;i <= n;i ++) sa[i] = i;
	auto gethash = [&](int l,int r) {
		return h[r] - h[l - 1] * Pow[r - l + 1];
	};
	stable_sort(sa + 1,sa + 1 + n,[n,gethash](const int &a,const int &b) {
		int lena = n - a + 1,lenb = n - b + 1;
		int l = 1,r = min(lena,lenb),res = r + 1;
		while(l <= r) {
			int mid = l + r >> 1;
			if (gethash(a,a + mid - 1) != gethash(b,b + mid - 1)) res = mid,r = mid - 1;
			else l = mid + 1;
		}
		if (res == min(lena,lenb) + 1) return lena < lenb;
		return s[a + res - 1] < s[b + res - 1];
	});
	auto print = [](vector<int> a) -> void {
		for (auto i : a) printf("%lld ",i);
		putchar('\n');
	};
	vector<int> ans;
	ans.reserve(n);
	for (int i = 1;i <= n;i ++) ans.push_back(sa[i] - 1);
	print(move(ans));
	for (int i = 1;i <= n;i ++) rk[sa[i]] = i;
	for (int i = 1,k = 0;i <= n;i ++) {
		if (k) k --;
		while(s[i + k] == s[sa[rk[i] - 1] + k]) k ++;
		height[rk[i]] = k;
	}
	ans.reserve(n);
	for (int i = 1;i <= n;i ++) ans.push_back(height[i]);
	print(move(ans));
	return 0;
}

注意到,stable_sort()什么时候比sort()更快呢?当且仅当比较操作比较昂贵的时候,其他情况时空均会更大。

lower_bound()、upper_bound()

在 C++ 标准库中,lower_boundupper_bound 的比较函数必须基于严格弱序(通常用 <,不能直接使用 <=>,否则会破坏二分查找的正确性。


1. 标准库的约定

函数 比较器调用方式 语义
lower_bound(first, last, value, comp) comp(*it, value) 查找第一个 不小于 value 的元素(即 *it >= value
upper_bound(first, last, value, comp) comp(value, *it) 查找第一个 大于 value 的元素(即 *it > value

默认的比较器是 std::less<>,即使用 <


2. 为什么是 < 而不是 <=>

  • 二分查找依赖“小于”关系的严格弱序(传递性、非对称性)。
  • 如果用 <=,则当两个元素相等时,comp(a,b)comp(b,a) 都可能为 true,破坏了严格弱序,导致二分查找错误。
  • 因此,比较器内部通常只使用 <(或自定义等价关系),并通过组合来实现 <=>=> 的效果。

3. 如何实现不同的查找目标?

假设元素类型为 T,值类型为 V

想要查找的目标 使用函数 比较器写法
第一个 >= value 的位置 lower_bound [](const T& elem, const V& val) { return elem < val; }(默认)
第一个 > value 的位置 upper_bound [](const V& val, const T& elem) { return val < elem; }(默认)
第一个 <= value 的位置(即最后一个 <= value 的后继) upper_bound 并自定义 [](const V& val, const T& elem) { return val < elem; } 仍然是 >,但将 value 改为 value+1 或利用 upper_bound 配合 greater 等,但更简单的做法是使用 lower_bound 配合反向迭代器。
第一个 < value 的位置(即第一个 >= value 的位置减1) lower_bound,然后 --it 用默认 lower_bound 找到 >= value,其前一个就是 < value 的最后位置。

注意upper_bound 默认查找 > value,但通过修改比较器可以让它查找其他条件,但必须保证严格弱序。最常见的是保持默认 value < *it,因为它已经实现了严格 >


4. 您代码中的用法(分块)

  • lower_bound(qid.begin(), qid.end(), val, [&](int id, int val){ return q[id].a < val; })
    这里比较器是 comp(element, value),即 q[id].a < val,正是 lower_bound 的标准写法,查找第一个 a >= val 的询问编号。

  • upper_bound(edges+L, edges+R+1, val, [](int val, const Edge& e){ return val < e.a; })
    这里比较器是 comp(value, element),即 val < e.a,正是 upper_bound 的标准写法,查找第一个 a > val 的边。

如果误写成 e.a < val,则相当于 lower_bound 的逻辑,会得到第一个 a >= val,而不是 a > val,导致块内多包含一条 a == val 的边,从而漏判或误判。


5. 总结

  • 始终使用 <(或自定义的严格弱序)作为比较核心。
  • lower_boundcomp(element, value) 实现 element < value
  • upper_boundcomp(value, element) 实现 value < element
  • 不要尝试直接用 <=>=,可以通过反转比较或调整查找值来达到目的。
posted @ 2026-07-20 20:28  high_skyy  阅读(5)  评论(0)    收藏  举报
动态线条
动态线条end
浏览器标题切换
浏览器标题切换end
💬 加载中……