十二、 C++特性之 杂合
static_assert和 type traits
static_assert提供一个编译时的断言检查。如果断言为真,什么也不会发生。如果断言为假,编译器会打印一个特殊的错误信息。
- template <typename T, size_t Size>
- class Vector
- {
- static_assert(Size < 3, "Size is too small");
- T _points[Size];
- };
- int main()
- {
- Vector<int, 16> a1;
- Vector<double, 2> a2;
- return 0;
- }
- error C2338: Size is too small
- see reference to class template instantiation 'Vector<T,Size>' being compiled
- with
- [
- T=double,
- Size=2
- ]
static_assert和type traits一起使用能发挥更大的威力。type traits是一些class,在编译时提供关于类型的信息。在头文件<type_traits>中可以找到它们。这个头文件中有好几种 class: helper class,用来产生编译时常量。type traits class,用来在编译时获取类型信息,还有就是type transformation class,他们可以将已存在的类型变换为新的类型。
下面这段代码原本期望只做用于整数类型。
- template <typename T1, typename T2>
- auto add(T1 t1, T2 t2) -> decltype(t1 + t2)
- {
- return t1 + t2;
- }
但是如果有人写出如下代码,编译器并不会报错
- std::cout << add(1, 3.14) << std::endl;
- std::cout << add("one", 2) << std::endl;
程序会打印出4.14和”e”。但是如果我们加上编译时断言,那么以上两行将产生编译错误。
- template <typename T1, typename T2>
- auto add(T1 t1, T2 t2) -> decltype(t1 + t2)
- {
- static_assert(std::is_integral<T1>::value, "Type T1 must be integral");
- static_assert(std::is_integral<T2>::value, "Type T2 must be integral");
- return t1 + t2;
- }
- error C2338: Type T2 must be integral
- see reference to function template instantiation 'T2 add<int,double>(T1,T2)' being compiled
- with
- [
- T2=double,
- T1=int
- ]
- error C2338: Type T1 must be integral
- see reference to function template instantiation 'T1 add<const char*,int>(T1,T2)' being compiled
- with
- [
- T1=const char *,
- T2=int
- ]
Move semantics (Move语义)
这是C++11中所涵盖的另一个重要话题。就这个话题可以写出一系列文章,仅用一个段落来说明显然是不够的。因此在这里我不会过多的深入细节,如果你还不是很熟悉这个话题,我鼓励你去阅读更多地资料。
C++11加入了右值引用(rvalue reference)的概念(用&&标识),用来区分对左值和右值的引用。左值就是一个有名字的对象,而右值则是一个无名对象(临时对 象)。move语义允许修改右值(以前右值被看作是不可修改的,等同于const T&类型)。
C++的class或者struct以前都有一些隐含的成员函数:默认构造函数(仅当没有显示定义任何其他构造函数时才存在),拷贝构造函数,析构 函数还有拷贝赋值操作符。拷贝构造函数和拷贝赋值操作符提供bit-wise的拷贝(浅拷贝),也就是逐个bit拷贝对象。也就是说,如果你有一个类包含 指向其他对象的指针,拷贝时只会拷贝指针的值而不会管指向的对象。在某些情况下这种做法是没问题的,但在很多情况下,实际上你需要的是深拷贝,也就是说你 希望拷贝指针所指向的对象。而不是拷贝指针的值。这种情况下,你需要显示地提供拷贝构造函数与拷贝赋值操作符来进行深拷贝。
如果你用来初始化或拷贝的源对象是个右值(临时对象)会怎么样呢?你仍然需要拷贝它的值,但随后很快右值就会被释放。这意味着产生了额外的操作开销,包括原本并不需要的空间分配以及内存拷贝。
现在说说move constructor和move assignment operator。这两个函数接收T&&类型的参数,也就是一个右值。在这种情况下,它们可以修改右值对象,例如“偷走”它们内部指针所 指向的对象。举个例子,一个容器的实现(例如vector或者queue)可能包含一个指向元素数组的指针。当用一个临时对象初始化一个对象时,我们不需 要分配另一个数组,从临时对象中把值复制过来,然后在临时对象析构时释放它的内存。我们只需要将指向数组内存的指针值复制过来,由此节约了一次内存分配, 一次元数组的复制以及后来的内存释放。
以下代码实现了一个简易的buffer。这个buffer有一个成员记录buffer名称(为了便于以下的说明),一个指针(封装在unique_ptr中)指向元素为T类型的数组,还有一个记录数组长度的变量。
- template <typename T>
- class Buffer
- {
- std::string _name;
- size_t _size;
- std::unique_ptr<T[]> _buffer;
- public:
- // default constructor
- Buffer():
- _size(16),
- _buffer(new T[16])
- {}
- // constructor
- Buffer(const std::string& name, size_t size):
- _name(name),
- _size(size),
- _buffer(new T[size])
- {}
- // copy constructor
- Buffer(const Buffer& copy):
- _name(copy._name),
- _size(copy._size),
- _buffer(new T[copy._size])
- {
- T* source = copy._buffer.get();
- T* dest = _buffer.get();
- std::copy(source, source + copy._size, dest);
- }
- // copy assignment operator
- Buffer& operator=(const Buffer& copy)
- {
- if(this != ©)
- {
- _name = copy._name;
- if(_size != copy._size)
- {
- _buffer = nullptr;
- _size = copy._size;
- _buffer = _size > 0 > new T[_size] : nullptr;
- }
- T* source = copy._buffer.get();
- T* dest = _buffer.get();
- std::copy(source, source + copy._size, dest);
- }
- return *this;
- }
- // move constructor
- Buffer(Buffer&& temp):
- _name(std::move(temp._name)),
- _size(temp._size),
- _buffer(std::move(temp._buffer))
- {
- temp._buffer = nullptr;
- temp._size = 0;
- }
- // move assignment operator
- Buffer& operator=(Buffer&& temp)
- {
- assert(this != &temp); // assert if this is not a temporary
- _buffer = nullptr;
- _size = temp._size;
- _buffer = std::move(temp._buffer);
- _name = std::move(temp._name);
- temp._buffer = nullptr;
- temp._size = 0;
- return *this;
- }
- };
- template <typename T>
- Buffer<T> getBuffer(const std::string& name)
- {
- Buffer<T> b(name, 128);
- return b;
- }
- int main()
- {
- Buffer<int> b1;
- Buffer<int> b2("buf2", 64);
- Buffer<int> b3 = b2;
- Buffer<int> b4 = getBuffer<int>("buf4");
- b1 = getBuffer<int>("buf5");
- return 0;
- }
默认的copy constructor以及copy assignment operator大家应该很熟悉了。C++11中新增的是move constructor以及move assignment operator,这两个函数根据上文所描述的move语义实现。如果你运行这段代码,你就会发现b4构造时,move constructor会被调用。同样,对b1赋值时,move assignment operator会被调用。原因就在于getBuffer()的返回值是一个临时对象——也就是右值。
你也许注意到了,move constuctor中当我们初始化变量name和指向buffer的指针时,我们使用了std::move。name实际上是一个 string,std::string实现了move语义。std::unique_ptr也一样。但是如果我们写_name(temp._name), 那么copy constructor将会被调用。不过对于_buffer来说不能这么写,因为std::unique_ptr没有copy constructor。但为什么std::string的move constructor此时没有被调到呢?这是因为虽然我们使用一个右值调用了Buffer的move constructor,但在这个构造函数内,它实际上是个左值。为什么?因为它是有名字的——“temp”。一个有名字的对象就是左值。为了再把它变为 右值(以便调用move constructor)必须使用std::move。这个函数仅仅是把一个左值引用变为一个右值引用。
更新:虽然这个例子是为了说明如何实现move constructor以及move assignment operator,但具体的实现方式并不是唯一的。在本文的回复中Member 7805758同学提供了另一种可能的实现。为了方便查看,我把它也列在下面:
- template <typename T>
- class Buffer
- {
- std::string _name;
- size_t _size;
- std::unique_ptr<T[]> _buffer;
- public:
- // constructor
- Buffer(const std::string& name = "", size_t size = 16):
- _name(name),
- _size(size),
- _buffer(size? new T[size] : nullptr)
- {}
- // copy constructor
- Buffer(const Buffer& copy):
- _name(copy._name),
- _size(copy._size),
- _buffer(copy._size? new T[copy._size] : nullptr)
- {
- T* source = copy._buffer.get();
- T* dest = _buffer.get();
- std::copy(source, source + copy._size, dest);
- }
- // copy assignment operator
- Buffer& operator=(Buffer copy)
- {
- swap(*this, copy);
- return *this;
- }
- // move constructor
- Buffer(Buffer&& temp):Buffer()
- {
- swap(*this, temp);
- }
- friend void swap(Buffer& first, Buffer& second) noexcept
- {
- using std::swap;
- swap(first._name , second._name);
- swap(first._size , second._size);
- swap(first._buffer, second._buffer);
- }
- };
结论
关于C++11还有很多要说的。本文只是各种入门介绍中的一个。本文展示了一系列C++开发者应当使用的核心语言特性与标准库函数。然而我建议你能更加深入地学习,至少也要再看看本文所介绍的特性中的部分。
Deleted和Defaulted函数
一个表单中的函数:
- struct A
- {
- A()=default; //C++11
- virtual ~A()=default; //C++11
- };
被称为一个defaulted函数,“=default;”告诉编译器为函数生成默认的实现。Defaulted函数有两个好处:比手工实现更高效,让程序员摆脱了手工定义这些函数的苦差事。
与defaulted函数相反的是deleted函数:
- int func()=delete;
Deleted函数对防止对象复制很有用,回想一下C++自动为类声明一个副本构造函数和一个赋值操作符,要禁用复制,声明这两个特殊的成员函数=delete即可:
- struct NoCopy
- {
- NoCopy & operator =( const NoCopy & ) = delete;
- NoCopy ( const NoCopy & ) = delete;
- };
- NoCopy a;
- NoCopy b(a); //compilation error, copy ctor is deleted
委托构造函数
在C++11中,构造函数可以调用相同类中的其它构造函数:
- class M //C++11 delegating constructors
- {
- int x, y;
- char *p;
- public:
- M(int v) : x(v), y(0), p(new char [MAX]) {} //#1 target
- M(): M(0) {cout<<"delegating ctor"<
构造函数#2,委托构造函数,调用目标构造函数#1。
线程库
站在程序员的角度来看,C++11最重要的新功能毫无疑问是并行操作,C++11拥有一个代表执行线程的线程类,在并行环境中用于同步,async()函数模板启动并行任务,为线程独特的数据声明thread_local存储类型。如果你想找C++11线程库的快速教程,请阅读Anthony William的“C++0x中更简单的多线程”。
新的算法
C++11标准库定义了新的算法模仿all_of(),any_of()和none_of()操作,下面列出适用于ispositive()到(first, first+n)范围,且使用all_of(), any_of() and none_of() 检查范围的属性的谓词:
- #include <algorithm>
- //C++11 code
- //are all of the elements positive?
- all_of(first, first+n, ispositive()); //false
- //is there at least one positive element?
- any_of(first, first+n, ispositive());//true
- // are none of the elements positive?
- none_of(first, first+n, ispositive()); //false
一种新型copy_n算法也可用了,使用copy_n()函数,复制一个包含5个元素的数组到另一个数组的代码如下:
- #include
- int source[5]={0,12,34,50,80};
- int target[5];
- //copy 5 elements from source to target
- copy_n(source,5,target);
算法iota()创建了一个值顺序递增的范围,好像分配一个初始值给*first,然后使用前缀++使值递增,在下面的代码中,iota()分配连续值{10,11,12,13,14}给数组arr,并将{‘a’,’b’,’c’}分配给char数组c。
- include <numeric>
- int a[5]={0};
- char c[3]={0};
- iota(a, a+5, 10); //changes a to {10,11,12,13,14}
- iota(c, c+3, 'a'); //{'a','b','c'}
C++11仍然缺乏一些有用的库,如XML API,套接字,GUI,反射以及前面提到的一个合适的自动垃圾回收器,但C++11的确也带来了许多新特性,让C++变得更加安全,高效,易学易用。
如果C++11的变化对你来说太大的话,也不要惊慌,多花些时间逐渐消化这一切,当你完全吸收了C++11的变化后,你可能就会同意Stroustrup的说法:C++11感觉就像一个新语言,一个更好的新语言。
变长参数的模板
我们在C++中都用过pair,pair可以使用make_pair构造,构造一个包含两种不同类型的数据的容器。比如,如下代码:
auto p = make_pair(1, "C++ 11");
由于在C++11中引入了变长参数模板,所以发明了新的数据类型:tuple,tuple是一个N元组,可以传入1个, 2个甚至多个不同类型的数据
auto t1 = make_tuple(1, 2.0, "C++ 11"); auto t2 = make_tuple(1, 2.0, "C++ 11", {1, 0, 2});
这样就避免了从前的pair中嵌套pair的丑陋做法,使得代码更加整洁
另一个经常见到的例子是Print函数,在C语言中printf可以传入多个参数,在C++11中,我们可以用变长参数模板实现更简洁的Print
template<typename head, typename... tail> void Print(Head head, typename... tail) { cout<< head <<endl; Print(tail...); }