目录
情况一 ParamType 是一个指针或者是一个引用类型,但并不是一个通用的引用类型(通用的引用类型的内容在条款24)。
情况二 ParamType 是一个通用的引用(既可以代表左值又可以代表右值)
以下两种情况是复用了模板的类型推导,而不是 auto 的类型推导:
如果一个非变量名的类型为 T 的左值表达式, decltype 报告的类型是 T&
一、类型推导
1. 三种类型推导
auto
auto 出现在调用函数模板时
auto 是非常简单,但细究起来却大有文章。用它来节省敲击键盘,此外它也防止了手动指定类型带来的的潜在错误和性能问题
decltype
decltype(auto)
2. 左值与右值**
C++ 增加了一个新的类型,右值引用,记作“&&”
- 左值
是指在内存中有明确的地址,我们可以找到这块地址的数据(可取地址)
- 右值
只提供数据,无法找到地址(不可取地址)
- 所有有名字的都是左值,而右值是匿名的
- 一般情况下,位于等号左边的是左值,位于等号右边的是右值,但是也可以出现左值给左值赋值的情况。
右值
C++11 中右值分为两种情况:一个是将亡值,另一个是纯右值:
将亡值 非引用返回的临时变量,运算表达式产生的临时变量,原始字面量,lambda 表达式等。
纯右值 与右值引用相关的表达式,比如:T&& 类型函数的返回值,std::move() 的返回值等。
右值引用
右值引用就是对右值引用的类型。因为右值是匿名的,所以我们只能通过引用的方式找到它。无论是左值引用还是右值引用,都必须初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。通过右值引用,该右值所占的内存又可以被使用。
#include<iostream>
#include<vector>
using namespace std;
int&& value = 520;//右值引用,520是字面值,是右值
class Test
{
public:
Test()
{
cout << "构造函数" << endl;
}
//const是万能引用,即可以接收左值,又能接收右值
Test(const Test& other)//常量左值引用
{
cout << "拷贝构造函数" << endl;
}
};
Test getObj()
{
return Test();
}
int main()
{
int a1;
//int &&a2 = a1; //报错,右值引用不能被左值初始化
//Test& t1 = getObj(); //右值不能初始化左值引用
Test&& t2 = getObj(); //函数返回的临时对象是右值,可以被引用
const Test& t3 = getObj(); //常量左值引用是万能引用,可以接收左值、右值、常量左值、常量右值
const int& t3 = a1; //被左值初始化
return 0;
}
3. 理解模板类型推导
template<typename T>
void f(const T& param); // ParamType 是 const T&
int x = 0;
f(x); // 使用int调用f
f(expr); // 从expr推导出T和ParamType的类型
T 的类型不仅和 expr 的类型独立,而且还取决于 ParamType 的形式。
情况一 ParamType 是一个指针或者是一个引用类型,但并不是一个通用的引用类型(通用的引用类型的内容在条款24)。
template<typename T>
void f(T& param); // param是一个引用类型
int x = 27; // x是一个int
const int cx = x; // cx是一个const int
const int& rx = x; // rx是const int的引用
f(x); // T是int,param的类型时int&
f(cx); // T是const int,param的类型是const int&
f(rx); // T是const int, param的类型时const int&
当传递一个 const 对象给一个引用参数,他们期望对象会保留常量特性;
引用特性会被类型推导所忽略;
如果我们把 f 的参数类型从 T& 变成 const T& ,由于 param 的声明是 const 引用的, cx 和 rx 的 const 特性会被保留,这样的话 T 的 const 特性就没有必要了
template<typename T>
void f(const T& param); // param现在是const的引用
int x = 27; // 和之前一样
const int cx = x; // 和之前一样
const int& rx = x; // 和之前一样
f(x); // T是int,param的类型是const int&
f(cx); // T是int,param的类型是const int&
f(rx); // T是int,param的类型是const int&
如果 param 是一个指针(或者指向 const 的指针)而不是引用,情况也是类似:
template<typename T>
void f(T* param); // param是一个指针
int x = 27; // 和之前一样
const int *px = &x; // px是一个指向const int x的指针
f(&x); // T是int,param的类型是int*
f(px); // T是const int, param的类型时const int*
情况二 ParamType 是一个通用的引用(既可以代表左值又可以代表右值)
如果 expr 是一个左值, T 和 ParamType 都会被推导成左值引用。这有些不同寻常。第一,这是模板类型 T 被推导成一个引用的唯一情况。第二,尽管 ParamType 利用右值引用的语法来进行推导,但是他最终推导出来的类型是左值引用。
如果 expr 是一个右值,那么就执行“普通”的法则(第一种情况)
template<typename T>
void f(T&& param); // param现在是一个通用的引用
int x = 27; // 和之前一样
const int cx = x; // 和之前一样
const int& rx = x; // 和之前一样
f(x); // x是左值,所以T是int&, param的类型也是int&
f(cx); // cx是左值,所以T是const int&, param的类型也是const int&
f(rx); // rx是左值,所以T是const int&, param的类型也是const int&
f(27); // 27是右值,所以T是int, 所以param的类型是int&&
情况三 ParamType 既不是指针也不是引用
实际上就是按值传递参数, const (和 volatile )在按值传递参数的时候会被忽略掉。
忽略引用的部分。
template<typename T>
void f(T param);
int x = 27; // 和之前一样
const int cx = x; // 和之前一样
const int& rx = x; // 和之前一样
f(x); // T和param的类型都是int
f(cx); // T和param的类型也都是int
f(rx); // T和param的类型还都是int;
数组参数
一个数组会被退化成一个指向其第一个元素的指针
const char name[] = "J. P. Briggs"; // name的类型是const char[13]
const char * ptrToName = name; // 数组被退化成指针
template<typename T>
void f(T param); // 模板拥有一个按值传递的参数
f(name); // name是个数组,但是T被推导成const char*
template<typename T>
void f(T& param); // 引用参数的模板
f(name); // 传递数组给f
声明数组的引用可以使的创造出一个推导出一个数组包含的元素长度的模板:
// 在编译的时候返回数组的长度(数组参数没有名字,因为只关心数组包含的元素的个数)
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept
{
return N; // constexpr和noexcept在随后的条款中介绍
}
int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 }; // keyVals有七个元素
int mappedVals[arraySize(keyVals)]; // mappedVals长度也是七
//作为一个现代的C++开发者,应该优先选择内建的 std::array
std::array<int, arraySize(keyVals)> mappedVals; // mappedVals长度是七
函数参数
数组并不是C++唯一可以退化成指针的东西。函数类型可以被退化成函数指针
void someFunc(int, double); // someFunc是一个函数
// 类型是void(int, double)
template<typename T>
void f1(T param); // 在f1中 参数直接按值传递
template<typename T>
void f2(T& param); // 在f2中 参数是按照引用传递
f1(someFunc); // param被推导成函数指针
// 类型是void(*)(int, double)
f2(someFunc); // param被推导成函数指针
// 类型时void(&)(int, double)
4. 理解auto类型推导
除了一个例外(花括号初始化 ), auto 类型推导就是模板类型推导;
auto 和模板类型推导的本质区别就是 auto 假设花括号初始化代表的是std::initializer_list,但是模板类型推导却不是
auto 在函数返回值或者lambda参数里面执行模板的类型推导,而不是通常意义的 auto 类型推导
当一个变量被声明为 auto , auto 相当于模板中的 T ,而对变量做的相关的类型限定就像 ParamType 。
auto x = 27; // 情况3(x既不是指针也不是引用)
const auto cx = x; // 情况3(cx二者都不是)
const auto& rx = x; // 情况1(rx是一个非通用的引用)
const int theAnswer = 42;
auto x = theAnswer; // x 的推导的类型是 int
auto y = &theAnswer; // y 的类型是 const int*
auto&& uref1 = x; // x是int并且是左值, 所以uref1的类型是int&
auto&& uref2 = cx; // cx是int并且是左值, 所以uref2的类型是const int&
auto&& uref3 = 27; // 27是int并且是右值, 所以uref3的类型是int&&yon
const char name[] = // name的类型是const char[13]
"R. N. Briggs";
auto arr1 = name; // arr1的类型是const char*
auto& arr2 = name; // arr2的类型是const char (&)[13]
void someFunc(int, double); // someFunc是一个函数,类型是 void (*)(int, double)
auto& func2 = someFunc; // func1的类型是, void (&)(int, double)
特殊情况:花括号初始化
当使用一对花括号来初始化一个 auto 类型的变量的时候,推导的类型是 std::intializer_list;
但是如果相同的初始化递给相同的模板,类型推导会失败,代码不能编译。
int x1 = 27;
int x2(27);
int x3 = { 27 };
int x4{ 27 };
auto x1 = 27; // 类型时int,值是27
auto x2(27); // 同上
auto x3 = { 27 }; // 类型是std::intializer_list<int>, 值是{ 27 }
auto x4{ 27 }; // 同上
template<typename T> // 和x的声明等价的
void f(T param); // 模板
f({ 11, 23, 9 }); // 错误的!没办法推导T的类型
//但是,如果你明确模板的 param 的类型是一个不知道 T 类型的 std::initializer_list<T>
template<typename T>
void f(std::initializer_list<T> initList);
f({ 11, 23, 9 }); // T被推导成int,initList的类型是std::initializer_list<int>
以下两种情况是复用了模板的类型推导,而不是 auto 的类型推导:
auto 表示推导的函数返回值;
lambda可能会在参数声明里面使用 auto;
auto createInitList()
{
return { 1, 2, 3 }; // 编译错误:不能推导出{ 1, 2, 3 }的类型
}
std::vector<int> v;
…
auto resetV = [&v](const auto& newValue) { v = newValue; } // C++14
…
resetV({ 1, 2, 3 }); // 编译错误,不能推导出{ 1, 2, 3 }的类型
5. 理解 decltype
decltype 几乎总是得到一个变量或表达式的类型而不需要任何修改;
对于非变量名的类型为 T 的左值表达式, decltype 总是返回 T&;
C++14 支持 decltype(auto) ,它的行为就像 auto ,从初始化操作来推导类型,但是它推导类型时使用 decltype 的规则;
decltype(变量名或者表达式)
与 templates 和 auto 在类型推导中行为相比(请见条款一和条款二), decltype 一般只是复述一遍你所给他的变量名或者表达式的类型,如下:
const int i = 0; // decltype(i) is const int
bool f(const Widget& w); // decltype(w) is const Widget&
// decltype(f) is bool(const Widget&)
struct Point{
int x, y; // decltype(Point::x) is int
};
Widget w; // decltype(w) is Widget
if (f(w)) ... // decltype(f(w)) is bool
template<typename T> // simplified version of std::vector
class vector {
public:
...
T& operator[](std::size_t index);
...
};
vector<int> v; // decltype(v) is vector<int>
...
if(v[0] == 0) // decltype(v[0]) is int&
在C++11中, decltype 最主要的用处可能就是用来声明一个函数模板,在这个函数模板中返回值的类型取决于参数的类型。
decltype(auto)
1) C++11 的尾随返回类型技术: 函数的返回值类型在函数参数之后声明(“->”后边), 尾随返回类型的一个优势是在定义返回值类型的时候使用函数参数
template<typename Container, typename Index> // works, but require refinements
auto authAndAccess(Container& c, Index i)-> decltype(c[i])
{
authenticateUser();
return c[i];
}
2) 在 C++14 中之中行为被拓展到包括多语句的所有的 lambda·表达式和函数。在上面 authAndAccess 中,意味着在 C++14 中我们可以忽略尾随返回类型,仅仅保留开头的 auto(将会使用类型推导)
template<typename Container, typename Index> // C++14;
auto authAndAccess(Container &c, Index i) // not quite correct
{
authenticateUser();
return c[i];
} // return type deduced from c[i]
条款二解释说,对使用 auto 来表明函数返回类型的情况,编译器使用模板类型推导。但是这样是回产生问题的。 正如我们所讨论的,对绝大部分对象类型为 T 的容器, [] 操作子返回的类型是 &T , 然而条款一提到,在模板类型推导的过程中,初始表达式的引用会被忽略
如下,d[5] 返回的是 int& ,但是 authAndAccess 的 auto 返回类型声明将会剥离这个引用,从而得到的返回类型是 int int 作为一个右值成为真正的函数返回类型。上面的代码尝试给一个右值 int 赋值为10。 这种行为是在 C++ 中被禁止的,所以代码无法编译通过
std::deque<int> d;
...
authAndAccess(d, 5) = 10; // authenticate user, return d[5],
// then assign 10 to it;
// this won't compile!
3) 为了让 authAndAccess 按照我们的预期工作,我们需要为它的返回值使用 decltype 类型推 导,即指定 authAndAccess 要返回的类型正是表达式 c[i] 的返回类型; 这个功能在 C++14 中通过 decltype(auto) 实现。这使这对原本的冤家( decltype 和 auto )在一起完美地发挥作 用: auto 指定需要推导的类型, decltype 表明在推导的过程中使用 decltype 推导规则
template<typename Container, typename Index> // C++14; works,
decltype(auto) // but still requires refinement
authAndAccess(Container &c, Index i)
{
authenticateUser();
return c[i];
}
现在 authAndAccess 的返回类型就是 c[i] 的返回类型。在一般情况下, c[i] 返回 T& , authAndAccess 就返回 T& ,
在不常见的情况下, c[i] 返回一个对象, authAndAccess 也返回一个对象。
当一个变量被声明为 auto , auto 相当于模板中的 T ,而对变量做的相关的类型限定就像 ParamType
myWidget1中, T、 ParamType是auto,属于情况三,故myWidget1的类型推导是Widget
Widget w;
const Widget& cw = w;
auto myWidget1 = cw; // auto type deduction, myWidget1's type is Widget
decltype(auto) myWidget2 = cw // decltype type deduction: myWidget2's type is const Widget&
万能应用 T&&
template<typename Container, typename Index>
decltype(auto) anthAndAccess(Container &c, Index i);
这个容器是通过非 const 左值引用传入的,因为通过返回一个容器元素的引用是来修改容器是被允许的
这里不可能将右值传入这个函数。右值不能和一个左值引用绑定(除非是 const 的左值引用,这不是这里的情况)
传递一个右值容器给 authAndAccess 是一种极端情况。一个右值容器作为一个临时对象,
在 anthAndAccess 所在语句的最后被销毁,意味着对容器中一个元素的引用(这个引用
通常是 authAndAccess 返回的)在创建它的语句结束的地方将被悬空。
然而,这对于传给 authAndAccess 一个临时对象是有意义的。一个用户可能仅仅想拷贝一个临时容器中的一个
元素,例如:
std::deque<std::string> makeStringDeque(); // factory function
// make copy of 5th element of deque returned
// from makeStringDeque
auto s = authAndAccess(makeStringDeque(), 5);
支持这样的应用意味着我们需要修改 authAndAccess 的声明来可以接受左值和右值。重载可以 解决这个问题(一个重载负责左值引用参数,另外一个负责右值引用参数),但是我们将有 两个函数需要维护。避免这种情况的一个方法是使 authAndAccess 有一个既可以绑定左值又可 以绑定右值的引用参数,条款24将说明这正是统一引用( universal reference )所做的。因 此 authAndAccess 可以像如下声明:5)
template<typename Container, typename Index> // c is now a universa reference (c是一个万能应用)
decltype(auto) authAndAccess(Container&& c, Index i);
template<typename Container, typename Index> // final C++14 version
decltype(auto)
authAndAccess(Container&& c, Index i)
{
authenticateUser();
return std::forward<Container>(c)[i];
}
template<typename Container, typename Index> // final C++11 version
auto
authAndAccess(Container&& c, Index i) -> decltype(std::forward<Container>(c)[i])
{
authenticateUser();
return std::forward<Container>(c)[i];
}
如果一个非变量名的类型为 T 的左值表达式, decltype 报告的类型是 T&
对一个变量名使用 decltype 得到这个变量名的声明类型。变量名属于左值表达式,但这并不影响 decltype 的行为。
然而,对于一个比变量名更复杂的左值表达式, decltype 保证返回的类型是左值引用。
因此说,如果一个非变量名的类型为 T 的左值表达式, decltype 报告的类型是 T& 。
绝大部分左值表达式的类型有内在的左值引用修饰符。例如,需要返回左值的函数返回的总是左值引用
int x = 0
x 是一个变量名,decltyper(x) 是 int
给 x 加上括号"(x)"就得到一个比变量名复杂的表达式,表达式 (x) 也是左值, decltype((x)) 是 int&
decltype(auto) f1()
{
int x = 0;
...
return x; // decltype(x) is int, so f1 returns int
}
decltype(auto) f2()
{
int x = 0;
return (x); // decltype((x)) is int&, so f2 return int&,
//f2 不仅返回值类型与 f1 不同,它返回的是对一个局部变量的引用
}
最主要的经验教训就是当使用 decltype(auto) 时要多留心一些。被推导的表达式中看上去无
关紧要的细节都可能影响 decltype 返回的类型。为了保证推导出的类型是你所期望的,请使
用条款4中的技术。
当然, decltype (无论只有 decltype 或者还是和 auto 联合使用)有可能偶尔会产生类型推导的惊奇行为,但是这不是常见的情况。
一般情况下, decltype 会产生你期望的类型。将 decltype 应用于变量名无非是正确的,因为在这种情况下, decltype 做的就是报告这个变量名的声明类型(decared type)
6. 如何查看decltype
类型推导的结果常常可以通过IDE的编辑器,编译器错误输出信息和Boost TypeIndex库的结果中得到
一些工具的结果不一定有帮助性也不一定准确,所以对C++标准的类型推导法则加以理解是很有必要的
IDE编辑器
在IDE里面的代码编辑器里面当你使用光标悬停在实体之上,常常可以显示出程序实体(例如变量,参数,函数等等)的类型
涉及到更加复杂的类型的时候,从IDE里面得到的信息就不太有用了。
编译器诊断信息
故意制造编译问题。编译的错误输出会报告会和捕捉到的类型相关错误。
举个例子,我们希望看在上面例子中的 x 和 y 被推导的类型。我们首先声明一个类模板,但是并不定义这个模板。
尝试实例化这个模板会导致错误信息,因为没有模板的定义实现。
想看 x 和 y 被推导的类型,只要尝试去使用这些类型去实例化 TD
template<typename T> // 声明TD
class TD; // TD 是"Type Displayer"的缩写
TD<decltype(x)> xType; // 引起的错误
TD<decltype(y)> yType; // 包含了x和y的类型
上面把变量的名称取名”变量名字+type“,这样更容易产生我们想看到的信息,编译器报错如下:
error: aggregate 'TD<int> xType' has incomplete type and cannot be defined
error: aggregate 'TD<const int *> yType' has incomplete type and cannot be defined
运行时输出
printf 到运行的时候可以用来显示类型信息,它提供了对输出格式的完全掌控.
std::cout << typeid(x).name() << '\n'; // display types for
std::cout << typeid(y).name() << '\n'; // x and y
x 或者 y 调用typeid , 可以得到一个 std::type_info 对象, std::type_info 有一个成员函数name, name 可以提供一个C-style的字符串(也就是 constchar* );
调用 std::type_info::name 并不会确定返回有意义的东西,例如,GNU和Clang编译器返回 x 的类型是“ i ”, y 的类型是“ PKi ”,
“ i ”意味着“ int ”,“ PK ”意味着“pointer to konst const”(所有的编译器都支持一个工具, C++filt ,它可以解析这样的“乱七八糟”的类型。);微软的编译器提供更加直白的输出:“ int ”对 x ,“ int const* ”对 y 。
std::type_info::name 可能在IDE中会显示类型失败,但是Boost TypeIndex库(经常写做Boost.TypeIndex)是被设计成可以成功显示的.
#include <boost/type_index.hpp>
template<typename T>
void f(const T& param)
{
using std::cout;
using boost::typeindex::type_id_with_cvr;
// show T
cout << "T = "
<< type_id_with_cvr<T>().pretty_name()
<< '\n';
// show param's type
cout << "param = "
<< type_id_with_cvr<decltype(param)>().pretty_name()
<< '\n';
…
}
std::vector<Widget> createVec(); // 工厂方法
const auto vw = createVec(); // init vw w/factory return
if (!vw.empty()) {
f(&vw[0]); // 调用f
…
}
在GNU和Clang的编译器下面,Boost.TypeIndex输出(准确)的结果:
T = Widget const*
param = Widget const* const&
Boost.TypeIndex的库仅仅是一个对你编译类型推导的一种工具而已。所有的都是有帮助意义
的,但是到目前为止,没有什么关于类型推导法则1-3的替代品。
二、auto
7. 优先使用auto,而非显式类型声明
什么是闭包
- C++11引入了lambda表达式,它是一种创建匿名闭包的语法糖,可以方便地定义和使用闭包¹². lambda表达式的一般形式是:
[capture list] (parameter list) -> return type { function body }
,其中capture list指定了要捕获的外部变量,可以是值捕获或引用捕获。
- std::function:C++11提供了std::function模板类,它是一种通用的函数对象包装器**,可以存储任何可调用的对象,包括普通函数、成员函数、函数对象、lambda表达式等³。std::function可以用来作为闭包的参数或返回值,提高了闭包的灵活性和通用性。
- std::bind:C++11提供了std::bind函数,它可以将一个可调用的对象和一些参数绑定在一起,生成一个新的函数对象,可以延迟调用或传递给其他函数。std::bind可以用来实现部分应用(partial application),即将一个多参数的函数转换为一个少参数的函数,也可以用来改变函数的参数顺序或忽略某些参数。
C++中的闭包:由lambda表达式定义(而不是封装)的值(value),该表达式由代码以及代码中引用的变量的值组成。所以闭包是一个匿名函数对象,由编译器作为lambda表达式的结果自动创建。闭包存储lambda表达式中使用的lambda表达式定义范围内的这些变量。 lambda表达式的运行时效果是生成对象。此类对象称为闭包。(The runtime effect of a lambda expression is the generation of an object. Such objects are known as closures) 闭包是一个通过引用其主体外部的字段来封闭其周围状态的函数。封闭状态在闭包调用中保持不变。
lambdas vs. Closures for C++:
lambda和相应的闭包之间的区别完全等同于类和类实例之间的区别。 闭包之于lambda就像对象之于类一样(Closures are to lambdas as objects are to classes)。 类只存在于源代码中;它在运行时不存在。运行时存在的是类类型的对象。同样地:每个lambda表达式都会导致生成一个唯一的类(在编译期间),并且还会导致创建该类类型的对象(闭包)(在运行时)。(As we know, a class exists only in source code; it doesn’t exist at runtime. What exists at runtime are objects of the class type. Similarily: Each lambda expression causes a unique class to be generated(during compilation) and also causes an object of that class type--a closure--to be created(at runtime)) (1).例如,lambda在运行时不占用数据内存,尽管它们可能占用代码内存。 (2).闭包占用数据内存,但不占用代码内存。
什么是std::function
std::function 是 C++11 标准库的一个模板,它可以使函数指针普通化。
鉴于函数指针只能指向一个函数,然而, std::function 对象可以应用任何可以被调用的对象,就像函数。
std::function填补了函数指针的灵活性,但std::function 方法通常体积比 auto 大,并且慢,还有可能导致内存不足
的异常。
std::function是一种通用、多态的函数封装的类模板
实例一个std::function对象很简单,就是将可调用对象的返回值类型和参数类型作为模板参数传递给std::function模板类。比如:
std::function<void()> func1;
std::function<int(int,int)> func2;
#include<iostream>
#include<functional>
//1.普通函数
int plus(int a,int b)
{
return a + b;
}
//2.lamda表达式
auto plus_lamda = [](int a,int b)->int{
return a + b;
};
//3.仿函数
class plus_func{
public:
int operator()(int a,int b){
return a + b;
}
};
//4.类成员函数 && 类静态函数
class plus_class{
public:
inline int plus(int a,int b)
{
return a + b;
}
static int plus_static(int a,int b)
{
return a + b;
}
};
int main()
{
std::function<int(int,int)> f1 = plus;
std::cout<< "function:" << f1(1,2) << std::endl;
std::function<int(int,int)> f2 = plus_lamda;
std::cout<< "function lamda:" << f2(1,2) << std::endl;
std::function<int(int,int)> f3 = plus_func();
std::cout<< "function 3:" << f3(1,2) << std::endl;
plus_class pc;
std::function<int(int,int)> f4 = std::bind(&plus_class::plus,pc,std::placeholders::_1,std::placeholders::_2);
std::cout<< "function class func:" << f4(1,2) << std::endl;
std::function<int(int,int)> f5 = plus_class::plus_static;
std::cout<< "function class static func:" << f5(1,2) << std::endl;
return 0;
}
auto 的优点:
1)可以避免未初始化的变量、啰嗦的变量声明;
int x1; // 有潜在的未初始化风险
auto x2; // error! initializer required
auto x3 = 0; // fine, x's value is well-defined
2)直接持有闭包;
auto derefLess = [](const auto& p1, const auto& p2) { return *p1 < *p2; };
//和auto实现一样功能,但是繁琐,且需要更多的内存、时间
std::function<bool(const std::unique_ptr<Widget>&,const std::unique_ptr<Widget>&)> derefUPLess =
[](const std::unique_ptr<Widget>& p1,const std::unique_ptr<Widget>& p2){return *p1 < *p2; };
3)可以避免“型别捷径”问题
std::vector<int> v;
...
unsigned sz = v.size(); //v.size() 定义的返回类型是 std::vector<int>::size_type,
//在32位 Windows 系统上, unsigned 和 std::vector<int>::size_type 有同样的大小,
//但是在64位的 Windows 上, unsigned 是32bit的,而 std::vector<int>::size_type 是64bit的
auto sz = v.size() // sz's type is std::vector<int>::size_type
std::unordered_map<std::string, int> m;
..
.//std::unorder_map 的 key 部分是 const 类型的,
//在哈希表中的 std::pair 的类型不是 std::pair<std::string, int> ,而是 std::pair<const std::sting, int>
//编译器竭尽全力去找到一种方式,把 std::pair<const std::string, int> 对象(正是哈希表中的内容)
//转化为 std::pair<std::string, int> 对象( p 的声明类型)
for (const std::pair<std::string, int>& p : m)
{
... // do something with p
}
for (const auto& p : m)
//使用 auto 作为目标变量的类型,你不必为你声明类型和用来初始化它的表达式类型之间的不匹配而担心
{
... // as before
}
8. 当auto推导出非预期类型时应当使用显式的类型初始化
std::vector<bool> features(const Widget& w);
Widget w;
…
bool highPriority = features(w)[5]; // w是不是个高优先级的?
…
processWidget(w, highPriority); // 配合优先级处理
上面代码工作正常,但是如果把 highPriority 的显式的类型换成 auto , processWidget的行为变得不可预测
auto highPriority = features(w)[5]; // w是不是个高优先级的?
processWidget(w, highPriority); // 未定义的行为,highPriority包含野指针
原因分析:
std::vector 是对 bool 数据封装的模板特化,一个bit对应一个 bool;
std::vector 的 operator[] 应该返回一个 T& ,但是C++禁止bits的引用。没办法返回一个 bool& ,
std::vector 的 operator[] 于是就返回了一个行为上和 bool& 相似的对象,std::vector::reference 对象(是一
个在 std::vector 中内嵌的class);
highPriority 使用显式类型时, features 返回了一个 std::vector 对象,
然后执行 operator[] , operator[] 返回一个 std::vector::reference 对象,这个对象又隐式的转换
成 highPriority 需要用来初始化的 bool 类型。
所以 features 返回的 std::vector 的第五个bit的数值给到 highPriority 的数值,这也是我们所预期的。
使用 auto 的 highPriority 声明, features 返回一个 std::vector 对象, operator[] 再次被调用,
operator[] 继续返回一个 std::vector::reference 对象,但是现在有一个变化,因为 auto 推导 highPriority 的类型。 highPriority 根本并没有 features 返回的 std::vector 的第五个bit的数值。
调用 features() 会返回一个临时的 std::vector 对象。这个对象是没有名字的,我会把它叫做 temp ,
operator[] 是在 temp 上调用的, std::vector::reference 返回一个由 temp 管理的包含一个指向一个包含bits的数据
结构的指针,在word上面加上偏移定位到第五个bit。
highPriority 也是一个 std::vector::reference 对象的一份拷贝,所以 highPriority 也在 temp 中包含一个
指向word的指针,加上偏移定位到第五个bit。在这个声明的结尾, temp 被销毁,因为它是个临时对象。因此, highPriority 包含一个野指针。
不可见”的代理类不能和 auto 和平共处
std::vector::reference 是代理类的一个例子:一个类的存在是为了模拟和对外行为和另外一个类保持一致。
一些代理类被设计用来隔离用户,这就是 std::shared_ptr 和 std::unique_ptr 的情况;
一些代理类是为了一些或多或少的不可见性;
同时在一些C++库里面的类存在一种被称作表达式模板的技术,例如下面的表达式,
Matrix sum = m1 + m2 + m3 + m4;
为了计算的更快, Matrix 的 operator+ 返回一个结果的代理(类似于 Sum<Matrix, Matrix> 的代理类)而不是结果本身Matrix 对象,这里会有一个隐式的从代理类到 Matrix 的转换,这个可能允许 sum 从由 = 右边的表达式产生的代理对象进行初始化;
(其中的对象可能会编码整个初始化表达式,也就是,变成一种类似于 Sum<Sum<Sum<Matrix,Matrix>, Matrix>, Matrix> 的类型。这是一个客户端需要屏蔽的类型。)
作为一个通用的法则,“不可见”的代理类不能和 auto 愉快的玩耍。这种类常常它的生命周期
不会被设计成超过一个单个的语句,所以创造这样的类型的变量是会违反库的设计假定。这
就是 std::vector::reference 的情况,而且我们可以看到这种违背约定的做法会导致未
定义的行为。
因此你要避免使用下面的代码的形式:
auto someVar = expression of "invisible" proxy class type;
如果发现代理类?
熟悉库文档;
看头文件,有函数签名常常可以表征它们的存在;
namespace std { // from C++ Standards
template <class Allocator>
class vector<bool, Allocator> {
public:
…
class reference { … };
reference operator[](size_type n); // std::vector<T> 的 operator[] 常常返回一个 T& ,
//在这个例子中的这种非常规的 operator[] 的返回类型一般就表征了代理类的使用
…
};
}
显式的类型初始化原则
下面就是一个强制 highPriority 类型是 bool 的例子:
auto highPriority = static_cast<bool>(features(w)[5]);
另外一个例子
auto sum = static_cast<Matrix>(m1 + m2 + m3 + m4);
例子
double calcEpsilon(); // 返回方差
//calcEpsilon 明确的返回一个 double ,但是假设你知道你的程序, float 的精度就够了的时候,
//而且你要关注 double 和 float 的长度的区别。你可以声明一个 float 变量去存储 calcEpsilon 的结果:
float ep = calcEpsilon(); // 隐式转换double到float
//但是这个会很难表明“我故意减小函数返回值的精度”,一个使用显式的类型初始化原则是这样做的:
auto ep = static_cast<float>(calcEpsilon());