wizlyk

wizlyk的代码小天地

0%

Moving to Modern C++ - 《Effective Modern C++》阅读笔记(三)

Item 7: 辨别生成对象时()和{}的不同
Item 8: 多用 nullptr 代替 0 和 null
Item 9: 多用 alias declarations 而不是 typedefs
Item 10: 多用 scoped enums 而不是 unscoped enums
Item 11: 使用 deleted 函数而不是 private undefined 函数
Item 12: 将重写的函数声明为 override

本文所有内容参考于 《Effective Modern C++》(Scott Meyers)一书,仅供个人学习

Moving to Modern C++

Item 7:辨别生成对象时()和{}的不同

当一个类没有以 initializer_list 为参数的构造函数时,使用()和{}并无二致:

1
2
3
4
5
6
7
8
9
10
class Widget {
public:
Widget(int i, bool b); // ctors not declaring
Widget(int i, double d); // std::initializer_list params

};
Widget w1(10, true); // calls first ctor
Widget w2{10, true}; // also calls first ctor
Widget w3(10, 5.0); // calls second ctor
Widget w4{10, 5.0}; // also calls second ctor

但若有了以 initializer_list 为参数的构造函数,使用{}会优先匹配对应的构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Widget {
public:
Widget(int i, bool b); // as before
Widget(int i, double d); // as before
Widget(std::initializer_list<long double> il); // added

};

Widget w1(10, true); // uses parens and, as before,
// calls first ctor
Widget w2{10, true}; // uses braces, but now calls
// std::initializer_list ctor
// (10 and true convert to long double)
Widget w3(10, 5.0); // uses parens and, as before,
// calls second ctor
Widget w4{10, 5.0}; // uses braces, but now calls
// std::initializer_list ctor
// (10 and 5.0 convert to long double)

这里可以看到,true 和 10 等都被隐式类型转换成了 long double,可见 initializer_list 的构造函数的优先级非常高。
它的优先级甚至高于了移动构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Widget {
public:
Widget(int i, bool b); // as before
Widget(int i, double d); // as before
Widget(std::initializer_list<long double> il); // as before
operator float() const; // convert
// to float
};

Widget w5(w4); // uses parens, calls copy ctor
Widget w6{w4}; // uses braces, calls
// std::initializer_list ctor
// (w4 converts to float, and float
// converts to long double)
Widget w7(std::move(w4)); // uses parens, calls move ctor
Widget w8{std::move(w4)}; // uses braces, calls
// std::initializer_list ctor
// (for same reason as w6)

待编辑:说实话,这里并不太懂,w4会直接使用operator()?

1
2
3
4
5
6
7
8
9
10
class Widget {
public:
Widget(int i, bool b); // as before
Widget(int i, double d); // as before
Widget(std::initializer_list<bool> il); // element type is
// now bool
// no implicit
}; // conversion funcs

Widget w{10, 5.0}; // error! requires narrowing conversions

从上面这例可以看出,编译器非常倾向于使用 std::initializer_list 的构造函数,从而使得正确匹配参数的构造函数并不会被使用。另外还有个小知识点:{}初始器中,缩小范围的隐式类型转换会报错。
只有在无法进行类型转换的情况下,编译器才会考虑其他的构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Widget {
public:
Widget(int i, bool b); // as before
Widget(int i, double d); // as before
// std::initializer_list element type is now std::string
Widget(std::initializer_list<std::string> il);
// no implicit
}; // conversion funcs

Widget w1(10, true); // uses parens, still calls first ctor
Widget w2{10, true}; // uses braces, now calls first ctor
Widget w3(10, 5.0); // uses parens, still calls second ctor
Widget w4{10, 5.0}; // uses braces, now calls second ctor

那么,当初始化是使用空的 {} 会怎么样呢?是空的 initializer_list 还是调用默认构造函数?结论是调用默认构造函数。当然了,如果你想使用空的 (),那会变成调用一个对应名字的函数了:

1
2
3
4
5
6
7
8
9
10
class Widget {
public:
Widget(); // default ctor
Widget(std::initializer_list<int> il); // std::initializer_list ctor
// no implicit
}; // conversion funcs

Widget w1; // calls default ctor
Widget w2{}; // also calls default ctor
Widget w3(); // most vexing parse! declares a function!

如果真的想调用空的 initializer_list,需要两重括号:

1
2
3
Widget w4({}); // calls std::initializer_list ctor
// with empty list
Widget w5{{}}; // ditto

对于模板编写者来说,若想自己定义对于 () 中参数的反应,可以像如下代码这样:

1
2
3
4
5
6
7
8
9
template<typename T, // type of object to create
typename... Ts> // types of arguments to use
void doSomeWork(Ts&&... params)
{
T localObject1(std::forward<Ts>(params)...); // using parens
// 或者像下面这样
T localObject2{std::forward<Ts>(params)...}; // using braces

}

这样,对于下面的代码,localObject1 会是一个由10个元素组成的 vector,而 localObject2 是由2个元素组成的 vector。

1
doSomeWork<std::vector<int>>(10, 20);

Things to Remember

  • Braced initialization is the most widely usable initialization syntax, it prevents narrowing conversions, and it’s immune to C++’s most vexing parse.
  • During constructor overload resolution, braced initializers are matched to std::initializer_list parameters if at all possible, even if other constructors offer seemingly better matches.
  • An example of where the choice between parentheses and braces can make a significant difference is creating a std::vector<numeric type> with two arguments.
  • Choosing between parentheses and braces for object creation inside templates can be challenging.

Item 8:多用 nullptr 代替 0 和 null

当 C++ 发现一个需要用指针的地方出现了0时,它通常会把0看作是一个空指针。然而,C++主要的原则还是将0看作是一个int。NULL 也类似,编译允许在一些情况下将 NULL 当成一个整数(如 long)。 在 C++98 中,使用0和 NULL 的重载可能会有一些意外:

1
2
3
4
5
6
7
void f(int); // three overloads of f
void f(bool);
void f(void*);

f(0); // calls f(int), not f(void*)
f(NULL); // might not compile, but typically calls
// f(int). Never calls f(void*)

如果 NULL 的定义是 0L,那么编译的结果可能是模糊的,因为从 long 到 int 和从 0L 到 void* 是同样优先的。
所以,之所以推荐使用 nullptr,就是因为 nullptr 没有 int 类型,从而可以把它看成是一个可以代表所有类型的空指针。

1
f(nullptr); // calls f(void*) overload

在 cppreference 网站上找到个有意思的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template<class F, class A>
void Fwd(F f, A a)
{
f(a);
}

void g(int* i)
{
std::cout << "Function g called\n";
}

int main()
{
g(NULL); // Fine
g(0); // Fine

Fwd(g, nullptr); // Fine
// Fwd(g, NULL); // ERROR: No function g(int)
}

这个例子有意思在哪呢?可以知道,如果单纯的调用函数 g:

1
2
g(0);
g(NULL);

是不会出错的,0 和 NULL 都被转换成了空指针;然而模板中 0 和 NULL 都被推断为 int 类型,从而导致了错误。这种错误排查起来可是很令人头疼的。

Things to Remember

  • Prefer nullptr to 0 and NULL.
  • Avoid overloading on integral and pointer types.

Item 9:多用 alias declarations 而不是 typedefs

当定义较长且常用时,C++11 以前一般用 typedef 来简化代码:

1
typedef std::unique_ptr<std::unordered_map<std::string, std::string>> UPtrMapSS;

C++11 提供了 alias declarations:

1
using UPtrMapSS = std::unique_ptr<std::unordered_map<std::string, std::string>>;

他们俩提供了相同的作用。那么为什么还提倡使用 alias declarations 呢?首先 alias declarations 在涉及到函数指针时更直观:

1
2
3
4
5
6
// FP is a synonym for a pointer to a function taking an int and
// a const std::string& and returning nothing
typedef void (*FP)(int, const std::string&); // typedef

// same meaning as above
using FP = void (*)(int, const std::string&); // alias declaration

其次, alias declarations 可以模板化,而 typedef 不能:

1
2
3
template<typename T> // MyAllocList<T>
using MyAllocList = std::list<T, MyAlloc<T>>; // is synonym for std::list<T, MyAlloc<T>>
MyAllocList<Widget> lw; // client code
1
2
3
4
5
template<typename T> // MyAllocList<T>::type
struct MyAllocList { // is synonym for
typedef std::list<T, MyAlloc<T>> type; // std::list<T,
}; // MyAlloc<T>>
MyAllocList<Widget>::type lw; // client code

当使用 typedef 时,如果我们想用 MyAllocList 在模板里创建一个对象,需要使用 typename:

1
2
3
4
5
6
template<typename T>
class Widget { // Widget<T> contains
private: // a MyAllocList<T>
typename MyAllocList<T>::type list; // as a data member

};

在这里,MyAllocList<T>::type 是一个依赖于参数T的依赖类型(dependent type),而C++要求依赖类型必须使用 typename。
而若是使用 alias template,代码会明快很多:

1
2
3
4
5
6
7
8
9
template<typename T>
using MyAllocList = std::list<T, MyAlloc<T>>; // as before

template<typename T>
class Widget {
private:
MyAllocList<T> list; // no "typename",
// no "::type"
};

之所以会这样,是因为使用 alias template 的 MyAllocList 必定是一个类型,而使用 typedef 的 MyAllocList::type 并不一定是类型,所以后者需要使用 typename 来保证编译不会出错。作者在这顺带吐槽了一下:“That sounds crazy, but don’t blame compilers for this possibility. It’s the humans who have been known to produce such code.”

在 C++11 中,有个叫 type_traits 的标准库,提供了允许用户得到不同的参数类型:

1
2
3
std::remove_const<T>::type // yields T from const T
std::remove_reference<T>::type // yields T from T& and T&&
std::add_lvalue_reference<T>::type // yields T& from T

注意这里都使用了 ::type。没错,这是 C++11 的,之所以不用 alias template 是由于一些历史原因。在 C++14 中,alias template 的 type_traits 也被加上了:

1
2
3
4
5
6
std::remove_const<T>::type // C++11: const T → T
std::remove_const_t<T> // C++14 equivalent
std::remove_reference<T>::type // C++11: T&/T&& → T
std::remove_reference_t<T> // C++14 equivalent
std::add_lvalue_reference<T>::type // C++11: T → T&
std::add_lvalue_reference_t<T> // C++14 equivalent

“The C++11 constructs remain valid in C++14, but I don’t know why you’d want to use them.”

当然了,如果你想自己实现 C++14 类似的 alias templates,可以像下面这样做:

1
2
3
4
5
6
7
8
template <class T>
using remove_const_t = typename remove_const<T>::type;

template <class T>
using remove_reference_t = typename remove_reference<T>::type;

template <class T>
using add_lvalue_reference_t = typename add_lvalue_reference<T>::type;

Things to Remember

  • typedefs don’t support templatization, but alias declarations do.
  • Alias templates avoid the “::type” suffix and, in templates, the “typename” prefix often required to refer to typedefs.
  • C++14 offers alias templates for all the C++11 type traits transformations.

Item 10:多用 scoped enums 而不是 unscoped enums

在 C++98 有 enum 类型,它的用法是这样的:

1
enum Color { black, white, red };

然而这样使用会有隐患,比如若在这个定义的下面加上这样一句,就会出错:

1
auto white = false;

这是由于 enum 里面的变量是包括在整个和 Color 一样的参数空间里的,即 Color 在哪有效,这些 black 等就在哪有效。 这样的范围没有被限制的 enums 被称作 unscoped enums。 为了避免这种隐患,C++11 引入了 scoped enums:

1
2
3
4
5
6
7
8
enum class Color { black, white, red }; // black, white, red
// are scoped to Color
auto white = false; // fine, no other "white" in scope
Color c = white; // error! no enumerator named
// "white" is in this scope
Color c = Color::white; // fine
auto c = Color::white; // also fine (and in accord
// with Item 5's advice)

Scoped enums 也被称为 enum classes。
除了作用范围以外,unscoped enums 和 scoped enums 还有类型转换上的差距:

1
2
3
4
5
6
7
8
enum Color { black, white, red }; // unscoped enum
vector<size_t> primeFactors(size_t x);
Color c = red;
...
if (c < 14.5) { // compare Color to double (!)
auto factors = primeFactors(c);// compute prime factors of a Color (!)

}

在 unscoped enums 里面的 enumerators 是被隐式转换为整数类型的,所以上面的代码不会出错。但是 scoped enums 是没有这种隐式类型转换的:

1
2
3
4
5
6
7
8
9
enum class Color { black, white, red }; // enum is now scoped
Color c = Color::red; // as before, but
// with scope qualifier
if (c < 14.5) { // error! can't compare
// Color and double
auto factors = primeFactors(c); // error! can't pass Color to
// function expecting std::size_t

}

若真的想用,得用显示类型转换:

1
2
3
4
5
if (static_cast<double>(c) < 14.5) { // odd code, but
// it's valid
auto factors = primeFactors(static_cast<std::size_t>(c)); // suspect, but it compiles

}

在 C++98,是不允许 enum 提前声明(forward-declared)的,因为编译器要根据 enum 里面 enumerators 的数量来决定用什么类型来表示这个 enum,从而提高程序的空间使用效率/运行速度。在 C++11 中允许了 forward-declared,这是因为 C++11 默认了 enumerators 的类型。想自己修改也行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum class Status; // underlying type is int
enum class Status: std::uint32_t; // underlying type for
// Status is std::uint32_t
// (from <cstdint>)
enum class Status: std::uint32_t {
good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
audited = 500,
indeterminate = 0xFFFFFFFF
};

enum Color: std::uint8_t;

unscoped enums 也不是一无是处,它 space leaking 的特点也具有一定的价值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using UserInfo =          // type alias; see Item 9
std::tuple<
std::string, // name
std::string, // email
std::size_t> ; // reputation
UserInfo uInfo; // object of tuple type


auto val = std::get<1>(uInfo); // get value of field 1
// 显然这里使用 get<1> 是想要得到 email 地址,然而这并不直观并且不易维护
// 利用 unscoped enums 可以改变这点


enum UserInfoFields { uiName, uiEmail, uiReputation };
UserInfo uInfo; // as before

auto val = std::get<uiEmail>(uInfo); // ah, get value of
// email field

注意上面的代码使用了 unscoped enums 的隐式类型转换。
如果在这里想用使用 scoped enums 也行:

1
2
3
4
5
enum class UserInfoFields { uiName, uiEmail, uiReputation };
UserInfo uInfo; // as before

auto val =
std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>(uInfo);

但这显得有点繁杂,那是不是可以用一个返回 size_t 的函数代替 static_cast 呢?这是一个 tricky 的地方:std::get< > 是一个模板,尖括号里面的值的类型需要在编译时就能知道,否则编译器不能将模板实例化,从而导致编译失败。因此我们需要一个 constexpr 函数模板,它可以返回任意类型的 enum:

1
2
3
4
5
6
template<typename E>
constexpr typename std::underlying_type<E>::type
toUType(E enumerator) noexcept
{
return static_cast<typename std::underlying_type<E>::type>(enumerator);
}

C++14 可以改成:

1
2
3
4
5
6
template<typename E> // C++14
constexpr std::underlying_type_t<E>
toUType(E enumerator) noexcept
{
return static_cast<std::underlying_type_t<E>>(enumerator);
}

或用上 auto:

1
2
3
4
5
6
template<typename E> // C++14
constexpr auto
toUType(E enumerator) noexcept
{
return static_cast<std::underlying_type_t<E>>(enumerator);
}

这样,获取 email 的信息可以这样用了:

1
auto val = std::get<toUType(UserInfoFields::uiEmail)>(uInfo);

Things to remember

  • C++98-style enums are now known as unscoped enums.
  • Enumerators of scoped enums are visible only within the enum. They convert to other types only with a cast.
  • Both scoped and unscoped enums support specification of the underlying type. The default underlying type for scoped enums is int. Unscoped enums have no default underlying type.
  • Scoped enums may always be forward-declared. Unscoped enums may be forward-declared only if their declaration specifies an underlying type.

Item 11:使用 deleted 函数而不是 private undefined 函数

如果你想避免其他开发者使用某些函数,一般来说不定义这个函数就行了(这不显然么)。然而其实并没那么显然,在定义一个类时,C++ 会自己定义一些函数(在 item 17(这里到时加个链接) 中有详细介绍)。比如流的拷贝问题。在C++ 标准库中有个 basic_ios 的模板类,所有的输出、输入流都继承自这个模板类。对流的拷贝的作用并不清楚,所以要避免用户对流进行拷贝。最简单的做法是做一个空的定义,下面的代码来自 C++98(包括注释):

1
2
3
4
5
6
7
8
template <class charT, class traits = char_traits<charT> >
class basic_ios : public ios_base {
public:

private:
basic_ios(const basic_ios& ); // not defined
basic_ios& operator=(const basic_ios&); // not defined
};

但是这样的话成员函数或是友元都有权限使用 private 里面的函数。C++11 有更好的做法—— deleted functions:

1
2
3
4
5
6
7
8
template <class charT, class traits = char_traits<charT> >
class basic_ios : public ios_base {
public:

basic_ios(const basic_ios& ) = delete;
basic_ios& operator=(const basic_ios&) = delete;

};

使用 delete 会让你在编译前就知道对这些函数的调用是不合法的,而若是友元等访问未定义的 private 函数,会在编译时才能知道。
有个小细节,deleted functions 是定义在 public 里的,原因是 C++ 会先检查函数的可访问性(accessibility),然后才检查函数的删除属性(deleted status)。如果放在 private 里面,C++ 可能只会说函数在 private 不能访问,而不是说该函数不能使用。因此,将代码的 private-and-not-defined 成员改成 public deleted 成员有益于改进 error 信息。

另外,deleted functions 可以用在其他的非成员函数上,如我们定义一个函数:

1
bool isLucky(int number);

有这么一些对它的调用:

1
2
3
4
if (isLucky('a')) … // is 'a' a lucky number?
if (isLucky(true)) … // is "true"?
if (isLucky(3.5)) … // should we truncate to 3
// before checking for luckiness?

如果我们想保证函数的输入只有整数的,可以使用 deleted function 来避免使用类型转换的合法类型:

1
2
3
4
bool isLucky(int number); // original function
bool isLucky(char) = delete; // reject chars
bool isLucky(bool) = delete; // reject bools
bool isLucky(double) = delete; // reject doubles and floats

deleted functions 还能避免模板函数的不合适的实例化。比如我们有一个作用于内置指针的模板:

1
2
template<typename T>
void processPointer(T* ptr);

在 C++ 中,有两类特殊的指针。一类是 void* 指针,因为它们不能被解引用或自增、自减;另一类是 char* 指针,因为它们指向的是 C 风格的字符串,而不是单个的 char。想避免使用这些指针类型的调用,可以用 deleted functions:

1
2
3
4
5
template<>
void processPointer<void>(void*) = delete;

template<>
void processPointer<char>(char*) = delete;

另外,使用 const void* 和 const char* 的调用也应该被避免:

1
2
3
4
5
template<>
void processPointer<const void>(const void*) = delete;

template<>
void processPointer<const char>(const char*) = delete;

如果还想彻底点,还有 const volatile void* 和 const volatile char* ,然而暂时不懂 volatile(划掉)。

如果是在类里使用模板,像下面的代码是不会编译的:

1
2
3
4
5
6
7
8
9
10
class Widget {
public:

template<typename T>
void processPointer(T* ptr)
{ … }
private:
template<> // error!
void processPointer<void>(void*);
};

其原因是对模板的特殊规定只能在命名空间(namespace scope)里写,而不能在类空间(class scope)。下面的代码可以实现上面代码的效果:

1
2
3
4
5
6
7
8
9
10
class Widget {
public:

template<typename T>
void processPointer(T* ptr)
{ … }

};
template<>
void Widget::processPointer<void>(void*) = delete; // still public but deleted

总结一下,C++98 的 private-and-not-defined 成员实现的就是 C++11 中 deleted functions 实现的效果,但它在类外不能用,在类内不一定能用,就算能用了也要等到链接(link-time)时才能起作用。所以坚持使用 deleted functions 吧。

Things to Remember

  • Prefer deleted functions to private undefined ones.
  • Any function may be deleted, including non-member functions and template instantiations.

Item 12:将重写的函数声明为 override

虚函数是 C++ 的类的一个工具。你可以在基类(base class)中定义或声明虚函数,并在派生类(derived classed)中重写(override)这个虚函数。然而何时该调用 overrided 的虚函数并不显然。先看下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Base {
public:
virtual void doWork(); // base class virtual function

};
class Derived: public Base {
public:
virtual void doWork(); // overrides Base::doWork ,"virtual" is optional here

};
// create base class pointer to derived class object;
// see Item 21 for info on std::make_unique
std::unique_ptr<Base> upb = std::make_unique<Derived>();

upb->doWork(); // call doWork through base class ptr; derived class function is invoked

上面这个例子,就是通过了一个基类的接口,调用了派生类的函数。
重写(Overring)的发生有以下几个必要要求:

  • 基类函数必须是虚函数(virtual)
  • 基类与派生类的该函数(下称基派函)同名(除了析构函数)
  • 基派函参数一致
  • 基派函const 性(constness)一致
  • 基派函返回类型与 exception specifications 兼容(compatible)

C++11 还加了一点:

  • 基派函引用限定符(reference qualifiers)一致

成员函数的引用限定符可以用来保证函数只被用于左值或右值,如:

1
2
3
4
5
6
7
8
9
10
11
12
class Widget {
public:

void doWork() &; // this version of doWork applies only when *this is an lvalue
void doWork() &&; // this version of doWork applies only when *this is an rvalue
};

Widget makeWidget(); // factory function (returns rvalue)
Widget w; // normal object (an lvalue)

w.doWork(); // calls Widget::doWork for lvalues (i.e., Widget::doWork &)
makeWidget().doWork(); // calls Widget::doWork for rvalues (i.e., Widget::doWork &&)

由重写引起的错误并不会被编译器发现,因为它是往往合法的,只是没有按你的原目的进行。如下面的代码,就完全没有任何重写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Base {
public:
virtual void mf1() const;
virtual void mf2(int x);
virtual void mf3() &;
void mf4() const;
};
class Derived: public Base {
public:
virtual void mf1(); // 没 const
virtual void mf2(unsigned int x); // 多了 unsigned
virtual void mf3() &&; // & &&
void mf4() const; // base 不是 virtual
};

C++11 给出了避免上面这种情况的方案:把要重写的函数声明为 override:

1
2
3
4
5
6
7
class Derived: public Base {
public:
virtual void mf1() override;
virtual void mf2(unsigned int x) override;
virtual void mf3() && override;
virtual void mf4() const override;
};

这样,上面的代码就会因为重写失败而产生错误。

在 C++98 中也有 override:

1
2
3
4
5
6
class Warning { // potential legacy class from C++98
public:

void override(); // legal in both C++98 and C++11
// (with the same meaning)
};

所以如果看见老代码用了 override 函数并不用改。
接下来补充一下引用限定符(reference qualifiers)的内容。假设有个Widget:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Widget {
public:
using DataType = std::vector<double>; // see Item 9 for
// info on "using"
DataType& data() & { return values; }
DataType data() && { return std::move(values); }// for rvalue Widgets, return rvalue

private:
DataType values;
};

// 给出两种调用
Widget w;

auto vals1 = w.data(); // copy w.values into vals1

Widget makeWidget();
auto vals2 = makeWidget().data();

这样,vals1 和 vals2 调用的是不同的 data() 函数。这就是 接受右值的函数定义 的作用。

Things to Remember

  • Declare overriding functions override.
  • Member function reference qualifiers make it possible to treat lvalue and rvalue objects (*this) differently.