这是 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
),其返回类型取决于模板参数 T
和 U
,但我们无法事先明确知道返回类型是什么。
举例说明:
cpp
template<typename T, typename U>
??? product(const T &t, const U &u)
{
return t * u;
}
你无法提前写出 ???
,因为:
T
和U
可能是不同类型;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::find
和std::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++ 用
0
或NULL
表示空指针,容易造成重载二义性。 - 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,匹配long
比char*
更合适(指针重载被忽略),仍然调用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>::iterator
和list<int>::const_iterator
。 - 使用迭代器遍历,代码比较啰嗦。
- 很容易写成
it != li.end()
出错,比如写成it < li.end()
,特别对新手不友好。
总结问题
- 计算数组长度麻烦,易错。
- 迭代器声明长且复杂。
- 代码冗长,易出现越界或写错条件的错误。
这个地方关注一点就是数组名什么时候不会退化成指针
数组名不会退化成指针的几种典型情况是:
- 用在
sizeof
操作符中
例:sizeof(arr)
计算整个数组大小,不是指针大小。 - 用在取地址操作符
&
时
例:&arr
取的是整个数组的地址,类型是指向数组的指针。 - 用在
decltype
操作符中
例:decltype(arr)
得到的是数组类型。 - 用作初始化列表时
例:int arr[3] = {1, 2, 3};
初始化数组时,数组名不退化。 - 用作模板参数推导时(如
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及之前),写嵌套模板时,两个
>
连着写会被当成右移运算符,必须写成> >
,即中间加空格,例如:cppstd::map<std::string, std::vector<std::string> > dictionary;
-
C++11 改进了语法,允许直接写成
>>
,不用空格,更简洁:cppstd::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
,告诉编译器"不要在这个翻译单元实例化模板函数",只做语法检查和类定义。cppextern template class std::vector<Widget>;
-
在一个翻译单元中 显式实例化 该模板,这里才真正生成对应的模板函数代码:
cpptemplate 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
}
这样写的好处:
sum
是Sum
结构体的静态成员函数,- 编译器允许
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 (约等)
}
- 这样写,
avg
和sum
都能自动推导正确返回类型,支持混合参数类型。 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
函数)。 - 仍禁止
goto
、try/catch
、调用非constexpr
函数等。
总结:C++14 的constexpr
更强大、更接近普通函数,但仍保证编译时求值的能力。
Template alias 用 using
关键字定义模板别名,替代以前复杂且容易出错的 typedef
。
-
例如:
cpptemplate<typename T> using setGT = std::set<T, std::greater<T>>;
这样
setGT<double>
就是std::set<double, std::greater<double>>
的别名。 -
也可以用
using
替代普通指针函数类型的typedef
,语法更清晰:cpptypedef void (*voidfunc)(); using voidfunc = void (*)();
-
总之,
using
语法更现代,更易读,推荐替代传统的typedef
。
Some String-Related Features
- Unicode 字符串字面量 :
u8"..."
表示 UTF-8 编码的字符串(类型是const char*
,C++11 起支持)u"..."
表示 UTF-16 编码的字符串(类型是const char16_t*
)U"..."
表示 UTF-32 编码的字符串(类型是const char32_t*
)
- 原始字符串字面量(Raw string literals) :
-
用
R"( ... )"
语法定义,字符串内部不需要转义,大大方便了包含大量反斜杠、引号的字符串。 -
例子:
cppstring s = "backslash: \"\\\", single quote: \"'\""; string t = R"(backslash: "\", single quote: "'")"; // s 和 t 内容相同
-
还可以使用自定义分隔符,避免字符串中包含的符号干扰原始字符串界定符:
cppstring 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:
- Scoped enums(enum classes)
- 使用
enum class
定义的枚举,枚举值不会直接泄露到外围作用域,避免命名冲突。 - 需要用
EnumName::Enumerator
访问枚举值。 - 减少了隐式转换,类型安全更好。
- 可以显式指定底层类型,如
enum class Color : uint8_t { Red, Green, Blue };
。
- 使用
- long long
- 新增的整型,至少64位宽,适合存储较大整数。
- 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:
- Generalized Unions(泛化的联合体)
- 传统 C++ 联合体只能包含没有构造函数、析构函数、赋值运算符的成员。
- C++11 开始,联合体成员允许拥有构造函数、析构函数和赋值操作符。
- 但如果联合体成员定义了这些用户自定义的函数,整个联合体对象将不能调用这些函数(编译器会当成
=delete
处理),意味着不能直接使用这些成员的构造、析构、赋值。
- 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 进一步补充:
- Garbage Collection ABI
- C++11 规范定义了一个"垃圾回收应用二进制接口"(Garbage Collection ABI),
- 主要是为实现垃圾回收机制的编译器/运行时制定基础规则和接口。
- 注意 :标准并不要求必须实现垃圾回收,也不包含具体的垃圾回收机制,
只是为未来支持垃圾回收的实现预留了接口规范。
- User-Defined Literals(用户自定义字面量)
-
允许程序员定义新的字面量后缀,将字面量直接转换成自定义类型对象。
-
例如,定义一个字面量操作符,可以让下面的写法合法:
cppBinary b = 11010101001011b;
-
这里
b
是用户自定义的后缀,11010101001011
是字面量(通常是整数或字符串),
编译器调用对应的字面量操作函数,构造出一个Binary
类型对象。 -
这种功能使代码更简洁、可读,且能轻松支持各种自定义字面量格式(比如时间、单位、颜色等)。
-
C++14 新增的两个很实用的语言特性:
- Binary Literals(二进制字面量)
-
使用
0b
或0B
作为前缀,可以直接写二进制数字, -
比如:
cppauto fifteen = 0b1111; // 十进制的15
-
方便直接用二进制表示数据,尤其对底层编程很有用。
-
- 单引号作为数字分隔符(Digit Separator)
-
可以在数字字面量中用单引号
'
来分隔数字部分,提升可读性, -
例如:
cppauto filler_word = 0xdead'beef; // 十六进制 auto aBillion = 1'000'000'000; // 十进制一亿
-
这个特性不会改变数字值,只是让数字更易读,尤其是大数字或长数字。
-
这是 C++11 和之后版本引入的一组支持更好类设计的特性总结:
- Generated functions:
default
/delete
- 允许程序员显式声明让编译器生成默认的构造函数、析构函数、拷贝/移动函数(
= default
),或显式禁止某些函数(= delete
)。 - 这样写代码更清晰,控制更精确。
- 允许程序员显式声明让编译器生成默认的构造函数、析构函数、拷贝/移动函数(
- Override control:
override
/final
override
关键字标记派生类中的函数覆盖基类虚函数,编译器帮你检查是否真正覆盖。final
用于阻止虚函数被进一步覆盖,或阻止类被继承。
- Delegating constructors(委托构造函数)
- 一个构造函数可以调用同类的另一个构造函数,避免代码重复。
- Inheriting constructors(继承构造函数)
- 派生类可以继承基类的构造函数,减少写构造函数的麻烦。
- In-class initializers(类内成员初始化)
- 可以直接在类定义中给成员变量赋默认值,简化构造函数代码。
- Explicit conversion operators
- 支持显式定义类型转换操作符,防止隐式类型转换导致的问题。
这个问题是关于"如何禁止对象拷贝"的。
旧的C++解决办法:
- 把拷贝构造函数和拷贝赋值运算符声明为私有(private):
- 这样外部代码无法访问它们,导致无法拷贝。
- 但这样会带来一些麻烦,比如需要手动声明且不定义(如果不定义,链接会报错),不直观且难以维护。
- 继承自专门的不可拷贝基类(如 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)接口设计的问题和潜在的错误风险。具体来说:
问题点
- 是否真的覆盖了基类的虚函数?
Derived::f(int)
明显是覆盖了Base::f(int)
,因为签名相同且基类函数是虚的。- 但是
Derived::g()
是不是覆盖了Base::g() const
?
不同点在于const
,所以它实际上不是覆盖,而是重载,可能会导致运行时调用基类的版本而不是派生类的版本,造成隐藏(bug隐患)。
Derived::ff(int)
是否是虚函数?- 基类中是
virtual void ff(int)
,派生类中写成了void ff(int)
,没有写virtual
,但它依然是虚函数,因为虚性在继承中保留,但这样写容易迷惑,代码可读性变差。
- 基类中是
Derived::h(int)
是不是覆盖基类的虚函数?- 基类的
h(int)
不是虚函数,派生类的h(int)
只是新成员函数,不是覆盖基类虚函数。 - 调用时可能不会有多态效果。
- 基类的
- 如何防止进一步覆盖?
- 旧式 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的改进:
-
允许非静态成员在类定义内直接初始化 ,如:
cppcomplex<double> capacity = 100;
-
但静态成员变量仍需在类外初始化(一般还是写在.cpp文件里):
cppint 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
来限制构造函数的隐式转换:cppexplicit 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)
-
允许用
{}
统一初始化容器或用户自定义类型。 -
示例:
cppstd::vector<int> v = {1, 2, 3, 4};
b. 统一初始化语法(Uniform Initialization)
-
用
{}
初始化任何对象,统一语法:cppint x{5}; MyClass obj{arg1, arg2};
c. 防止窄化(Prevention of Narrowing)
-
窄化(narrowing)是把大类型的值赋给小类型,可能丢失精度:
cppint x1 = 3.14; // OK,旧 C++ int x2{3.14}; // 错误,C++11 检查
2. Lambdas(λ表达式)
-
内联定义匿名函数,适用于算法、回调、函数对象等:
cppauto 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(移动语义)
-
让类支持将资源"移走"而不是复制。
cppstd::vector<int> a = {1,2,3}; std::vector<int> b = std::move(a); // 移动资源而非复制
4. Universal References(万能引用)
-
出现在模板中,形如
T&&
,可以绑定左值或右值。cpptemplate<typename T> void func(T&& arg); // 这是万能引用
5. Perfect Forwarding(完美转发)
-
保留传入参数的左/右值性质,用
std::forward
实现:cpptemplate<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
有两个元素,分别是 10
和 20
。
为什么危险?
同样的数字组合,不同的括号,语义完全不同!
很容易在维护或阅读代码时误解含义。
建议:
"避免写与 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
,会影响后续代码的行为。
建议:
-
如果想要普通的类型推断,用圆括号或等号初始化 :
cppauto 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; }
这里,target
和 epsilon
是从外部作用域捕获进来的,它们被"打包"进了 lambda,方便后续调用时使用。
- 这样就实现了局部灵活的函数逻辑,同时还能用 STL 算法(这里是
partition
和for_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 :不能取地址的表达式,通常是临时对象、字面量常量等,比如
42
、x + 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++ 有 拷贝构造函数 和 拷贝赋值运算符 :
cppT::T(const T&); T& operator=(const T&);
它们负责复制对象的内容。
-
C++11 新增了 移动构造函数 和 移动赋值运算符 :
cppT::T(T&&); T& operator=(T&&);
它们的作用是"偷走"参数对象的资源,把资源转移给当前对象,参数对象则变成"空壳",方便高效处理临时对象。
-
移动操作通常被声明为
noexcept
,表示不会抛异常,更安全。
现代 C++ 类设计中的"六大法宝"------六个经典函数:
- 默认构造函数
Big()
- 析构函数
~Big()
- 拷贝构造函数
Big(const Big&)
- 拷贝赋值运算符
Big& operator=(const Big&)
- 移动构造函数
Big(Big&&)
- 移动赋值运算符
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及以后):
- 使用完美转发(perfect forwarding)和模板构造函数
用模板构造函数统一处理所有组合,结合std::forward
完美转发参数,避免写大量重载。 - 成员的统一构造方式
在成员的构造调用中使用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
当作左值还是右值转发。- 这样,成员对象
b
和s
会根据传进来的参数的实际类型,调用它们的拷贝构造或移动构造。 - 这种写法避免了大量的构造函数重载,且效率更高。
原则:只有当移动比复制更高效时,才应该实现移动构造函数和移动赋值运算符。
- 大多数 C++11 标准库组件都支持移动操作,比如
std::vector
,std::string
等,能在内部用移动来提升性能。 - 有些类型只支持移动(比如
std::unique_ptr
),不支持复制,这样避免了潜在的资源管理问题。
"五法则"(The Rule of 5)
随着 C++11 引入移动语义,传统的"三法则"(Rule of 3)扩展为"五法则":
如果一个类需要自定义以下任意一个:
- 析构函数
~T()
- 拷贝构造函数
T(const T&)
- 拷贝赋值运算符
T& operator=(const T&)
- 移动构造函数
T(T&&)
- 移动赋值运算符
T& operator=(T&&)
那通常你应该同时定义这五个,保证资源正确管理和性能最优。
你说的内容是关于 C++11 中著名的 "五法则"(Rule of 5) ,它是对传统 "三法则"(Rule of 3) 的扩展:
Rule of 5 要点总结:
- 传统的 Rule of 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
。