C++11新增特性
in Cplusplus Pageviews
cplusplus语言特性
核心语言特性
auto
与decltype
auto 占位类型说明符
变量的类型说明符
从初始化器中推导
auto x = 1 + 2;
从模板实参推导
std::pair p(2, 4.5);
std::tuple t(4, 3, 2.5);
std::copy_n(vi1, 3, std::back_insert_iterator(vi2));
std::for_each(vi.begin(), vi.end(), Foo([&](int i) {
// ...
}));
auto lck = std::lock_guard(foo.mtx);
std::lock_guard lck2(foo.mtx, ul);
从函数调用推导
如果 P
在移除引用和 cv
限定符后得到 std::initializer_list <P'>
且 A
是花括号初始化器列表,那么对该初始化器列表中的每个元素进行推导,以 P'
为形参,并以列表元素 A'
为实参:
template<class T>
void f(std::initializer_list<T>);
f({1, 2, 3}); // P = std::initializer_list<T>, A = {1, 2, 3}
// P'1 = T,A'1 = 1:推导出 T = int
// P'2 = T,A'2 = 2:推导出 T = int
// P'3 = T,A'3 = 3:推导出 T = int
// OK:推导出 T = int
f({1, "abc"}); // P = std::initializer_list<T>,A = {1, "abc"}
// P'1 = T,A'1 = 1:推导出 T = int
// P'2 = T,A'2 = "abc":推导出 T = const char*
// 错误:推导失败,T 有歧义
如果 P
在移除引用和 cv 限定符后得到 P'
[N
] 且 A
是非空花括号初始化器列表,那么按上述方法进行推导,但如果 N
是非类型模板形参,那么它会从初始化器列表的长度被推导:
template<class T, int N>
void h(T const(&)[N]);
h({1, 2, 3}); // 推导出 T = int 以及 N = 3
template<class T>
void j(T const(&)[3]);
j({42}); // 推导出 T = int,数组边界不是形参,不考虑
struct Aggr {
int i;
int j;
};
template<int N>
void k(Aggr const(&)[N]);
k({1, 2, 3}); // 错误:推导失败,没有从 int 到 Aggr 的转换
k({{1}, {2}, {3}}); // OK:推导出 N = 3
template<int M, int N>
void m(int const(&)[M][N]);
m({{1, 2}, {3, 4}}); // 推导出 M = 2 以及 N = 2
template<class T, int N>
void n(T const(&)[N], T);
n({{1}, {2}, {3}}, Aggr()); // 推导出 T = Aggr 以及 N = 3
如果形参包作为最后的 P
出现,那么对调用的每个剩余实参类型 A
与类型 P
匹配。每个匹配为包展开中的下个位置推导模板实参
template<class... Types>
void f(Types&...);
void h(int x, float& y)
{
const int z = x;
f(x, y, z); // P = Types&..., A1 = x:推导出 Types... 的第一成员 = int
// P = Types&..., A2 = y:推导出 Types... 的第二成员 = float
// P = Types&..., A3 = z:推导出 Types... 的第三成员 = const int
// 调用 f<int, float, const int>
}
如果用占位类型说明符声明多个变量,那么推导出的类型必须互相匹配。例如,声明 auto i = 0, d = 0.0; 非良构,而声明 auto i = 0, *p = &i; 良构并将 auto
推导为 int。
new
表达式中的类型标识
从初始化器推导类型
函数或lambda
表达式中的返回类型
auto &f();
返回类型推导
如果函数是声明的
int x = 1;
auto f() {return x;} // 返回类型是int
const auto &f() {return x;} // 返回类型是const int&
如果返回类型是decltype(auto),那么返回类型是将返回语句中所用的表达式包裹到decltype
中所得到的类型:
int x = 1;
decltype(auto) f() {return x;} // 返回类型是int, 同decltype(int)
decltype(auto) f() {return (x);} // 返回类型是int&, 同decltype((int))
如果有多条返回语句,那么它们必须推导出相同的类型
auto f(bool val)
{
if (val) {
return 123; // 推导出int
} else {
return 3.14f; // 错误:推导出返回类型 float
}
}
如果没有返回语句或返回语句的实参是 void
表达式,那么所声明的返回类型,必须要么是 decltype(auto)
,此时推导返回类型是 void
,要么是(可有 cv 限定的)auto
,此时推导的返回类型是(具有相同 cv 限定的)void
。
auto f() {} // 返回void
auto g() {return f();} // 返回void
auto *x() {} // 错误:不能从void推导auto*
一旦在函数中见到一条返回语句,那么从该语句推导的返回类型就可以用于函数的剩余部分,包括其他返回语句
auto sum(int i) {
if (i == 1) {
return i; // sum 的返回类型是 int
} else {
return sum(i - 1) + i; // OK,sum 的返回类型已知
}
}
如果返回语句使用花括号初始化器列表(brace-init-list
),那么就不能推导:
auto f() {return {1, 2, 3};} // 错误
非类型模板形参的形参声明
// 从对应的实参推导它的类型
template<auto I> struct A;
auto类型推导
#include <iostream>
#include <utility>
template<class T, class U>
auto add(T t, U u) { return t + u; } // 返回类型是 operator+(T, U) 的类型
// 在它调用的函数返回引用的情况下
// 函数调用的完美转发必须用 decltype(auto)
template<class F, class... Args>
decltype(auto) PerfectForward(F fun, Args&&... args)
{
return fun(std::forward<Args>(args)...);
}
template<auto n> // C++17 auto 形参声明
auto f() -> std::pair<decltype(n), decltype(n)> // auto 不能从花括号初始化器列表推导
{
return {n, n};
}
int main()
{
auto a = 1 + 2; // a 的类型是 int
auto b = add(1, 1.2); // b 的类型是 double
static_assert(std::is_same_v<decltype(a), int>);
static_assert(std::is_same_v<decltype(b), double>);
auto c0 = a; // c0 的类型是 int,保有 a 的副本
decltype(auto) c1 = a; // c1 的类型是 int,保有 a 的副本
decltype(auto) c2 = (a); // c2 的类型是 int&,它是 a 的别名
std::cout << "通过 c2 修改前,a = " << a << '\n';
++c2;
std::cout << "通过 c2 修改后,a = " << a << '\n';
auto [v, w] = f<0>(); // 结构化绑定声明
auto d = {1, 2}; // OK:d 的类型是 std::initializer_list<int>
auto n = {5}; // OK:n 的类型是 std::initializer_list<int>
// auto e{1, 2}; // C++17 起错误,之前是 std::initializer_list<int>
auto m{5}; // OK:DR N3922 起 m 的类型是 int,之前是 initializer_list<int>
// decltype(auto) z = {1, 2} // 错误:{1, 2} 不是表达式
// auto 常用于无名类型,例如 lambda 表达式的类型
auto lambda = [](int x) {
return x + 3;
};
[](...){}(c0, c1, v, w, d, n, m, lambda); // 阻止"变量未使用"警告
}
decltype类型说明符
检查实体的声明类型,或表达式的类型和值类别。
decltype
( entity )
decltype
(expression)
#include <iostream>
#include <type_traits>
struct A { double x; };
const A* a;
decltype(a->x) y; // y 的类型是 double(其声明类型)
decltype((a->x)) z = y; // z 的类型是 const double&(左值表达式)
template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) { // 返回类型依赖于模板形参
return t + u; // C++14 开始可以推导返回类型
}
int main()
{
int i = 33;
decltype(i) j = i * 2;
std::cout << "i = " << i << ", " << "j = " << j << '\n';
std::cout << "i 和 j 的类型相同吗?"
<< (std::is_same_v<decltype(i), decltype(j)> ? "相同" : "不同") << '\n';
auto f = [](int a, int b) -> int {
return a * b;
};
decltype(f) g = f; // the type of a lambda function is unique and unnamed
i = f(2, 2);
j = g(3, 3);
std::cout << "i = " << i << ", "
<< "j = " << j << '\n';
}
引用声明
左值引用 T&
;
右值引用 T&&
;
左值引用
左值引用可以用来作为对象的别名;
#include <iostream>
#include <string>
int main()
{
std::string s = "Ex";
std::string& r1 = s;
const std::string& r2 = s;
r1 += "ample"; // 修改 s
// r2 += "!"; // 错误:不能通过到 const 的引用修改
std::cout << r2 << '\n'; // 打印 s,它现在保有 "Example"
}
左值引用用于在函数调用中实现按引用传递语义;
#include <iostream>
#include <string>
void doubleString(std::string& s)
{
s += s; // 's' 与 main() 的 'str' 是同一对象
}
int main()
{
std::string str = "Test";
doubleString(str);
std::cout << str << std::endl;
}
函数的返回值是左值引用时,函数调用变成左值表达式;
#include <iostream>
#include <string>
char& charNumber(std::string& s, std::size_t n)
{
return s.at(n); // string::at() 返回 char 的引用
}
int main()
{
std::string str = "Test";
chaNumber(str, 1) = 'a'; // 函数调用是左值,可被赋值
std::cout << str << std::endl;
}
右值引用
右值引用可用于为临时对象延长生存期(const 的左值引用也能延长对象的生存期,但这些对象无法被修改);
#include <iostream>
#include <string>
int main()
{
std::string s1 = "Test";
// std::string&& r1 = s1; // 错误:不能绑定到左值
const std::string& r2 = s1 + s1; // OK:到 const 的左值引用延长生存期
// r2 += "Test"; // 错误:不能通过到 const 的引用修改
std::string&& r3 = s1 + s1; // OK:右值引用延长生存期
r3 += "Test"; // OK:能通过到非 const 的引用修改
std::cout << r3 << '\n';
}
如果存在同时存在左值与右值的重载函数时,左值引用匹配左值,右值引用匹配右值;
#include <iostream>
#include <utility>
void f(int& x)
{
std::cout << "lvalue reference overload f(" << x << ")\n";
}
void f(const int& x)
{
std::cout << "lvalue reference to const overload f(" << x << ")\n";
}
void f(int&& x)
{
std::cout << "rvalue reference overload f(" << x << ")\n";
}
int main()
{
int i = 1;
const int ci = 2;
f(i); // 调用 f(int&)
f(ci); // 调用 f(const int&)
f(3); // 调用 f(int&&),如果没有 f(int&&) 重载函数则会调用 f(const int&)
f(std::move(i)); // 调用 f(int&&)
// 右值引用变量在用于表达式时是左值
int&& x = 1;
f(x); // 调用 f(int& x)
f(std::move(x)); // 调用 f(int&& x)
}
转发引用
转发引用是一种特殊的引用,它能够保持函数实参的值类型,使得std::forward
能用来转发实参。
函模板的函数形参
template<class T> int f(T&& x) // x 是转发引用 { return g(std::forward<T>(x)); // 从而能被转发 } int main() { int i = 5; f(i); // 实参是左值,调用 f<int&>(int&),std::forward<int&>(x) 是左值 f(0); // 实参是右值,调用 f<int>(int&&),std::forward<int>(x) 是右值 } template<class T> int g(const T&& x); // x 不是转发引用:const T 不是无 cv 限定的 template<class T> struct A { template<class U> A(T&& x, U&& y, int* p); // x 不是转发引用:T 不是构造函数的类型模板形参 // 但 y 是转发引用 };
auto&&
,当其从花括号包含的初始化列表推导时除外auto&& vec = foo(); // foo() 可以是左值或右值,vec 是转发引用 auto i = std::begin(vec); // 也可以 (*i)++; // 也可以 g(std::forward<decltype(vec)>(vec)); // 转发,保持值类别 for (auto&& x: f()) { // x 是转发引用;这是使用范围for循环最安全的方式 } auto&& z = {1, 2, 3}; // 不是转发引用(初始化器列表的特殊情形)
完美转发
std::forward常用来做完美转发使用,它的作用是保持原来的值属性不变化,能够将左值,右值,const类型完美转给另一个函数;
template<typename T>
void print(T &t) {
std::count << "lvalue" << std::endl;
}
template<typename T>
void print(T &&t) {
std::count << "rvalue" << std::endl;
}
template<typename T>
void perfectForward(T &&v) {
print(v);
print(std::forward<T>(v));
print(std::move(v));
}
int main(int argc, char *argv[])
{
perfectForward(1); // output: lvalue rvalue rvalue
int x = 1;
perfectForward(x); // output: lvalue lvalue rvalue
system("pause");
return 0;
}
悬垂引用
尽管引用一开始初始化就始终指代一个有效的对象和函数,但有可能创建一个函数,其中被指代对象的生存期结束后而引用仍然保持可访问(dangling),访问这种引用会产生未定义行为。
std::string& f()
{
std::string s = "Example";
return s; // 退出s的作用域:调用其析构函数并解分配其存储
}
std::string& r = f(); // 悬垂引用
std::cout << r; // 未定义行为:从悬垂引用读取
std::string s = f(); // 未定义行为:从悬垂引用复制初始化