(Effective Modern C++) – 第三章

Item 7: Distinguish between () and {} when creating objects.

花括号初始器{}可以指定对象的初始的值,而()则仅仅调用类的构造函数以此实例化对象。

  • 类内中不能定义成员但是可以指定成员的初始值。使用{}或=可以指定而()不行。

class Widget {
  …
private:
  int x{ 0 };                  // fine, x's default value is 0
  int y = 0;                   // also fine
  int z(0);                    // error!
};
  • {}也可初始化那些不能拷贝构造(=)的对象。
  • {}可以阻止对象的收窄转换,而()和=则不会阻止:
double x, y, z;
…
int sum1{ x + y + z };       // error! sum of doubles may
                             // not be expressible as int

缩窄变换
1.从浮点数转换为整数
2.从取值范围大的浮点数转换为取值范围小的浮点数(在编译期可以计算并且不会溢出的表达式除外)
3.从整数转换为浮点数(在编译期可以计算并且转换之后值不变的表达式除外)
4.从取值范围大的整数转换为取值范围小的整数(在编译期可以计算并且不会溢出的表达式除外)
而且变量需要能够被转换

  • {}可以调用默认的构造函数,这样就可以避免一些因想当然而导致的问题。
Widget w1(10);     // call Widget ctor with argument 10

Widget w2();       // most vexing parse! declares a function
                   // named w2 that returns a Widget!
Widget w3{};       // calls Widget ctor with no args

{}带来的一些问题:

  • 构造函数的冲突(包含std::initializer_list的时候)
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)

这样就会导致原先的{}调用变成了调用std::initliazer_list(调用的时候,有必要就可以发生类型转换)如果类内部加入了operator float() const;甚至会导致原先的{}拷贝构造变成调先转换->std::initalizer_list!

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)

编译器选择{}作为std::initliazer_list的参数的优先性高于作为构造函数的参数。如果{}中的值需要发生缩窄变换才能变为std::initliazer_list指定的模板的参数类型(即使其他的构造函接受)。则代码是无效的。

  • 如果不能把{}的参数转换为std::initliazer_list<T>的类型T,那么带有std::initliazer_list的构造函数将成为与其他构造函数相同的候选函数。
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

如果希望传递一个空的{}给以std::initliazer_list<T>作为参数的构造函数:

Widget w4({});        // calls std::initializer_list ctor
                      // with empty list
Widget w5{{}};        // ditto
  • 当添加{}构造函数的时候,类用户的代码的调用构造函数可能会穿线歧义。
  • 模板函数可能无法判断返回的类型应该使用{}还是()构造。
template<typename T,                // type of object to create
         typename... Ts>            // types of arguments to use
void doSomeWork(Ts&&... params)
{
  create local T object from params...
  …
}

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 construc‐
tors 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: Prefer nullptr to 0 and NULL.

  • NULL可以转换为整数类型,造成多个函数的匹配
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*)
  • nullptr的类型是nullptr_t,可以隐式地转换为原始指针。
  •  模板中,推导NULL地类型为int,而int一般不能转换为shared_ptr或者指针

例如:

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

调用:

int    f1(std::shared_ptr<int> spw) { return 1; }  // call these only when
auto new_result1 = lockAndCall(&f1, f1m, NULL);//0无法隐式转换为PtrType推导为int,无法转换为spw的类型
  • 而 nullptr的类型std::nullptr_t可以隐式转换为T*。
Things to Remember
• Prefer nullptr to 0 and NULL.
• Avoid overloading on integral and pointer types.

Item 9: Prefer alias declarations to typedefs.

using可以直接定义模板的别名,而typedef不能直接的定义模板的别名。

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

typedef:

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

如果在模板中使用模板的别名,则需要使用typename关键字指定类型是一个类型而非静态成员或者enum成员:typename MyAllocList<T>::type list。而using的模板别名不需要。

C++14 在C++11的基础上提供了一些使用using的别名:

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

他们的实现类似于:

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: Prefer scoped enums to unscoped enums.

没有限定作用域的enum会将成员暴露在与enum同级别的作用域上。并且能够饮食地转换为整型(甚至实型)。

他们都可以:

  • 他们都可以使用 :类型 指定enum的大小( underlying type)(声明或定义时)enum class Status: std::uint32_t;

限定了作用域地enum:

  • 不能隐式发生转换,有必要的话,使用强制转换:static_cast<std::size_t>()
  • enum也可以识别元组的信息,但是让冗长:
enum class UserInfoFields { uiName, uiEmail, uiReputation };
UserInfo uInfo;                        // as before
…
auto val =
  std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>
    (uInfo);

为了避免冗长,可以使用constexpr、noexcept函数来做这些转换:

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 版本:
template<typename E>                               // C++14
constexpr std::underlying_type_t<E>
  toUType(E enumerator) noexcept
{
  return static_cast<std::underlying_type_t<E>>(enumerator);
}

The even-sleeker auto return type (see Item 3) is also valid in C++14:
template<typename E>                               // C++14
constexpr auto
  toUType(E enumerator) noexcept
{
  return static_cast<std::underlying_type_t<E>>(enumerator);
}

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

不限定作用域的enum:

  • 可以帮助用户识别元组的某些信息:
enum UserInfoFields { uiName, uiEmail, uiReputation };
UserInfo uInfo;                        // as before
…
auto val = std::get<uiEmail>(uInfo);   // ah, get value of
                                       // email field
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 12: Declare overriding functions override.

要在类内使用覆盖关键字必须满足一下条件:

  • 虚函数
  • 在基类和派生类的函数名、参数的类型必须一致。
  • const限定的函数必须一致
  • 返回的类型、异常的限定符(noexcept)必须兼容。
  • 成员函数的引用限定一致(C++14 似乎也是如此)

如果不添加override,有些时候你可能无法看出函数是否真的被覆盖!

使用引用限定的情况:(&和&&需要同时存在或同时没有,const限定也是一样)

class Widget {
public:
  using DataType = std::vector<double>;
  …
  DataType& data() &                // for lvalue Widgets, 
  { return values; }                // return lvalue
  DataType data() &&                // for rvalue Widgets,
  { return std::move(values); }     // return rvalue
  …
private:
  DataType values;
};

调用:

auto vals1 = w.data();             // calls lvalue overload for
                                   // Widget::data, copy-
                                   // constructs vals1
auto vals2 = makeWidget().data();  // calls rvalue overload for
                                   // Widget::data, move-
                                   // constructs vals2
Things to Remember
• Declare overriding functions override.
• Member function reference qualifiers make it possible to treat lvalue and
rvalue objects (*this) differently.

Item 13: Prefer const_iterators to iterators.

在C++14中,可以是用std::cbegin(),std::cend()来创建const_iterator的迭代器,这样有助于江容器或者是数组转换为常量指针。

模拟一个返回常量迭代器:

template <class C>
auto cbegin(const C& container)->decltype(std::begin(container))
{
    return std::begin(container);         // see explanation below
}
Things to Remember
• Prefer const_iterators to iterators.
• In maximally generic code, prefer non-member versions of begin, end,
rbegin, etc., over their member function counterparts.

Item 14: Declare functions noexcept if they won’t emit
exceptions.

noexcept关键字:

  1. 一个不抛出异常的限定
  2. 一个运算符,在编译期求出noexcept的属性值返回一个bool类型。
  3. 一个bool作为参数表示是否抛出异常:noexcept(bool)

如果有noexcept的限定,那么编译器就会做一些优化,这样的话不会记录一些调试的信息。(我觉得用于try..catch..)

RetType function(params) noexcept;     // most optimizable
RetType function(params) throw();      // less optimizable C++98版本的noexcept
RetType function(params);              // less optimizable
  • 有些时候,使用例如push_back函数,他在将元素拷贝到新的更大的内存空间成功之前将就的空间销毁。这样就不会在修改原保存的对象在拷贝的时候发生错误时,导致的原数据被破坏。而现如今的优化,将使用move将原对象移动到新的位置。这样当某一个元素move抛出异常时,之前的原对象被修改了,这样就不能保证不抛出异常了。  “move if you can, but copy if you must”。所以新版本的push_back会检查move是否是保证不会抛出异常的。
The checking is typically rather roundabout. Functions like std::vector::push_back call
std::move_if_noexcept, a variation of std::move that conditionally casts to an rvalue (see Item 23),
depending on whether the type’s move constructor is noexcept. In turn, std::move_if_noexcept consults
std::is_nothrow_move_constructible, and the value of this type trait (see Item 9) is set by compilers,
based on whether the move constructor has a noexcept (or throw()) designation.
  • swap函数:swap函数也会检查交换函数是否会抛出异常:

交换

#include <stdio.h>
#include <string.h>
#include <stdexcept>
#include <utility>
using std::pair;
struct d
{
	~d() noexcept(true)
	{
		throw std::logic_error("fgergr");
	}
};
template<typename T>
void swap(T t1, T t2) noexcept(false)
{
	//TO DO
}
template<typename T,typename T::size_t N>
void swap(T(&t1)[N], T(&t2)[N]) noexcept(swap(*t1,*t2))
{
	//TO DO
}
int main()
{
	int f[3] = { 1,2,3 };
	int g[3];
	bool exp1=noexcept(swap(f, g));//false
	return 0;
}

swap整个容器(数组为例子),他是否抛出异常取决于交换单个元素是否会抛出异常。用作者的原话是:

swapping higher-level data structures can generally be noexcept only if
swapping their lower-level constituents is noexcept should motivate you to offer
noexcept swap functions whenever you can.

接口swap、move并不一定声明为noexcept,但是没有抛出异常加上也无妨。

The interface specifications for move operations on containers in the Standard Library lack noexcept. How‐
ever, implementers are permitted to strengthen exception specifications for Standard Library functions, and,
in practice, it is common for at least some container move operations to be declared noexcept. That practice
exemplifies this Item’s advice. Having found that it’s possible to write container move operations such that
exceptions aren’t thrown, implementers often declare the operations noexcept, even though the Standard
does not require them to do so.
  • 如果一开始的函数的实现有noexcept限定,但是后来的修改抛出了异常,这样函数的调用者就无法捕获异常而造成程序终止(先前的noexcept保证不抛出异常)
  • exception-neutral函数允许抛出异常,异常沿着调用链栈传递。这样的函数不使用noexcept。如果你非常确定不跑出异常,那么使用noexcept限定是很不错的。
  • 在函数中使用函数返回的状态值(用于表示失败或成功)、捕获调用其他函数产生的异常等会使得代码变得复杂和难以理解。
  • 在C++11中,无论是用户定义的或是编译期生成的都会隐式地定义为noexcept。例外是,类地数据成员地析构函数声明为了“noexcept(false)”
  • 值得注意的是一些库接口的设计者以wide contract和narrow contract区分函数。wide contract函数没有前提条件,不管程序处于什么状态都可以调用,也不会限制调用者传递给它的参数,而且wide contract函数从不呈现未定义行为。

现在假如f函数有一个前提条件:参数std::string的长度不能超过32。如果f的参数std::string长度大于32,那么函数行为是未定义的,因为根据定义,违反前提条件会导致未定义行为。f没有义务检查参数是否符合前提条件,因为函数假定它们的前提条件是满足的(调用者有责任确保这个假定有效)。然后呢,就算有前提条件,把f声明为noexcept看起来也是合适的:

void f(const std::string& s) noexcept;    // precondition:
                                          // s.length() <= 32

调用预先检查是必要的:在此时中捕获异常要比追踪未定义地行为简单。

  • 在noexcept限定的函数调用没有noexcept限定的函数也是合法的(具体的行为去决议外部函数是否抛出异常,尽管没有农noexcept限定)
void setup();           // functions defined elsewhere
void cleanup();
void doWork() noexcept
{
  setup();              // set up work to be done
  …                     // do the actual work
  cleanup();            // perform cleanup actions
}

这里举例的etup和cleanup假设不抛出异常。

Things to Remember
• 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.

Item 15: Use constexpr whenever possible.

  • 关键字constexpr可以确保定义的变量在编译期时是可以计算的。constexpr是指代的变量本身在编译期已知(顶层const)。
  • constexpr函数可以在编译期返回字面值类型,这样就可以在编译期而非运行时得出结果,大大地提高了程序的运行效率。
  • constexpr初始化的表达式必须是字面值常量表达式: array sizes, integral template arguments (including lengths of std::array objects), enumerator values, alignment specifiers, and more.

虽然指针和引用可以是constexpr,但是他们初始化为0、nullptr或者保存在固定地址的对象。

  • constexpr函数:

1.constexpr函数:可以用在需求编译期间常量的上下文。在这种上下文中,如果你传递参数的值在编译期间已知,那么函数的结果会在编译期间计算。如果任何一个参数的值在编译期间未知,代码将不能通过编译。

2.如果constexpr的参数是在编译期不可计算的,则constexpr的计算会在运行时进行。这样不需要两个函数分别处理编译期和运行期计算的情况。

  • C++11和C++14的限制:

C++11:返回值只能有一个return。

C++14:移除了限制,可以使用一般的语句。计算的在编译期进行。

constexpr int pow(int base, int exp) noexcept       // C++14
{
  auto result = 1;
  for (int i = 0; i < exp; ++i) result *= base;
  return result;
}
  • 字面值类型:

条件:(《C++ Primer》(5e))

  1. 数据成员事是字面值类型
  2. 类至少有constexpr的构造函数
  3. 数据成员还有类内初始值:内置类型必须是一条常量表达式初始化;自定义类型必须使用自己的constexpr的构造函数。
  4. 类必须使用析构函数的默认定义
//字面值类型
class Point {
public:
	constexpr Point(double _x,double _y) noexcept:x(_x),y(_y){}
	constexpr double getX() const noexcept { return x; }
	constexpr double getY() const noexcept  { return y; }
	void setX(double _x) noexcept { x = _x; }
	void setY(double _y) noexcept { y = _y; }
	//mid
private:
	double x;
	double y;
};
constexpr Point midPoint(const Point &p1, const Point &p2) {
	return { (p1.getX() + p2.getX()) / 2,
			(p1.getY() + p2.getY()) / 2
	};
}

浮点型的constexpr变量不能初始化枚举类型,但是可以使用static_cats<T>(x)转换为可以在编译期计算的值,x必须要是constexpr或constexpr函数返回的值。

  • const成员

C++11,中void不是字面值类型。并且constexpr隐式限定为const(this指向的对象我iconst)。C++14解除了这些限制。所以在成员函数中可以修改成员的值。

  • 总结:Part of “whenever possible” in “Use constexpr whenever possible” is your willingness to make a long-term commitment to the constraints it imposes on the objects and functions you apply it to.(待理解)
Things to Remember
• 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.

Item 16: Make const member functions thread safe(等学习了多线程再来回顾)

Item 17: Understand special member function generation

这个主要是类的设计,需要注意编译器对于类生成的缺省的构造函数和运算符。

  • 如果类没用定义自己的构造函数,而用户的代码有需要使用。则编译器会生成默认的拷贝构造函数和拷贝运算符,他们的作用是拷贝非静态的成员
  • 一旦声明了拷贝构造函数(运算符)而没有声明移动构造函数(运算符),那么用户的移动操作将会由拷贝操作来执行。
  • 移动操作的成员必须支持移动操作(例如:某个成员不支持移动,那么默认生成的移动操作就不会生成移动构造函数),而拷贝操作就不会。
  • 移动操作和拷贝操作,如果其中之一被定义那么他们会抑制另一个操作的默认生成。
  • 五三法则:
  1. 需要析构函数的类也需要拷贝构造函数和拷贝赋值运算符:需要拷贝操作代表这个类在拷贝时需要进行一些额外的操作。赋值操作=先析构+拷贝,所以拷贝需要的赋值也需要。反之亦然。
  2. 需要拷贝操作的类也需要赋值操作,反之亦然。
  3. 析构函数不能是删除的:无法实例出对象或者是在对内存分配后无法使用delete释放资源
  4. 如果一个类有删除的或不可访问的析构函数,那么其默认和拷贝构造函数会被定义为删除的。
  5. 如果一个类有const或引用成员,则不能使用合成的拷贝赋值操作。
  •  默认的移动操作(隐式)生成的条件:
  1. 类内没有声明拷贝赋值运算符
  2. 类内没用声明拷贝构造函数
  3. 没有声明析构函数(通常,自定义的析构函数意味着成员包含动态分配的内存。)
  •  请注意没有规则说明成员函数模板会阻止编译器生成特殊成员函数。这意味着如果Widget是这样的:
class Widget {
  …
  template<typename T>                // construct Widget
  Widget(const T& rhs);               // from anything
  template<typename T>                // assign Widget
  Widget& operator=(const T& rhs);    // from anything
  …
};

编译器还是会为Widget生成拷贝构造和拷贝赋值(假如条件满足),尽管这些模板可以被实例化来产生拷贝构造和拷贝赋值的签名(当T是Widget的时候)。你十有八九觉得这仅仅是值得了解的边缘情况,但我提到它是有原因的,在Item26中我会展示它导致的重大后果。

Things to Remember
• 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, and 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 explic‐
itly 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.

参考&引用