C++11新增特性

C++ core feature!

cplusplus语言特性

核心语言特性

autodecltype

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能用来转发实参。

  1. 函模板的函数形参

     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 是转发引用
     };
    
  2. 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();  // 未定义行为:从悬垂引用复制初始化