前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[Effective Modern C++(11&14)]Chapter 3: Moving to Modern C++

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

原创
作者头像
昊楠Hacking
发布2018-05-26 00:11:05
1.8K0
发布2018-05-26 00:11:05
举报

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

  • C++11中,初始化值的指定方式有三种:括号初始化,等号初始化和花括号初始化;其中花括号初始化是为了解决C++98的表达能力而引入的一种统一初始化思想的实例。
    • 等号初始化和花括号初始化可以用于非静态成员变量的初始化
代码语言:txt
复制
  class Widget {
  ...
  private:
     int x {0}; // ok
     int y = 0; // ok
     int z(0); // error
  };
    • 括号初始化和花括号初始化可以用于不可拷贝对象的初始化
代码语言:txt
复制
std::atomic<int> ai1 {0}; // ok
std::atomic<int> ai2 (0); //ok
std::atomic<int> ai3 = 0; // error
    • 花括号初始化会禁止窄化转型,而等号初始化和括号初始化会自动窄化转型
代码语言:txt
复制
double x, y, z;
...
int sum1 {x+y+z}; // error
int sum2 (x+y+z); // ok
int sum3 = x+y+z; // ok
    • 调用对象的无参构造函数时,使用括号初始化会被编译器错误识别为声明了一个函数,而花括号初始化则能正确匹配到无参构造函数的调用
代码语言:txt
复制
Widget w1(); // error
Widget w2{}; // ok 
    • 花括号初始化与std::initializer_lists和构造函数重载解析的同时出现时容易造成错误调用
      • 在调用构造函数的时候,只要不涉及到std::initializer_list参数,括号和花括号初始化有相同的含义
代码语言:txt
复制
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参数的重载构造函数
代码语言:txt
复制
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构造函数给劫持
代码语言:txt
复制
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构造函数不能被调用,编译器也会优先选择它
代码语言:txt
复制
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的参数类型之间进行转换时,编译器才会重新选择正常的构造函数
代码语言:txt
复制
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构造函数,需要在花括号中嵌套一个空的花括号进行初始化
代码语言:txt
复制
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型重载共存时产生意外匹配调用
代码语言:txt
复制
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,可以被隐式地转换成所有原始指针类型
代码语言:txt
复制
f(nullptr); // calls f(void*)
  • 当在使用模板时,nullptr的优势就发挥出来了,可以转换成任意指针类型
代码语言:txt
复制
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更容易理解
代码语言:txt
复制
typedef void (*FP)(int, const std::string&);
using FP = void(*)(int, const std::string&);
  • alias可以模板化,而typedef不能直接模板化,需要借助结构体来实现
    • 如果要定义一个使用自定义分配器的链表
代码语言:txt
复制
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
代码语言:txt
复制
template<typename T>
class Widget {
    private:
        typename MyAllocList<T>::type list;
        ...
};
      • MyAllocList<T>::type指的是一个取决于模板类型参数T的类型,因此就是一个依赖类型,C++规定依赖类型前面必须加上typename
      • 如果使用alias定义模板,就不需要typename
代码语言:txt
复制
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>的一个特例而它们没看到,例如:
代码语言:txt
复制
class Wine{...};

template<>
class MyAllocList<Wine> {
    private:
        enum class WineType {White, Red, Rose};
        WineType type;  //!!!!!!!!!!!!!!!
        ...
};
  • C++11以类型萃取的形式提供了许多形式转换工具,模板都在<type_traits>头文件中,例如
代码语言:txt
复制
std::remove_const<T>::type
std::remove_reference<T>::type
std::add_lvalue_reference<T>::type
    • 但是要在模板内部使用它们时,仍然要在前面加上typename,因为它们实际上还是用嵌套typedef实现的
    • 但在C++14中,它们有了替代的方案
代码语言:txt
复制
std::remove_const_t<T>
std::remove_reference_t<T>
std::add_lvalue_reference_t<T>
      • 原理显而易见
代码语言:txt
复制
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并非如此,其对外也是可见的
代码语言:txt
复制
enum Color {black, white, red};
auto while = false; // error, while already declared in this scope
  • C++11的新标准,有范围限制的enums,并不会对命名空间造成污染
代码语言:txt
复制
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中枚举常量会被隐式转换成整型类型
代码语言:txt
复制
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<>()
代码语言:txt
复制
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类可以消除这个编译需求,例如
代码语言:txt
复制
#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,底层类型可以指定
代码语言:txt
复制
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中的用途
代码语言:txt
复制
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,略显拖沓
代码语言:txt
复制
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,例如删除某些过时的重载函数
代码语言:txt
复制
bool isLucky(int number);
bool isLucky(char) = delete;
bool isLucky(bool) = delete;
bool isLucky(double) = delete;
    • 虽然删除的函数不能使用,但仍然是程序的一部分,因此,在重载解析过程中也会被纳入考虑中
    • 模板函数可以通过删除来阻止部分实例化函数,而允许其他实例化存在
代码语言:txt
复制
template<typename T>
void processPointer(T* ptr);

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

template<>
void processPointer<char>(char*) = delete;
    • 有意思的是,如果在类里面有一个模板函数,则不能通过设置private来禁用一些实例化,因为不能给一个成员函数的模板特化一个不同于主模板的访问权限,例如
代码语言:txt
复制
class Widget {
    public:
        ...
        template<typename T>
        void processPointer(T* ptr) {...}
    private:
        template<>
        void processPointer<void>(void*); // error
};
      • 问题在于模板特化必须被卸载命名空间范围内,而不是在类范围内,因此可以使用delete来实现
代码语言:txt
复制
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)
代码语言:txt
复制
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能使得编译器检查是否正确覆盖,而不是在没有正确覆盖时隐式地转换成了重载或者其他合法函数,而使得调用时发生意外调用,例如
代码语言:txt
复制
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
};
    • 虽然上面的函数都没有发生覆盖,但是有些编译器认为都是合法的,而不会给出警告,正确的做法是
代码语言:txt
复制
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)
代码语言:txt
复制
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.

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. Distinguish between () and {} when creating objects
  • 2. Prefer nullptr to 0 and NULL
  • 3. Prefer alias declarations to typedefs
  • 4. Prefer scoped enums to unscoped enums
  • 5. Prefer deleted functions to private undefined ones
  • 6. Declare overriding functions override
  • 7. Prefer const_iterators to iterators
  • 8. Declare functions noexcept if they won't emit exceptions
  • 9. Use constexpr whenever possible
  • 10. Make const member functions thread safe
  • 11. Understand special member function generation
  • 12. Summary
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档