本节解释move()、move_if_noexcept()、forward()、swap()和exchange()。顺便还介绍了移动语义和完美转发的概念。
如果某个对象的前一个用户不再需要它,可以将其移动到其他地方(而不是复制)。将资源从一个对象移动到另一个对象通常比(深度)复制它们更有效。例如,对于一个string对象,移动通常就像复制一个char*指针和一个长度(常数时间)一样简单;没有必要复制整个char数组(线性时间)。
除非另外指定,否则被移出的源对象处于未定义但有效的状态,除非重新初始化,否则不应再使用。例如,移动一个std::string(参见第 6 章)的有效实现可以将源的char*指针设置为nullptr,以防止数组被删除两次,但这不是标准所要求的。同样,也没有说明length()被移走后将返回什么。某些操作,尤其是赋值,仍然是允许的,如下例所示:
尽管名字如此,std::move()函数在技术上并不移动任何东西:相反,它只是简单地标记一个给定的T、T&或T&&值可以被移动,实际上是通过静态地将它转换成一个右值引用T&&。由于类型强制转换,其他函数可能会被重载决策选择,和/或值参数对象可能会使用它们的移动构造函数(形式为T(T&& t))进行初始化,如果可用的话,而不是它们的复制构造函数。这种初始化发生在被调用方,而不是调用方。一个右值参数T&&强制调用者总是移动。
类似地,也可以使用移动赋值操作符(形式为operator=(T&&))将一个对象移动到另一个对象:
如果没有定义移动成员,无论是显式的还是隐式的,T&&的重载解析回退到T&或T,在后一种情况下仍然创建一个副本。生成隐式移动成员的条件包括不能有任何用户定义的复制、移动或析构成员,也不能有任何不能移动的非静态成员变量或基类。
move_if_noexcept()函数类似于move(),除了如果T的 move 构造函数已知不从其异常规范中抛出(noexcept,或已弃用的throw()),它只向T&&进行强制转换;否则,它强制转换为const T&。
标准定义的所有类都有适当的移动成员。例如,第三章中的许多容器可以在固定时间内移动(不是std::array,尽管它会移动单个元素以避免深度复制)。
Tip
对于重要的自定义类型,为了获得最佳性能,不仅定义移动成员至关重要,而且始终使用noexcept说明符来定义成员也同样重要。第三章中的容器类广泛使用移动来加速操作,比如添加一个新元素,或者重新定位元素数组(例如,用顺序容器)。类似地,如果提供有效的移动成员(和/或非成员操作,稍后讨论),第 4 章的许多算法都会受益。但是,尤其是在移动元素数组时,这些优化通常只有在已知值的移动成员不抛出时才会生效。
std::forward() helper 函数旨在模板化函数中使用,以便在保留任何 move 语义的同时有效地将其参数传递给其他函数。如果forward<T>()的参数是一个左值引用T&,那么这个引用将被原封不动地返回。否则,参数被转换为右值引用T&&。一个例子将阐明它的预期用途:
good_fwd()用的成语叫完美转发。它最佳地保留了右值引用(比如那些std::move() d 或临时对象)。这个习惯用法的第一个要素是所谓的转发或通用引用:一个T&&参数,一个T模板类型参数。如果没有它,模板参数演绎将删除所有引用:对于ugly_fwd();A&和A&&都变成了A。有了转发引用,分别推导出A&和A&&:也就是说,即使转发引用看起来像T&&,如果传递了A&,则推导出A&而不是A&&。尽管如此,仅使用转发引用是不够的,如bad_fwd()所示。当按原样使用命名变量t时,它与一个左值函数参数绑定(所有命名变量都这样),即使其类型被推断为A&&。这就是std::forward<T>()的用武之地。与std::move()类似,它会转换为T&&,但前提是给定一个右值类型的值(包括类型为A&&的命名变量)。
所有这些都很微妙,更多的是关于 C++ 语言(特别是类型演绎)而不是标准库。这里的要点是,为了将函数模板的参数正确地转发给函数,您应该考虑使用完美转发——也就是说,转发引用与std::forward()相结合。
std::swap()模板函数交换两个对象,好像实现为:
template<typename T> void swap(T& one, T& other)
{ T temp(std::move(one)); one = std::move(other); other = std::move(temp); }
还定义了一个类似的swap()函数模板来分段交换等长T[N]数组的所有元素。
虽然如果有合适的移动成员就已经很有效了,但是为了获得真正的最佳性能,您应该考虑专门化这些模板函数:例如,消除移动到临时。比如第四章的很多算法都调用这个非成员swap()函数。对于标准类型,swap()在适当的地方已经定义了专门化。
与swap()类似的一个函数是std::exchange(),它在返回旧值的同时给某物赋予一个新值。有效的实现是
template<typename T, typename U=T> T exchange(T& x, U&& new_val)
{ T old_val(std::move(x)); x = std::forward<U>(new_val); return old_val; }
Tip
尽管swap()和exchange()可能在std名称空间中被特殊化,但是大多数人建议将它们特殊化到与它们的模板参数类型相同的名称空间中。这样做的好处是所谓的参数相关查找(ADL)是可行的。换句话说,例如swap(x,y)不需要using指令或声明,也不需要指定swap()的名称空间。ADL 规则基本上规定,非成员函数应该首先在其参数的名称空间中查找。如果需要的话,通用代码应该使用下面的习惯用法回到std::swap():using std::swap; swap(x,y);。简单地编写std::swap(x,y)将不会在std名称空间之外使用用户定义的swap()函数,而单独的swap(x,y)将不会工作,除非有这样的用户定义函数。
std::pair<T1,T2>模板struct是一个可复制的、可移动的、可交换的(按字典顺序)可比较的struct,它在其公共成员变量first和second中存储了一对T1和T2值。默认构造的对对其值进行零初始化,但也可以提供初始值:
std::pair<unsigned int, Person> p(42u, Person("Douglas", "Adams"));
使用辅助功能std::make_pair()可以自动推导出两个模板类型参数:
auto p = std::make_pair(42u, Person("Douglas", "Adams"));
Tip
不是所有的类型都可以被有效地移动,并且在构造一个对的时候必须被复制。对于较大的对象(例如,包含固定大小数组的对象),这可能是一个性能问题。其他类型甚至根本不可复制。对于这种情况,std::pair有一个特殊的“分段”构造函数来执行其两个成员的就地构造。它是用一个特殊的常量调用的,后跟两个元组(见下一节),其中包含要转发给两个成员的构造函数的参数。
例如(forward_as_tuple()用于不将字符串复制到临时元组):
std::pair<unsigned, Person> p(std::piecewise_construct,
std::make_tuple(42u), std::forward_as_tuple("Douglas", "Adams"));
分段构造也可以与第 3 章中容器的emplace()函数一起使用(这些函数的定义类似,以避免不必要的复制),特别是与std::map和std::unordered_map的函数一起使用。
std:: tuple是pair的推广,允许存储任意数量的值(即零个或更多,而不仅仅是两个):std::tuple<Type...>。大部分类似于pair,包括make_tuple()辅助功能。主要区别在于单个值不存储在公共成员变量中。相反,您可以使用get()模板函数之一来访问它们:
获取tuple值的另一种方法是使用tie()函数解包。特殊的std::ignore常量可用于排除任何值:
int one, two; double three;
std::tie(one, two, three, std::ignore) = t;
Tip
std::tie()函数可用于基于多个值紧凑地实现字典式比较。例如,简介中的Person类的operator<主体可以写成
return std::tie(lhs.m_isVIP, lhs.m_lastName, lhs.m_firstName)
< std::tie(rhs.m_isVIP, rhs.m_lastName, rhs.m_firstName);
还有两个助手struct用于获取给定元组的大小和元素类型,这在编写通用代码时非常有用:
注意pair和std::array(见第 3 章)在各自的头中也定义了get()、tuple_size和tuple_element,但没有定义tie()。
tuple s 的最后一个帮助函数是std::forward_as_tuple(),它创建了一个引用其参数的元组。这些通常是左值引用,但是保留右值引用,就像前面解释的std::forward()一样。它被设计为将参数转发给tuple的构造函数(也就是说,同时避免复制),特别是在通过值接受元组的函数的上下文中。例如,函数f(tuple<std::string, int>)可以如下调用:f(std::forward_as_tuple("test", 123));。
元组也为自定义分配器提供了便利,但是这是一个高级主题,超出了本书的范围。
在std::rel_ops名称空间中提供了一组很好的关系操作符:!=、<=、>和>=。第一个按照operator==实现,其余的转发到operator<。所以,你的类只需要实现operator==和<,其他的都是在你添加一个using namespace std::rel_ops;时自动生成的
智能指针是一个 RAII 样式的对象,它(通常)修饰并模仿一个指向堆分配内存的指针,同时保证在适当的时候总是释放这些内存。作为一条规则,现代 C++ 程序不应该使用原始指针来管理(共有的)动态内存:所有由new或new[]分配的内存都应该由一个智能指针来管理,或者,对于后者,由一个容器来管理,比如vector(参见第 3 章)。因此,C++ 程序应该很少再直接调用delete或delete[]。这样做将大大有助于防止内存泄漏。
一个unique_ptr独占一个指向堆内存的指针,因此不能被复制,只能被移动或交换。除此之外,它的行为很像一个普通的指针。下面说明了它在堆栈上的基本用法:
->和*操作符确保了unique_ptr通常可以像原始指针一样使用。比较运算符==、!=、<、>、<=和>=用于比较两个unique_ptrs或一个unique_ptr与nullptr(按任意顺序),但不用于比较一个unique_ptr<T>与一个T值。要实现后者,必须调用get()来访问原始指针。一个unique_ptr也方便地转换成一个布尔值来检查nullptr。
使用助手功能make_unique()有助于构建。例如:
{ auto jeff = std::make_unique<Person>("Jeffrey");
...
Tip
使用make_unique()不仅可以缩短代码,还可以防止某些类型的内存泄漏。考虑一下f(unique_ptr<X>(new X), g())。如果g()在X被构造之后,但是在它被分配给它的unique_ptr之前抛出,那么X指针就会泄漏。相反,编写f(make_unique<X>(), g())可以保证这种泄漏不会发生。
使它们成为真正重要的实用工具的其他用途包括:
- 它们是转移独占所有权的最安全和推荐的方法,要么从创建堆对象的函数返回一个
unique_ptr,要么将一个作为参数传递给接受进一步所有权的函数。这有三个主要优点:在这两种情况下,通常都必须使用std::move(),使得所有权转移显式。预期的所有权转移也从函数的签名中变得显而易见。它可以防止内存泄漏(这种错误有时很微妙:参见下一篇技巧)。 - 它们可以安全地存放在第 3 章的容器中。
- 当用作另一个类的成员变量时,它们消除了在析构函数中显式
delete的需要。此外,它们防止编译器为应该独占动态内存的对象生成容易出错的副本成员。
A unique_ptr也可以管理用new[]分配的内存:
对于这个模板专门化,解引用操作符*和->被替换为索引数组访问操作符[]。一个更强大更方便的管理动态数组的类std::vector,在第 3 章中解释。
一个unique_ptr<T>有两个相似的成员经常被混淆:release()和reset(T*=nullptr)。前者用nullptr替换旧的存储指针(如果有的话),而后者用给定的T*替换。关键区别在于release()不删除旧指针。相反,release()旨在释放存储指针的所有权:它只是将存储指针设置为nullptr并返回它的旧值。这有助于将所有权传递给例如遗留 API。另一方面,reset()旨在用新值替换存储的指针,不一定是nullptr。在覆盖旧指针之前,它会被删除。因此,它也不返回任何值:
Tip
使用release()转移所有权时,注意内存泄漏。假设前面的例子以TakeOwnership(niles.release(), f())结束。如果对f()的调用在unique_ptr拥有release d 的所有权后抛出,奈尔斯泄密。因此,始终确保包含release()子表达式的表达式也不包含任何抛出子表达式。在本例中,解决方案是在前面的行中计算f(),将其结果存储在一个已命名的变量中。前面推荐的用std::move(niles)传输,顺便说一下也绝对不会漏。但是,对于遗留 API,这并不总是一个选项。
Caution
一个相当常见的错误是在应该使用reset()的地方使用release(),后者使用默认的nullptr参数,忽略由release()返回的值。先前由unique_ptr拥有的物品随后泄露,这通常不会被注意到。
的一个高级特性是他们可以使用自定义的删除器。删除器是销毁所拥有的指针时执行的函子。这对于非默认内存分配很有用,可以进行额外的清理,或者,例如,管理由 C 函数fopen()(在<cstdio>中定义)返回的文件指针:
这个例子使用了一个类型为std::function(在<functional>头中定义,将在本章后面讨论)的删除器,它用一个函数指针初始化,但是也可以使用任何仿函数类型。
在编写时,<memory>头仍然为独占所有权定义了第二种智能指针类型,即std::auto_ptr。然而,在 C++11 中,这已经被弃用,取而代之的是unique_ptr,并且在 C++17 中将被移除。因此,我们不详细讨论它。本质上,auto_ptr是一个有缺陷的unique_ptr,在复制时被隐式移动:这使得它们不仅容易出错,而且与第 3 和 4 章中的标准容器和算法一起使用也很危险(事实上是非法的)。
当多个实体共享同一个堆分配的对象时,为它分配一个所有者并不总是显而易见或可能的。对于这种情况,shared_ptr s 存在,在<memory>中定义。这些智能指针为一个共享内存资源维护一个线程安全的引用计数,一旦它的引用计数达到零,它就会被删除:也就是说,一旦最后一个共同拥有它的shared_ptr被析构。use_count()成员返回引用计数,unique()检查计数是否等于 1。
像unique_ptr一样,它有->、*、转换为布尔和比较操作符来模拟原始指针。同样提供等效的get()和reset()成员,但没有release()。然而,shared_ptr不能管理动态数组。真正让它与众不同的是shared_ptr s 可以而且打算被复制:
一个shared_ptr可以通过将一个unique_ptr移动到其中来构造,但不能反过来。为了构造一个新的shared_ptr,再次推荐使用make_shared():原因与make_unique()(更短的代码和内存泄漏预防)相同,但在这种情况下也是因为它更有效。
再次支持自定义删除器。然而,与unique_ptr不同的是,删除者的类型不是shared_ptr模板的类型参数。因此,类似于前面示例中的声明变成了:
std::shared_ptr<FILE> smartFilePtr(fopen("test.txt", "r"), fclose);
要获得相关类型的shared_ptr,请使用std::static_pointer_cast()、dynamic_pointer_cast()或const_pointer_cast()。如果结果不是null,引用计数安全地增加 1。一个例子将阐明:
shared_ptr s 的一个鲜为人知的特性叫做别名,用于共享已经共享的对象的部分。最好用一个例子来介绍:
一个shared_ptr既有自己的指针又有存储的指针。前者决定引用计数,后者由get()、*和->返回。通常两者是相同的,但如果用别名构造函数构造就不同了。几乎所有的操作都使用存储的指针,包括比较运算符<、>=等等。为了基于拥有的指针而不是存储的指针进行比较,使用owner_before()成员或std::owner_less<>仿函数类(仿函数将很快解释)。例如,当将shared_ptr存储在std::set中时,这很有用(参见第 3 章)。
有时,尤其是在构建共享对象的缓存时,您希望在需要时保留对共享对象的引用,但又不希望引用必然会阻止对象的删除。这个概念通常被称为弱引用,由<memory>以std::weak_ptr的形式提供。
一个非空的weak_ptr由一个shared_ptr构成,或者是后来给它赋值一个shared_ptr的结果。这些指针可以自由地复制、移动或交换。虽然一个weak_ptr并不共同拥有这个资源,但是它可以访问它的use_count()。为了检查共享资源是否仍然存在,也可以使用expired()(相当于use_count()==0)。然而,weak_ptr不能直接访问共享的原始指针,因为没有什么可以阻止最后一个共有者同时删除它。要访问资源,首先必须使用lock()成员将weak_ptr提升为共有shared_ptr:
函数对象或仿函数是带有operator()(T1,...,Tn) ( n可能为零)的对象,允许它像函数或运算符一样被调用:
函子不仅可以传递给许多标准算法(第章第 4 节)和并发构造(第章第 7 节),而且对于创建你自己的通用算法也非常有用,例如,存储或提供回调函数。
本节概述了在<functional>中定义的函子,以及创建和使用函子的工具。 1 我们还将简要介绍 lambda 表达式,这是一种强大的 C++11 语言构造,用于创建函子。
不过,在我们深入研究函子之前,先简单介绍一下在<functional>头文件中定义的引用包装器实用程序。
函数std::ref()和cref()返回std::reference_wrapper<T>实例,这些实例简单地包装了对其输入参数的(const ) T&引用。然后,可以使用get()显式提取该引用,或者通过强制转换为T&隐式提取该引用。
因为这些包装器可以被安全地复制,所以它们可以被用来传递对模板函数的引用,这些模板函数通过值获取它们的参数,错误地转发它们的参数(转发在本章前面已经讨论过了),或者出于其他原因复制它们的参数。不接受引用作为参数,但使用ref() / cref()的标准模板函数包括std::thread()和async()(参见第 7 章),以及稍后讨论的std::bind()函数。
这些包装器也可以分配给,这样就可以将引用存储到第三章的容器中。例如,在下面的例子中,您不能声明一个vector<int&>,因为int&的值不能赋给:
<functional>头提供了一整个系列的仿函数struct,类似于本节介绍中前面使用的my_plus示例:
plus、minus、multiplies、divides、modulus和negateequal_to、not_equal_to、greater、less、greater_equal和less_equallogical_and、logical_or和logical_notbit_and、bit_or、bit_xor和bit_not
这些函子通常会产生简短易读的代码,甚至比 lambda 表达式更容易理解。以下示例使用第 4 章中介绍的sort()算法对数组进行降序排序(默认为升序):
int array[] = { 7, 9, 7, 2, 0, 4 };
std::sort(begin(array), end(array), std::greater<int>());
从 C++14 开始,所有这些仿函数类都有一个特殊的专门化,即T等于void,void也成为了默认的模板类型参数。这些被称为透明运算符函子,因为它们的函数调用运算符可以方便地推导出参数类型。例如,在前面的sort()示例中,您可以简单地使用std::greater<>。同一函子甚至可以用于不同的类型:
正如第 3 章所解释的,透明的std::less<>和greater<>函子也是有序关联容器的首选比较函子。
将一元/二元仿函数predicate传递给std::not1()/not2()会创建一个新的仿函数(类型为unary_negate / binary_negate),该仿函数对predicate的结果求反(即计算结果为!predicate())。为此,predicate的类型必须定义一个公共成员类型argument_type。<functional>中的所有函子类型都有这个。
模板类是为包装任何类型的可调用实体而设计的:也就是说,任何类型的函数对象或指针。这包括,例如,bind或 lambda 表达式的结果(稍后将更详细地解释这两个表达式):
如果调用默认构造的function对象,就会抛出std::bad_function_call异常。为了验证一个function是否可以被调用,它方便地转换为一个布尔值。或者,您可以使用==或!=将function与nullptr进行比较,就像使用函数指针一样。
其他成员包括target<Type>()以获得指向包装实体的指针(必须指定正确的Type;否则成员返回nullptr,target_type()返回该包装实体的type_info(type_info将在本章后面的“类型实用程序”中解释)。
Tip
前面提到的std::ref()、cref()及其返回类型reference_wrapper的一个鲜为人知的特性是,它们也可以用来包装可调用函数。然而,与存储可调用对象副本的std::function不同的是,reference_wrapper存储的是对它的引用。这在将您不希望被复制的仿函数(例如,因为它太大(性能)、有状态或根本不可复制)传递给接受它或可能通过值传递它的算法时非常有用。例如:
function_that_copies_its_callable_argument(std::ref(my_functor));
请注意,对于第 4 章中的标准算法,通常不指定它们多长时间复制一次参数。所以为了保证没有拷贝,你必须使用(c)ref()。
std::bind()函数可以用来包装任何可调用函数的副本,同时改变其签名:参数可以被重新排序,被赋予固定值,等等。为了指定将哪些参数转发给包装的可调用函数,一系列值或所谓的占位符(_1、_2等)被传递给bind()。传递给绑定函子的第一个参数被转发给占位符_1的所有实例,第二个被转发给_2的实例,依此类推。占位符的最大数量取决于具体的实现;并且返回的函子的类型是未指定的。一些例子将阐明:
前面介绍的std::function和bind()都可以用来创建仿函数,这些仿函数计算给定对象的成员变量,或者调用给定对象的成员函数。第三种选择是使用std::mem_fn(),它专门用于此目的:
struct my_struct { int val; bool fun(int i) { return val == i; } };
int main() {
my_struct s{234};
std::function<int(my_struct&)> f_get_val = &my_struct::val;
std::function<bool(my_struct&,int)> f_call_fun = &my_struct::fun;
std::cout << f_get_val(s) << ' ' << f_call_fun(s, 123) << std::endl;
using std::placeholders::_1;
auto b_get_val = std::bind(&my_struct::val, _1);
auto b_call_fun_on_s = std::bind(&my_struct::fun, std::ref(s), _1);
std::cout << b_get_val(s) << ' ' << b_call_fun_on_s(234) << std::endl;
auto m_get_val = std::mem_fn(&my_struct::val);
auto m_call_fun = std::mem_fn(&my_struct::fun);
std::cout << m_get_val(s) << ' ' << m_call_fun(s, 456) << std::endl;
}
由bind()和mem_fn()创建的成员函子,而不是std::function创建的成员函子,也可以用一个指针或一个标准智能指针(见上一节)作为第一个参数来调用(也就是说,不用解引用)。关于bind()选项的有趣之处还在于它可以绑定目标对象本身(参见b_call_fun_on_s)。如果这不是必需的,std::mem_fn()通常会产生最短的代码,因为它推导出了整个类型。更现实的例子是这样的(分别在第 3 、 4 和 6 章节中解释了vector、count_if()和string):
LAMBDA EXPRESSIONS
虽然不是标准库的一部分,但是 lambda 表达式是创建函子的强大工具,非常值得简单介绍一下。特别是,当与第 4 章中的算法、第 7 章中的并发结构等等结合起来时,它们通常构成了极具表现力的优雅代码的基础。在本书中可以找到几个 lambda 表达式的例子,尤其是在第 4 章中。
lambda 表达式通常被认为创建了一个匿名函数,但实际上它创建了一个未指定类型的函子,也称为闭包。lambda 表达式不必像<functional>构造那样从现有的函数开始:其闭包的函数调用操作符的主体可以包含任意代码。
lambda 表达式的基本语法如下:
[CaptureBlock](Parameters)
mutable -> ReturnType {Body}
Capture block:指定从封闭范围中捕获哪些变量。实际上,对于每个捕获的变量,创建的仿函数都有一个同名成员,如果是通过值捕获的,则包含该捕获变量的副本,如果是通过引用捕获的,则包含对该变量的引用。因此,这些变量在身体中变得可用。捕获块的基本语法:
[]不捕获变量(不能省略)。[x, &y]通过值捕获x,通过引用捕获y。[=, &x]通过值捕获封闭范围内的所有变量,除了x通过引用捕获。[&, x,y]通过引用捕获所有变量,除了x和y通过值捕获。[this]捕获this指针,授权主体访问周围对象的所有成员。
Parameters:调用仿函数时要传递的参数。省略等同于指定一个空列表()。参数类型可以是auto。
mutable:默认情况下,lambda 仿函数的函数调用操作符总是被标记为const,这意味着通过值捕获的变量(即复制到成员变量中的变量)不能被修改(赋值给被调用的非const成员,等等)。指定mutable使函数调用操作符非const。
Return type:返回值的类型。只要主体的所有返回语句返回完全相同的类型,就可以省略。
Body:调用 lambda 仿函数时要执行的代码(非可选)。
也可以指定noexcept和/或属性(在可选的mutable之后),但是很少使用。
C++ 编译器使用initializer_list<T>类型来表示初始化列表声明的结果:
这种花括号语法是创建非空初始化列表的唯一方法。一旦创建,initializer_list就不可改变。它们的几个操作size()、begin()和end()类似于容器的操作(第 3 章)。当从一个初始化值列表中构造一个initializer_list时,该列表存储这些值的一个副本。然而,复制一个initializer_list并不会复制元素:新的副本只是引用相同的值数组。
最常见的用例可能是初始化列表构造函数,当使用花括号时,它们优先于其他构造函数,这一点很特别:
例如,第 3 章的所有容器类都有初始化列表构造函数,用一个值列表来初始化它们。
<chrono>库引入了一些工具,主要用于跟踪不同精度的时间和持续时间,这由所使用的时钟类型决定。要处理日期,您必须使用 C 风格的日期和时间类型以及在<ctime>中定义的函数。来自<chrono>的system_clock允许与<ctime>的互操作性。
A std::chrono::duration<Rep, Period=std::ratio<1>>将时间跨度表示为滴答计数,表示为通过count() ( Rep是或模拟算术类型)可获得的Rep值。两个连续分笔成交点之间的时间或周期由Period静态确定,一种表示秒数(或分数)的std::ratio类型(std::ratio在第 1 章中解释)。默认的Period是一秒钟:
只要不需要截断,duration构造函数可以在不同Period的duration和/或 count Rep表示之间进行转换。duration_cast()函数也可以用于截断转换:
为了方便起见,在std::chrono名称空间中预定义了几个类似于前一个例子的typedef:hours、minutes、seconds、milliseconds、microseconds和nanoseconds。每个都使用未指定的有符号整数Rep类型,至少大到足以表示大约 1000 年的持续时间(Rep分别至少有 23、29、35、45、55 和 64 位)。为了进一步方便起见,名称空间std::literals::chrono_literals包含文字操作符,可以很容易地分别创建duration类型的实例:h、min、s、ms、us和ns。它们也可以通过using namespace std::chrono声明获得。当应用于浮点文字时,结果具有未指定的浮点类型,如Rep:
支持您直觉上期望使用duration的所有算术和比较运算符:+、-、*、/、%、+=、-=、*=、/=、%=、++、--、==、!=、<、>、<=和>=。例如,下面的表达式计算出带有count() == 22的duration:
duration_cast<minutes>((12min + .5h) / 2 + (100ns >= 1ms? -3h : ++59s))
一个std::chrono::time_point<Clock, Duration=Clock::duration>代表一个时间点,表示为从一个Clock纪元开始的一个Duration。这个Duration可以从它的time_since_epoch()成员那里获得。历元被定义为被选作特定时钟的原点的时刻,即测量时间的参考点。下一节将介绍可用的标准Clock。
一个time_point通常最初是从它的Clock的类的成员中获得的。不过,它也可以由给定的Duration构建而成。如果默认构造,它代表Clock的纪元。几个算术(+、-、+=、-=)和比较(==、!=、<、>、<=、>=)再次可用。减去两个time_point得到一个Duration,并且Duration可以被加到一个time_point上或者从一个time_point上减去。不允许将time_point加在一起,也不允许从Duration中减去 1:
具有不同Duration类型的time_point之间的转换类似于duration的转换:只要不需要截断,就允许隐式转换;否则,可以使用time_point_cast():
auto one_hour = time_point_cast<hours>(sixty_minutes);
The std::chrono名称空间提供了三种时钟类型:steady_clock、system_clock和high_resolution_clock。所有时钟都定义了以下静态成员:
now():返回当前时间点的函数。rep、period、duration、time_point:具体实现类型。time_point是now()返回的类型:一个std::chrono::time_point的实例化,其Duration类型参数等于duration,进而等于std::chrono::duration<rep, period>。is_steady:一个布尔常量,如果时钟滴答之间的时间是常数,并且连续两次调用now()总是返回time_pointst1和t2,其中t1 <= t2。
唯一保证稳定的时钟是steady_clock。也就是说,这个时钟不能调整。另一方面,system_clock对应于系统范围的实时时钟,通常可以由用户随意设置。最后,high_resolution_clock是库实现支持的周期最短的时钟(可能是steady_clock或system_clock的别名)。
为了测量一个操作花费的时间,应该使用一个steady_clock,除非你的实现的high_resolution_clock是稳定的:
system_clock应保留用于日历时间。因为<chrono>在这方面的功能有些有限,所以这个时钟提供了静态函数来将其time_point转换为time_t对象,反之亦然(分别为to_time_t()和from_time_t()),然后可以与下一小节中讨论的 C 风格的日期和时间实用程序一起使用:
<ctime>头定义了两种可互换的类型来表示日期和时间:time_t,算术类型的别名(一般是 64 位有符号整数),以平台特定的方式表示时间;以及tm,一个便携的struct,带有这些字段:tm_sec(范围[ 0、60,其中60用于闰秒)、tm_min、tm_hour、tm_mday(一个月中的某一天,范围[ 1、31)、tm_mon(范围[ 0、11)、tm_year(自 1900 年以来的年份)、tm_wday(范围[ 0、6),带有【T20)
以下功能可通过<ctime>使用。本地时区由当前活动的 C 语言环境决定(语言环境在第 6 章中解释):
请查阅您的实现文档,以获得更安全的localtime()和gmtime()的替代方案(例如 Windows 的localtime_s()或 Linux 的localtime_r())。对于将日期和时间转换成字符串,首选的 C 风格函数是strftime()(在本节的最后,我们指出了 C++ 风格的替代方法):
size_t strftime(char* result, size_t n, const char* format, const tm*);
在<cwchar>中定义了转换为宽字符串(wchar_t序列)、wcsftime()的等价形式。这些函数将一个以null结尾的字符序列写入result,该字符序列必须指向一个预先分配的大小为n的缓冲区。如果这个缓冲区太小,则返回零。否则,返回值等于写入的字符数,不包括终止字符null。
指定所需文本表示的语法定义如下:format字符串中的任何字符都被复制到result,除了某些特殊说明符被替换,如下表所示:
许多说明符的结果,包括那些扩展为名称或首选格式的说明符,取决于活动的语言环境(参见第 6 章)。例如,当使用法语语言环境执行时,上一个示例的输出可能是“Today is mer. 21/11"和"10/21/15 16:29:00--10/21/15 16:29:00”。要使用依赖于语言环境的替代表示法(如果当前语言环境定义了替代表示法),C、c、X、x、Y和y前面可以有E ( %EC、%Ec等等);为了使用替代数字符号,d、e、H、I、M、m、S、u、U、V、W、w和y可以用字母O进行修改。
如第 5 章所述,C++ 库也提供了从/向流中读取/写入tm的工具,即get_time()和put_time()。因此,<ctime>中唯一一个 C 风格的函数是localtime()(将system_clock的time_t转换成tm)),你通常需要用 C++ 风格输出日历日期和时间。
下一版本的 C++ 标准库有望包含一个更强大的 C++ 风格的文件系统库。目前,<cstdio>头文件中有限的一组 C 风格函数是标准中唯一可用的可移植文件实用程序:
C++ typeid()操作符用于获取一个值的运行时类型信息。它返回对在<typeinfo>中定义的std::type_info类的全局实例的引用。这些实例不能被复制,但是使用指向它们的引用或指针是安全的。使用它们的==, !=和before()成员可以进行比较,并且可以为它们计算一个hash_code()。特别有趣的是name(),它返回值的类型的特定于实现的文本表示:
打印的name()可能类似于“std::basic_string<char, std::char_traits<char>, std::allocator<char>>””(见第 6 章),但对于其他实现,它也可能是“Ss””。
当用在指向派生类D实例的B*指针上时,如果B是多态的,即至少有一个virtual成员,那么typeid()只给出动态类型D*,而不是静态类型B*。
因为type_info s 不能被复制,所以它们不能直接用作第 3 章中关联数组的键。正是为了这个目的,<typeindex>头定义了std::type_index装饰器类:它模仿包装的type_info&的接口,但是它是可复制的;有<、<=、>、>=操作符;并为其定义了一个专门化std::hash。
类型特征是一种构造,用于获取给定类型的编译时信息,或将一种或多种给定类型转换为相关类型。类型特征通常用于在编写泛型代码时检查和操作模板类型参数。
标题定义了许多特征。由于篇幅的限制,并且因为模板元编程是一个高级主题,本书无法对所有这些进行详细介绍。不过,我们提供了不同类型特征的简要参考,这对于基本的使用应该是足够的。
C++ 中的每个类型都属于 14 个主要类型类别中的一个。除此之外,该标准还定义了几个复合类型类别,以便于引用属于两个或更多相关主要类别的所有类型。对于其中的每一个,存在一个类型特征struct来检查给定的类型是否属于那个类别。它们的名称形式为is_类别,类别等于图 2-1 中所示的名称之一。一个名为value的特征的静态布尔包含了它的类型参数是否属于相应的类别。特征是返回和转换到这个value的函子。下面是一些例子(代码指int main()):
图 2-1。
Overview of the type classification traits. The second column lists the 14 primary categories; the other names are those of the composite categories.
第二个类型特征系列用于静态查询类型的属性。它们的使用方式与前一小节中的使用方式完全相同,除了一个名称has_virtual_destructor之外,所有的名称都采用了is_属性的形式。
以下属性值用于检查指示的类型属性:
- 类型量词的存在:
const和volatile - 类的多态性属性:
polymorphic(有虚拟成员);abstract(纯虚拟成员)和final - 算术类型的有符号性:
signed(包括浮点数)和unsigned(包括布尔值)
此外,还有一大类特征,其属性是具有指定参数类型的构造或赋值语句的有效性,或者销毁语句的有效性(总是省略is_):
- 基本的有
constructible<T,Args...>、assignable<T, Arg>、destructible<T>。所有标量类型都是可析构的,前两个属性对于非类类型也适用(因为像int i(0);这样的构造是有效的)。 - 辅助特征用于检查默认结构(
default_constructible)和复制/移动结构和赋值(copy_constructible<T> == constructible<T, const T&>等)的有效性。 - 所有先前的属性名称可以进一步以
trivially或nothrow为前缀。比如:trivially_destructible、nothrow_constructible或者nothrow_move_assignable。
如果静态地知道构造、赋值或销毁永远不会抛出,那么nothrow属性就成立。如果类型是scalar或者这个操作是默认操作的非多态类(也就是说,不是由用户指定的),则trivial属性成立,trivial属性也适用于它的所有基类和非静态成员变量。对于普通的可构造属性,类也不允许有任何带有类内初始值设定项的非静态数据成员。
属性值的最终列表在以下条件下基本成立。满足这些条件的类型数组也具有相同的属性:
trivially_copyable,如果trivially_destructible和trivially_(copy|move)_(constructible|assignable)都成立。像std::memcpy()这样的按位复制函数被定义为对trivially_copyable类型是安全的。trivial、iftrivially_default_constructible和trivially_copyable,并且不存在非默认构造函数。standard_layout、ifscalar或者一个类,对于该类,指向该类的指针可以安全地转换为指向其第一个非静态成员类型的指针(也就是说,没有多态、有限的多重继承等等)。这是为了与 C 兼容,因为这种强制转换(用 Cstructs)在 C 代码中是常见的做法。pod(普通旧数据),如果trivial和standard_layout。literal_type,if 值可能用在constexpr表达式中(即可以静态求值,没有副作用)。empty,对于没有非静态成员变量的非多态类。
类型特征的value不总是布尔值。对于以下特征,它包含指定的size_t类型属性:
std::alignment_of<T>:操作符alignof(T)的值std::rank<T>:数组维数,如rank<int>() == 0、rank<int[]>() == 1、rank<int[][5][6]>() == 3等std::extent<T,N=0>:第N个数组维度的元素个数,如果未知或无效则为 0;比如extent<int[]>() == 0和extent<int[][5][6], 1>() == 5。
这三个类型特征比较类型:is_same<T1, T2>、i s_base_of<Base, Derived>和is_convertible<From, To>(使用隐式转换)。
大多数类型转换特征也非常相似,除了它们没有value,而是一个名为type的嵌套typedef:
std::add_x同x``const``volatile``cv(const``volatile)pointer``lvalue_reference``rvalue_reference其中之一。std::remove_x``x``const``volatile``cv``pointer``reference(左值或右值)extent``all_extents其中之一。除了最后一种情况,只有顶层/第一个类型修饰符被删除。比如:remove_extent<int[][5]>::type == int[5]。std::decay<T>:将T转换为相关的type,可以通过值存储,模拟通过值传递参数。比如数组类型int[5]变成指针类型int*,函数变成函数指针,const和volatile被剥离,等等。一个可能的实现作为一个例子简短地示出。std::make_y同y``signed或unsigned。如果应用于整数类型T,type分别是带符号或不带符号的整数类型sizeof(type) == sizeof(T)。- 仅针对
functional类型定义的std::result_of,给出函数的返回type。 - 仅针对
enum类型定义的std::underlying_type,给出了这个enum下面的【整数】type。 std::common_type<T...>有一个type所有类型T都可以隐式转换为。
这个头还包含两个实用特征来帮助类型元编程。通过几个例子来说明它们的基本用途。
- 如果
constexpr B评估为true则std::conditional<B,T1,T2>有type T1,否则type T2。 std::enable_if<B,T=void>有type T,但前提是constexpr B求值为true。否则,type就没有定义。
对于该子部分的所有特征,名为std::特征_t<T>的便利类型被定义为std::特征<T>::type。例如,接下来的例子显示了与完整表达式相比,enable_if_t<>是多么方便。
第一个例子展示了如何使用 C++ SFINAE 习惯用法在重载决策中有条件地添加或删除函数。SFINAE 是替换失败不是错误的首字母缩写,它利用了这样一个事实,即未能专门化模板并不构成编译错误。在这种情况下,缺少type typedef会导致替换失败:
第二个例子显示了根据std::conditional元函数的std::decay转换特征的可能实现。后者用于在类型层面上基本形成一个if - else if - else结构;
using namespace std;
template<typename T> struct my_decay {
private:
typedef remove_reference_t<T> U;
public:
typedef conditional_t<is_array<U>::value, remove_extent_t<U>*,
conditional_t<is_function<U>::value, add_pointer_t<U>,
remove_cv_t<U>>> type;
};
Footnotes 1
<functional>包含许多我们没有讨论的不推荐使用的设施:ptr_fun()、mem_fun()、mem_fun_ref()、bind1st()和bind2nd(),加上它们的返回类型,以及基类unary_function和binary_function。所有这些都已经从标准的 C++17 版本中删除,不应该使用。
































