[Effective Modern C++(11&14)]Chapter 3: Moving to Modern C++

1. Distinguish between () and {} when creating objects

  • C++11中,初始化值的指定方式有三种:括号初始化,等号初始化和花括号初始化;其中花括号初始化是为了解决C++98的表达能力而引入的一种统一初始化思想的实例。
    • 等号初始化和花括号初始化可以用于非静态成员变量的初始化
  class Widget {
  ...
  private:
     int x {0}; // ok
     int y = 0; // ok
     int z(0); // error
  };
    • 括号初始化和花括号初始化可以用于不可拷贝对象的初始化
std::atomic<int> ai1 {0}; // ok
std::atomic<int> ai2 (0); //ok
std::atomic<int> ai3 = 0; // error
    • 花括号初始化会禁止窄化转型,而等号初始化和括号初始化会自动窄化转型
double x, y, z;
...
int sum1 {x+y+z}; // error
int sum2 (x+y+z); // ok
int sum3 = x+y+z; // ok
    • 调用对象的无参构造函数时,使用括号初始化会被编译器错误识别为声明了一个函数,而花括号初始化则能正确匹配到无参构造函数的调用
Widget w1(); // error
Widget w2{}; // ok 
    • 花括号初始化与std::initializer_lists和构造函数重载解析的同时出现时容易造成错误调用
      • 在调用构造函数的时候,只要不涉及到std::initializer_list参数,括号和花括号初始化有相同的含义
class Widget {
    public:
        Widget(int i, bool b);
        Widget(int i, double d);
        ...
};

Widget w1(10, true); // calling 1
Widget w2{10, true}; // calling 2
Widget w3(10, 5.0);  // calling 1
Widget w4{10, 5.0};  // calling 2
      • 如果涉及到std::initializer_list参数,在使用花括号初始化时,编译器会强烈地偏向于调用使用std::initializer_list参数的重载构造函数
class Widget {
    public:
        Widget(int i, bool b);
        Widget(int i, double d);
        Widget(std::initializer_list<long double> il);
        ...
};

Widget w1(10, true); // calling 1
Widget w2{10, true}; // calling 3, 10 and true convert to long double
Widget w3(10, 5.0);  // calling 1
Widget w4{10, 5.0};  // calling 3 , 10 and 5.0 convert to long double
        • 甚至本来应该调用拷贝构造函数或者移动构造函数,也会被std::initializer_list构造函数给劫持
Widget w5(w4); // copy construction 
Widget w6{w4}; // std::initializer_list construction
Widget w7(std::move(w4)); // move construction
Widget w8{std::move(w4)}; // std::initializer_list construction
        • 编译器非常偏向选择std::initializer_list构造函数,以至于即便最匹配的std::initializer_list构造函数不能被调用,编译器也会优先选择它
class Widget {
   public:
       Widget(int i, bool b);
       Widget(int i, double d);
       Widget(std::initializer_list<bool> il);
       ...
};

Widget w{10, 5.0}; // error, requires narrowing conversions
        • 只有当没有办法在花括号初始化的参数类型和std::initializer_list的参数类型之间进行转换时,编译器才会重新选择正常的构造函数
class Widget {
   public:
       Widget(int i, bool b);
       Widget(int i, double d);
       Widget(std::initializer_list<std::string> il);
       ...
};

Widget w1(10, true); // calling 1
Widget w2{10, true}; // calling 1
Widget w3(10, 5.0);  // calling 2
Widget w4{10, 5.0};  // calling 2
        • 当类同时支持默认构造函数和std::initializer_list构造函数时,此时调用空的花括号初始化,编译器会解析为调用默认构造函数,而要解析成std::initializer_list构造函数,需要在花括号中嵌套一个空的花括号进行初始化
class Widget {
    public:
        Widget();
        Widget(std::initializer_list<int> il);
        ...
};

Widget w1; // calling 1
Widget w2{}; // calling 1
Widget w3{{}}; // calling 2
Widget w4({}); // calling 2

2. Prefer nullptr to 0 and NULL

  • C++会在需要指针的地方把0解释成指针,但是需要策略还是把0解释成int
  • C++98中上面这种做法会使得在指针和int型重载共存时产生意外匹配调用
void f(int);
void f(bool);
void f(void*);
f(0);  // calls f(int)
f(NULL); // might not compile, but typically calls f(int)
  • nullptr的优点在于它没有一个整型类型,也没有一个指针类型,但是可以代表所有类型的指针,nullptr的实际类型是nullptr_t,可以被隐式地转换成所有原始指针类型
f(nullptr); // calls f(void*)
  • 当在使用模板时,nullptr的优势就发挥出来了,可以转换成任意指针类型
int f1(std::shared_ptr<Widget> spw);
int f2(std::unique_ptr<Widget> upw);
bool f3(Widget* pw);
std::mutex f1m, f2m, f3m;

template<typename FuncType, typename MuxType, typename PtrType>
auto lockAndCall(FuncType func, MuxType& mutex, PtrType ptr) -> decltype(func(ptr))
{
     using MuxGuard = std::lock_guard<MuxType>;
     MuxGuard g(mutex);
     return func(ptr);
}

auto result1 = lockAndCall(f1, f1m, 0); // error, PtrType is int
auto result2 = lockAndCall(f2, f2m, NULL); // error, PtrType is int / long
auto result3 = lockAndCall(f3, f3m, nullptr); // ok 

3. Prefer alias declarations to typedefs

  • aliastypedef更容易理解
typedef void (*FP)(int, const std::string&);
using FP = void(*)(int, const std::string&);
  • alias可以模板化,而typedef不能直接模板化,需要借助结构体来实现
    • 如果要定义一个使用自定义分配器的链表
template<typename T>
using MyAllocList = std::list<T, MyAlloc<T>>;
MyAllocList<Widget> lw;

template<typename T>
struct MyAllocList {
   typedef std::list<T, MyAlloc<T>> type;
};
MyAllocList<Widget>::type lw;
    • 如果要在模板内部创建一个持有模板参数类型的链表,必须在typedef名字前面加上typename
template<typename T>
class Widget {
    private:
        typename MyAllocList<T>::type list;
        ...
};
      • MyAllocList<T>::type指的是一个取决于模板类型参数T的类型,因此就是一个依赖类型,C++规定依赖类型前面必须加上typename
      • 如果使用alias定义模板,就不需要typename
template<typename T>
using MyAllocList = std::list<T, MyAlloc<T>>; 

template<typename T>
class Widget {
   private:
       MyAllocList<T> list;
       ...
};
      • 此处看起来MyAllocList<T>是一个与模板参数T存在依赖关系的对象,但是当编译器处理Widget模板时,它知道MyAllocList<T>是一个类型的名字,因为MyAllocList是一个别名模板:它必须命名一个类型,因此MyAllocList<T>是一个无依赖类型,也就不需要typename
      • typedef中,当编译器在Widget模板中看到MyAllocList<T>::type时,它们不能确定这是否是一个类型,因为有可能是MyAllocList<T>的一个特例而它们没看到,例如:
class Wine{...};

template<>
class MyAllocList<Wine> {
    private:
        enum class WineType {White, Red, Rose};
        WineType type;  //!!!!!!!!!!!!!!!
        ...
};
  • C++11以类型萃取的形式提供了许多形式转换工具,模板都在<type_traits>头文件中,例如
std::remove_const<T>::type
std::remove_reference<T>::type
std::add_lvalue_reference<T>::type
    • 但是要在模板内部使用它们时,仍然要在前面加上typename,因为它们实际上还是用嵌套typedef实现的
    • 但在C++14中,它们有了替代的方案
std::remove_const_t<T>
std::remove_reference_t<T>
std::add_lvalue_reference_t<T>
      • 原理显而易见
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;

4. Prefer scoped enums to unscoped enums

  • 通常情况下,在花括号内声明一个名字可以限制名字对外的可见性,但是对于C++98enums中的enumerators并非如此,其对外也是可见的
enum Color {black, white, red};
auto while = false; // error, while already declared in this scope
  • C++11的新标准,有范围限制的enums,并不会对命名空间造成污染
enum class Color {black, white, red};
auto white = false; // fine
Color c = white; // error, no enumerator named “white" is in this scope
Color c = Color::white; // fine
auto c = Color::white; // fine
  • 有范围限制enums中的枚举常量有更强的类型,而对于无范围限制的enums中枚举常量会被隐式转换成整型类型
enum Color {black, white, red};
std::vector<std::size_t> primeFactors(std::size_t x);

Color c = red;
...
if( c < 14.5){ // compare Color to double!!
   auto factors = primeFactors(c); // compute prime factors of a Color!!
   ...
}

enum class Color {black, white, red};
Color c = Color::red;
...
//error, can't compare Color and double!!!
if( c < 14.5){ 
//error, can't pass Color to function expecting std::size_t
    auto factors = primeFactors(c); 
    ...
}
  • 如果要把C++11中的enums变量转换成其他类型,需要使用static_cast<>()
if( static_cast<double>(c) < 14.5 ){ // valid
    auto factors = primeFactors(static_cast<std::size_t>(c)); // valid
    ...
}
  • C++中每个enum都有一个由编译器决定的整型底层类型,为了有效利用内存,编译器通常会选择足够代表枚举量范围的最小的底层类型,为此,C++98只支持enum定义(列出所有的枚举值),而不支持声明,这使得在使用enum前,编译器能选择一个底层类型。
  • 无法对enum前置声明有许多缺点,最显著的就是增加编译的依赖性,如果一个enum被系统中每个组件都有可能用到,那么都得包含这个enum所在的头文件,如果需要新加入一个枚举值,整个系统就有可能重新编译,即便只有一个函数使用这个新的值
  • C++11中的enum类可以消除这个编译需求,例如
#file 1
enum Status {
   good = 0,
   failed = 1,
   incomplete = 100,
   corrupt = 200,
   audited = 500,
   indeterminate = 0xFFFFFFFF
};

#file 2
enum class Status;
void continueProcessing(Status s);
    • 如果修改了Status,而且continueProcessing没有使用到新的值,那么file2就不需要重新编译
    • 但是如果编译器在使用一个enum之前,需要知道它的大小该怎么办?
      • 对于一个有范围限制的enum,它的底层类型是已知的(默认是int,可以手动覆盖),而对于没有范围限制的enum,底层类型可以指定
enum class Status; //int, declaration 
enum class Status: std::uint32_t; //uin32_t, declaration
enum Color: std::uint8_t;// uint8_t, declaration

enum class Status: std::uint32_t {
    good = 0,
    failed = 1,
    incomplete = 100,
    corrupt = 200,
    audited = 500,
    indeterminate = 0xFFFFFFFF
};
  • 无范围限制的enumC++11std::tuples中的用途
using UserInfo = std::tuple<std::string, std::string, std::size_t>; // name, email, reputation
UserInfo uInfo;
...
//get value of field 1
//but can you always remember what the hell 1 represents?
auto val = std::get<1>(uInfo); 

//Improve
enum UserInfoFields {uiName, uiEmail, uiReputation};
UserInfo uInfo;
...
//implicit conversion from UserInfoFields to std::size_t
//which is the type that std::get requires
auto val = std::get<uiEmail>(uInfo); 
    • 如果要改写成有范围限制的enum,略显拖沓
enum class UserInfoFields {uiName, uiEmail, uiReputation};
UserInfo uInfo;
...
auto val = std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>(uInfo);

5. Prefer deleted functions to private undefined ones

  • 删除的函数和声明为private的函数之间的区别
    • 删除的函数在任何地方都不能使用,所以成员函数和友元函数都不能使用已经删除的函数,否则会编译失败,这在C++98中会推迟到链接阶段才会报错
    • 删除的函数是pulic而不是private,因为当客户端代码试图使用这个删除的成员函数时,C++会首先检查访问权限,后检查删除状态,如果设为private,编译器给出的是权限不足警告而不是函数不可用警告
    • 任何函数都可以是deleted状态,而只有成员函数可以是private,例如删除某些过时的重载函数
bool isLucky(int number);
bool isLucky(char) = delete;
bool isLucky(bool) = delete;
bool isLucky(double) = delete;
    • 虽然删除的函数不能使用,但仍然是程序的一部分,因此,在重载解析过程中也会被纳入考虑中
    • 模板函数可以通过删除来阻止部分实例化函数,而允许其他实例化存在
template<typename T>
void processPointer(T* ptr);

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

template<>
void processPointer<char>(char*) = delete;
    • 有意思的是,如果在类里面有一个模板函数,则不能通过设置private来禁用一些实例化,因为不能给一个成员函数的模板特化一个不同于主模板的访问权限,例如
class Widget {
    public:
        ...
        template<typename T>
        void processPointer(T* ptr) {...}
    private:
        template<>
        void processPointer<void>(void*); // error
};
      • 问题在于模板特化必须被卸载命名空间范围内,而不是在类范围内,因此可以使用delete来实现
class Widget {
    public:
        ...
        template<typename T>
        void processPointer(T* ptr) {...}
        ...  
};

template<>
void Widget::processPointer<void>(void*) = delete;

6. Declare overriding functions override

  • 覆盖产生的必要条件
    • 基类函数必须是virtual
    • 基类和派生类的函数名必须一致
    • 基类和派生类函数的参数类型必须一致
    • 基类和派生类函数的const属性必须一致
    • 基类和派生类函数的返回类型以及异常说明必须兼容
    • 函数的引用修饰必须一致(C++11)
      • 限制成员函数的使用只能是左值或者右值(*this)
class Widget {
   public:
   ...
   void doWork() &; // only when *this is an lvalue
   void doWork() &&; // only when *this is an rvalue
};

...
Widget makeWidget();
Widget w;
...
w.doWork();
makeWidget().doWork();
  • 显式地对成员函数声明override能使得编译器检查是否正确覆盖,而不是在没有正确覆盖时隐式地转换成了重载或者其他合法函数,而使得调用时发生意外调用,例如
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();  // not const 
       virtual void mf2(unsigned int x);  // not int
       virtual void mf3() &&; // not &
       void mf4() const; // not virtual in base
};
    • 虽然上面的函数都没有发生覆盖,但是有些编译器认为都是合法的,而不会给出警告,正确的做法是
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;
};
      • 此时,编译器能检查出所有的错误覆盖

7. Prefer const_iterators to iterators

8. Declare functions noexcept if they won't emit exceptions

9. Use constexpr whenever possible

  • 对于constexpr对象,它们具有const属性,并且它们的值在编译的时候确定(从技术角度讲,是在转换期间确定,转换期包括编译和链接),它们的值也许会被放在只读内存区中,它们的值也能被用在整型常量表达式中,例如数组长度,整型模板参数,枚举值,对齐指示符等等
  • constexpr函数使用constexpr对象时,它们会产生编译期常量,如果constexpr函数使用了运行时的值,它们就会产生运行时的值,但是如果constexpr函数使用的所有参数都是运行时的值,那么就会报错
  • C++11中,constexpr函数只能包含不超过一条return语句的执行语句,但是可以使用条件运算符和递归来实现多重运算。
  • C++14中,constexpr函数的语句数量没有限制,但是函数必须接收和返回字面值类型,也就是指可以在编译期间确定值的类型。
  • 字面值类型包括除了void修饰的类型和带有constexpr修饰的用户自定义类型(因为构造函数和其他成员函数也可能是constexpr)
class Point {
   public:
       constexpr Point(double xVal = 0, double yVal = 0) noexcept: x(xVal), y(yVal) {}
       constexpr double xValue() const noexcept { return x;}
       constexpr double yValue() const noexcept { return y;}
       void setX(double newX) noexcept { x = newX;}
       void setY(double newY) noexcept { y = newY;}
   private:
       double x, y;
};
constexpr Point p1(9.4, 2.7);
constexpr Point p2(28.8, 5.3);

constexpr Point midpoint(const Point& p1, const Point& p2) noexcept
{
     return { (p1.xValue() + p2.xValue()) / 2, (p1.yValue() + p2.yValue()) / 2 };
}

constexpr auto mid = midpoint(p1, p2);
  • C++11中,setXsetY不能被声明为constexpr,因为不能在const成员函数中修改成员变量,而且返回值为void,并不是字面值常量,但是C++14中是允许的

10. Make const member functions thread safe

11. Understand special member function generation

  • 特殊成员函数是C++会自动生成的函数,C++98中有四个这样的函数:默认构造函数,析构函数,拷贝构造函数,拷贝赋值运算符;C++11中多了两个:移动构造函数和移动赋值运算符
  • 两个拷贝操作是无关的,声明一个不会阻止编译器产生另一个
  • 两个移动操作是相关的,声明一个会阻止编译器自动产生另一个
  • 显式声明一个拷贝操作后,移动操作就不会被自动生成,反之依然,理由是:比如声明了拷贝运算,就说明移动操作不适合用于此类
  • 三条规则:如果声明了拷贝构造,拷贝赋值或者析构函数中任何一个,都应该将三个一起声明,因为这三个函数是相互关联的
  • 三条规则暗示了析构函数的出现使得简单的memberwise拷贝不适合类的拷贝操作,也就是说如果声明了析构函数,那么就不应该自动生成拷贝操作相关的函数,因为可能会存在不一致的资源管理行为。同样的,也不应该自动生成移动操作相关的函数。所以,只有当类满足下面三个条件时,移动操作才会自动生成:
    • 没有声明拷贝操作
    • 没有声明移动操作
    • 没有声明析构函数
  • 假如编译器生成的函数行为正确,那么我们只需要在函数名后面加上default就可以了,然编译器接管一切具体事务。

12. Summary

  • 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.
  • 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.
  • C++98-style enum 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 alaways be forward-declared. Unscoped enums may be forward-declared only if their declaration specifies an underlying type.
  • Prefer deleted functions to private undefined ones.
  • Any function may be deleted, including non-member functions and template instantiations.
  • Declare overriding functions override.
  • Member function reference qualifiers make it possible to treat lvalue and rvalue objects (*this) differently.
  • Prefer const_iterators to iterators.
  • In maximally generic code, prefer non-member versions of begin, end, rbegin, etc., over their member function counterparts.
  • noexcept is part of a function's interface, and that means that callers may depend on it.
  • noexcept functions are more optimizable than non-noexcept functions.
  • noexcept is particularly valuable for the move operations, swap, memory deallocation functions, and destructors.
  • Most functions are exception-neutral rather than noexcept.
  • constexpr objects are const and are initialized with values known during compilation.
  • constexpr functions can produce compile-time results when called with arguments whose values are known during compilation.
  • constexpr objects and functions may be used in a wider range of contexts than non-constexpr objects and functions.
  • constexpr is part of an object's or function's interface.
  • Make const member functions thread safe unless you're certain they'll never be used in a concurrent context.
  • Use of std::atomic variables may offer better performance than a mutex, but they're suited for manipulation of only single variable or memory location.
  • The special member functions are those compilers may generate on their own: default constructor, destructor, copy operations, and move operations.
  • Move operations are generated only for classes lacking explicitly declared move operations, copy operations, or a destructor.
  • The copy constructor is generated only for classes lacking an explicitly declared copy constructor, and it's deleted if a move operation is declared. The copy assignment operator is generated only for classes lacking an explicitly declared copy assignment operator, and it's deleted if a move operation is declared. Generation of the copy operations in classes with an explicitly declared destructor is deprecated.
  • Member function templates never suppress generation of special member functions.

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏张善友的专栏

C# 内部类

        C#中的内部类能够使用外部类定义的类型和静态方法,但是不能直接使用外部类的实例方法,直接看来,外部类对于内部类的作用更像是一个命名空间,在C#中...

2318
来自专栏软件开发 -- 分享 互助 成长

排序算法总结

关于各种排序算法的总结表格,这里偷个懒直接用Simple life的博客http://blog.csdn.net/whuslei/article/details...

2145
来自专栏Golang语言社区

深入分析golang多值返回以及闭包的实现

一、前言 golang有很多新颖的特性,不知道大家的使用的时候,有没想过,这些特性是如何实现的?当然你可能会说,不了解这些特性好像也不影响自己使用golang,...

4926
来自专栏微信公众号:Java团长

深入理解Java:String

按照官方的说法:Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配。

1071
来自专栏Golang语言社区

go interface

Go不是一种典型的OO语言,它在语法上不支持类和继承的概念。 没有继承是否就无法拥有多态行为了呢?答案是否定的,Go语言引入了一种新类型—Interface,它...

3325
来自专栏mathor

异常的捕获与处理

1712
来自专栏Java帮帮-微信公众号-技术文章全总结

Java企业面试——Java基础

1. Java基础部分 1.1 Java中的方法覆盖(Overwrite)和方法重载(Overloading)是什么意思? 重载Overload表示同一个类中...

2784
来自专栏全沾开发(huā)

搞懂JavaScript中的连续赋值

搞懂JavaScript中的连续赋值 前段时间老是被一道题刷屏,一个关于连续赋值的坑。 遂留下一个笔记,以后再碰到有人问这个题,直接...

3936
来自专栏从零开始学 Web 前端

01 - JavaSE之基础及面向对象

byte(-128 ~ 127) short(-32768 ~ 32767) int(-2147483648 ~ 2147483647)

1674
来自专栏Java帮帮-微信公众号-技术文章全总结

【选择题】Java基础测试四(15道)

【选择题】Java基础测试四(15道) 41.以下哪项是接口的正确定义?( B D ) A、 interface B { void print...

4849

扫码关注云+社区

领取腾讯云代金券