《Effective Modern C++》学习总结(条款6- 10)
程序员文章站
2022-07-12 17:50:42
...
条款6:当auto推导出非预期类型时应当使用显式的类型初始化
1.一种特殊情况:auto
在推导vector<bool>
时会返回vector<bool> reference
- 常识是:
std::vector<T>
的operator[]
常常返回一个T&
std::vector<bool> features();
auto ret = features(); //返回vector<bool> reference
bool ret2 = features(); //返回bool(通过隐式转换)
auto ret3 = static_cast<bool>(features); //返回bool(通过static_cast强制casting)
-
vector<bool>
的operator[]
的返回值其实并不是bool
类型,vector<bool>
比较特殊,它返回的是vector<bool>::reference
,为什么要这样呢?返回一个bool引用类型不就完了嘛,标准库干嘛非要这么大费周章的搞了个这样的类型呢?,原因有以下几个:- 因为
bool
占用一个字节,标准库为了节省内存,改用bit来表示; - 因为
operator[]
需要返回一个内部元素的引用,但是没办法对一个bit进行引用; - 为了让返回的类型统一,无论是
bool
类型,还是其它类型;
- 因为
-
为此标准库为了实现上述三个目标就封装了一个内部的类型
vector<bool>::reference
,是一个proxy代理类,具体参见cppreference因此auto在这里老老实实得到了一个vector<bool>::reference
类型。 -
我们可以通过使用
static_cast
强制进行类型转换得到我们想要的类型,以避免上述问题
2.个人思考:
- 如果我都使用强制转换类型了,那还有什么必要使用
auto
呢?使用auto
的目的本来就是想要让编译器帮助我们进行类型推断,以减少重复的类型名使用,那么这样做的意义在哪? 仅仅是为了解决在使用auto
时出现的问题吗?个人的一个思路是——auto
常常用于泛型编程中实现自动推断类型,这样在整个模板在有一处发生变动时由于有auto
的存在可以不用改动太多地方就能继续适用。同时,对于一些基础的内置类型,也可以使用auto
(基本不会犯错)。
3.请记住:
- 对于一些看不见的proxy类型,使用auto对这类初始化表达式进行类型推导会推导出错误的类型。
- 通过显示的类型初始化惯用法可以强制auto推导出目标类型。
第三章:使用Modern C++(C++ 2.0及以上)
条款7:在创建对象时区分()
和{}
1.一般来说,值得初始化有如下三种方式:
int x(0); // initializer is in parentheses
int y = 0; // initializer follows "="
int z{0}; // initializer is in braces
int c = {0}; // initializer uses "=" and braces
std::vector<int> v1(10,20); //使用的是非初始化列表的版本,10个元素,每个元素的值是20
std::vector<int> v2{10,20}; //使用的带初始化列表的版本,2个元素,值分别是10,20
2.等号往往是最容易误导人判断的符号,必须区分赋值与初始化的关系
Widget w1(2); //调用的默认构造函数
Widget w2 = w1; //调用的是拷贝构造函数
w1 = w2; //调用的赋值操作符
3.特例:在C++11中引入的std::atomic
是一个不可拷贝的对象,对于它的初始化是不能利用 y = 0
这种形式的,因为它会调用默认拷贝构造函数。
std::atomic<int> ail{0}; //fine
std::atomic<int> ai3 = 0; //error
- 因此我们得到结论:
y = 0
和x(0)
这两种形式都有其不适用的地方,而z{0}
这种形式则都可以适用,这也就是为什么在C++11中这种初始化方式被称为统一初始化的原因。除此之外统一初始化的这种方式还可以避免窄化的转换和复杂的语法分析。
4.()
与{}
二者最大的区别是:{}
这种初始化方式调用的是带有initializer_list
的构造函数
class Widget {
public:
Widget(int i,bool b);
Widget(int i,double b);
Widget(std::initializer_list<long double> il);
};
Widget(10,true); //调用的是第一个构造函数,
Widget{10,true}; //按理应该是调用第一个构造函数,但是却调用了带初始化列表的构造函数
//特化,而且是宽化转换,10和true都转换成long double
5.得出结论:统一使用{}
进行初始化,这会有效避免很多问题
Widget w1(10); // call Widget ctor with argument 10
Widget w2(); // 对于C++编译器来说需要区别这是一个函数声明还是一个变量的初始化
Widget w3{}; // calls Widget ctor with no args
6.一个特例:The rule is that you get default construction. Empty braces mean no arguments, not an empty std::initializer_list
:
class Widget {
public:
Widget();
Widget(std::initializer_list<int> il);
};
Widget w1; // 调用默认的构造函数
Widget w2{} // 也是调用默认的构造函数
Widget w3(); // 最头疼的解析(most vexing parse) ,
// 此处我们本来是想调用默认构造函数,但实际上编译器认为这是名为w3函数的声明
- 在使用
{}
这种方式进行初始化的时候选择的不是带有std::initializer_list
的构造函数吗?。这里怎么和上面说的不一致呢? 没办法,这是一个特例,如果你想让他调用带有初始化列表的构造函数,你需要像下面这样来调用它:
Widget w3({}); // calls std::initializer_list ctor with empty list
Widget w4{{}}; // ditto
7.请记住:
-
{}
初始化是最广泛的初始化语法,它可以阻止窄化转换,并且避免了C++最复杂的语法解析; - 在构造函数做函数重载的时候,
{}
会优先匹配带有std::initializer_list
参数的版本,即使其他构造函数看起来更匹配; - 对与
std::vector
两个参数的构造函数来说,其{}
和()
两种初始化方式有很大的不同; - 在模版中对于
{}
和()
初始化如何进行选择是一个挑战;
条款8:优先使用nullptr
而不是0
或NULL
1.0
是int
类型,并不是指针类型,但是当0
赋值给一个指针类型的时候,0
将会被解释成空指针,在C++98中关键字NULL
其本质就是一个long int
类型的数值0
,在实际使用过程中这带来了很多模棱两可的问题。
void f(int); //函数f的三个重载
void f(bool);
void f(void*);
f(0); //调用f(int),而非f(void*)
f(NULL); //NULL和0都不属于指针类型,可能无法编译,但是调用f(int),不可能调用f(void*)
2.C++ 11 引入的nullptr
不再是整型了,但它也不是空指针类型,而是std::nullptr_t
类型,更奇怪的是std::nullptr_t
的类型又是nullptr
类型,这是一个循环类型定义,而std::nullptr_t
类型可以隐式转换为任意类型的指针。
-
nullptr
可以理解为“可以指向任意类型的指针”——因为它可以隐式地转换为所有原始的指针类型
f(nullptr) //调用f(void*)
3.0
可以隐式地转换成指针类型,0
本身是int
类型,那么是不是所有的int
类型都可以转换为指针类型呢?
void test(void*);
test(0);
int data = 0;
test(data); //编译不通过 无法从int转换为void*
template<typename FuncType,
typename PtrType>
decltype(auto) Call(FuncType func,PtrType ptr) {
return func(ptr);
}
Call(test,0); //编译不通过,0经过模板类型推导后变成了int类型的ptr
//而int类型是无法转换为指针类型的
//如果在这里把0换成nullptr就可以调用成功了
4.在泛型编程领域,nullptr
的作用更加明显——对于模板的类型推导不会造成额外的困扰
5.请记住:
- 优先使用
nullptr
替换0
和NULL
- 避免同时重载带有整型参数和指针类型的参数
条款9:优先使用alias declarations
而不是typdef
1.C++11中引入了一个using
别名的机制,与typedef
的使用如下:
typedef void(*FP)(int,const std::string&);
using FP = void(*)(int,const std::string&);
template<typename T>
using aliasList = std::list<T>;
aliasList<int> li;
template<typename T>
struct aliasList {
typedef std::list<T> type;
};
aliasList<int>::type li;
2.首先,二者具有完全一样的意义,但也有所不同:
-
用声明别名可以使涉及到函数指针的类型的声明变得容易理解
-
在模板中:
typedef
没有办法在模板声明的作用域中做类型重定义,必须放在一个自定义类型作用域内。而using
没有这个限制 -
对于嵌套类型来说,
using
不需要使用typename
(不会有::type
这样的后缀,它会让编译器迷惑这到底是类型还是成员变量),而typedef
必须使用其来说明其嵌套类型
template<typename T> //Widget<T> 包含
class Widget { //一个MyAloocList<T>
private: //作为一个数据成员
typename MyAllocList<T>::type list;
...
};
template<typname T>
using MyAllocList = std::list<T, MyAlloc<T>>; //和以前一样
template<typename T>
class Widget {
private:
MyAllocList<T> list; //没有typename
... //没有::type
};
3.C++14给所有的C++11模版类型萃取提供了别名
std::remove_const<T>::type //C++11: const T -> T
std::remove_const_t<T> //等价的C++14
std::remove_reference<T>::type //C++11: T&/T&& -> T
std::remove_reference_t<T> //等价的C++14
std::add_lvalue_reference<T>::type //C++11: T -> T&
std::add_lvalue_reference_t<T> //等价的C++14
4.请记住:
-
typedef
不支持模版化,但是using
的别名声明可以 - 模版别名避免了传统的
typedef
带来的::type
后缀,以及在类型引用的时候需要的typename
前缀 - C++14给所有的C++11模版类型萃取提供了别名
条款10:优先使用作用域限制的enums
而不是无作用域的enum
1.通常来说我们在花括号中定义的名称其作用域就在花括号中,但是C++98的枚举类型的声明却不遵从这个规则。
enum Color {black,white,red};
auto white = false; //编译出错,white已经声明了
- 事实上,上面这些枚举名称都暴露到外层的作用域中了,官方称这种枚举类型成为unscoped,也就是无作用限制的,
2.在C++11中引入了scoped的枚举类型(也被称作枚举类enum class
),枚举的名称不会暴露到外层作用域中
enum class Color {black,white,red}; //black, white, red 作用域为 Color
auto white = false; //fine, 在这个作用域内没有其他的"white"
Color c = white; //错误!在这个定义域中没有叫"white"的枚举元素
Color c = Color::white; //fine
auto c = Color::white; //更好,使用auto避免错误(条款5)
- 限制作用域的
enum
可以减少命名空间的污染; - 同时,它们的枚举元素可以是更丰富的类型(无作用域的
enum
会将枚举元素隐式的转换为整数类型)——在scoped enum class
中不存在从枚举元素到其他类型的隐式转换;
enum Color {black,white,red};
Color c = red;
if (c < 14.5) { //和浮点型进行比较
.....
}
enum class CColor {black,white,red};
CColor cc = Color::red;
if (cc < 14.5) { //编译出错,无法进行隐式类型转换。
....
}
- 如果非要进行转换,可以借助与
static_cast
来进行显式转换;
if(static_cast<double>(c) < 14.5) { // 怪异但是有效的代码
auto factors = // 感觉不可靠
primeFactors(static_cast<std::size_t(c)); //但是可以编译
...
}
- scoped类型还可以前向声明,这样可以加快编译的速度;
enum Color; //编译不通过
enum class Color; //编译通过
3.unscoped枚举类型的实际类型并不是enum
,它有一个底层存储类型。而这个底层存储类型是编译器在编译的时候决策的,根据你的取值范围来定义你的底层存储类型。
enum Color : std::uint8_t {black,white,red}
enum Color : std::uint8_t; //前向声明
- 上面就是unscoped enum的前向声明
- 如果默认的类型不适用于你,你可以重载它:
enum class Status: std::uint32_t; //Status潜在类型是
//std::uint32_t
//来自<cstdint>)
4.当然unscoped也有它的优点:把一些无意义的数值有意义化,典型的比如函数返回值,下标位置等。
using UserInfo = std::tuple<std::string,std::string,std::size_t>;
UserInfo uInfo;
auto val = std::get<1>(uInfo)
- 上面使用了C++11中的tuple来表示一个用户的姓名,email,和年龄等信息,通过
std::get<1>
取出email信息,很明显数值1
在这里是无意义的不易读,如果换成枚举类型就会很易读了。
enum UserInfoFields {uiName,uiEmail,uiAge}; //unscoped enum 易读
auto val = std::get<uiEmail>(uInfo)
auto val = std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>(uInfo);
//如果使用scoped enum就会很臃肿
- 需要先转换为
std::size_t
类型,这里有点投机取巧,不应该转换为std::size_t
类型的,应该转换为枚举类型的底层存储类型,因为如果底层存储类型比较大,转换成std::size_t
可能会导致窄化。为此需要有个手段获取枚举类型的底层存储类型。而且还需要是编译时获取,因为获取的值是作为std::get
这个模板的参数。
template<typename E>
constexpr auto toUType(E enumerator) noexcept //必须是一个constexpr(条款15)
{
return static_cast<std::underlying_type_t<E>>(enumerator);
}
auto val = std::get<toUType(UserInfoFields::uiEmail)>(uInfo);
5.请记住:
- C++98种的枚举众所周知是无作用域限制的;
- C++11中的枚举类是有作用域限制的,不能进行隐式的类型转换需要使用C++的类型cast进行转换;
- 无论是枚举类还是传统的枚举类型都支持指定底层的存储,对于枚举类来说默认的底层存储类型是int,而传统的枚举类型其底层存储是未知的,需要在编译器进行选择;
- 枚举类总是可以进行前向声明的,而枚举类型则不行,必须是在明确指定其底层存储的时候才能进行前向声明。
上一篇: Effective Java(6~10)
下一篇: effective c++条款10