十二、 C++特性之 杂合

一笑奈何 提交于 2020-03-06 23:50:02

static_assert和 type traits

static_assert提供一个编译时的断言检查。如果断言为真,什么也不会发生。如果断言为假,编译器会打印一个特殊的错误信息。

  1. template <typename T, size_t Size> 
  2. class Vector 
  3.    static_assert(Size < 3, "Size is too small"); 
  4.    T _points[Size]; 
  5. }; 
  6.   
  7. int main() 
  8.    Vector<int, 16> a1; 
  9.    Vector<double, 2> a2; 
  10.    return 0; 
  11. }
  1. error C2338: Size is too small 
  2. see reference to class template instantiation 'Vector<T,Size>' being compiled 
  3.    with 
  4.    [ 
  5.       T=double, 
  6.       Size=2 
  7.    ] 

 

static_assert和type traits一起使用能发挥更大的威力。type traits是一些class,在编译时提供关于类型的信息。在头文件<type_traits>中可以找到它们。这个头文件中有好几种 class: helper class,用来产生编译时常量。type traits class,用来在编译时获取类型信息,还有就是type transformation class,他们可以将已存在的类型变换为新的类型。

下面这段代码原本期望只做用于整数类型。

  1. template <typename T1, typename T2> 
  2. auto add(T1 t1, T2 t2) -> decltype(t1 + t2) 
  3. return t1 + t2; 

但是如果有人写出如下代码,编译器并不会报错

  1. std::cout << add(1, 3.14) << std::endl; 
  2. std::cout << add("one", 2) << std::endl; 

程序会打印出4.14和”e”。但是如果我们加上编译时断言,那么以上两行将产生编译错误。

  1. template <typename T1, typename T2> 
  2. auto add(T1 t1, T2 t2) -> decltype(t1 + t2) 
  3.    static_assert(std::is_integral<T1>::value, "Type T1 must be integral"); 
  4.    static_assert(std::is_integral<T2>::value, "Type T2 must be integral"); 
  5.   
  6.    return t1 + t2; 
  7. }
  1. error C2338: Type T2 must be integral 
  2. see reference to function template instantiation 'T2 add<int,double>(T1,T2)' being compiled 
  3.    with 
  4.    [ 
  5.       T2=double, 
  6.       T1=int 
  7.    ] 
  8. error C2338: Type T1 must be integral 
  9. see reference to function template instantiation 'T1 add<const char*,int>(T1,T2)' being compiled 
  10.    with 
  11.    [ 
  12.       T1=const char *, 
  13.       T2=int 
  14.    ] 

 

 

 

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类型的数组,还有一个记录数组长度的变量。

 

  1. template <typename T> 
  2. class Buffer 
  3.    std::string          _name; 
  4.    size_t               _size; 
  5.    std::unique_ptr<T[]> _buffer; 
  6.   
  7. public: 
  8.    // default constructor 
  9.    Buffer(): 
  10.       _size(16), 
  11.       _buffer(new T[16]) 
  12.    {} 
  13.   
  14.    // constructor 
  15.    Buffer(const std::string& name, size_t size): 
  16.       _name(name), 
  17.       _size(size), 
  18.       _buffer(new T[size]) 
  19.    {} 
  20.   
  21.    // copy constructor 
  22.    Buffer(const Buffer& copy): 
  23.       _name(copy._name), 
  24.       _size(copy._size), 
  25.       _buffer(new T[copy._size]) 
  26.    { 
  27.       T* source = copy._buffer.get(); 
  28.       T* dest = _buffer.get(); 
  29.       std::copy(source, source + copy._size, dest); 
  30.    } 
  31.   
  32.    // copy assignment operator 
  33.    Buffer& operator=(const Buffer& copy) 
  34.    { 
  35.       if(this != ©) 
  36.       { 
  37.          _name = copy._name; 
  38.   
  39.          if(_size != copy._size) 
  40.          { 
  41.             _buffer = nullptr; 
  42.             _size = copy._size; 
  43.             _buffer = _size > 0 > new T[_size] : nullptr; 
  44.          } 
  45.   
  46.          T* source = copy._buffer.get(); 
  47.          T* dest = _buffer.get(); 
  48.          std::copy(source, source + copy._size, dest); 
  49.       } 
  50.   
  51.       return *this; 
  52.    } 
  53.   
  54.    // move constructor 
  55.    Buffer(Buffer&& temp): 
  56.       _name(std::move(temp._name)), 
  57.       _size(temp._size), 
  58.       _buffer(std::move(temp._buffer)) 
  59.    { 
  60.       temp._buffer = nullptr; 
  61.       temp._size = 0; 
  62.    } 
  63.   
  64.    // move assignment operator 
  65.    Buffer& operator=(Buffer&& temp) 
  66.    { 
  67.       assert(this != &temp); // assert if this is not a temporary 
  68.   
  69.       _buffer = nullptr; 
  70.       _size = temp._size; 
  71.       _buffer = std::move(temp._buffer); 
  72.   
  73.       _name = std::move(temp._name); 
  74.   
  75.       temp._buffer = nullptr; 
  76.       temp._size = 0; 
  77.   
  78.       return *this; 
  79.    } 
  80. }; 
  81.   
  82. template <typename T> 
  83. Buffer<T> getBuffer(const std::string& name) 
  84.    Buffer<T> b(name, 128); 
  85.    return b; 
  86. int main() 
  87.    Buffer<int> b1; 
  88.    Buffer<int> b2("buf2", 64); 
  89.    Buffer<int> b3 = b2; 
  90.    Buffer<int> b4 = getBuffer<int>("buf4"); 
  91.    b1 = getBuffer<int>("buf5"); 
  92.    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同学提供了另一种可能的实现。为了方便查看,我把它也列在下面:

  1. template <typename T> 
  2. class Buffer 
  3.    std::string          _name; 
  4.    size_t               _size; 
  5.    std::unique_ptr<T[]> _buffer; 
  6.   
  7. public: 
  8.    // constructor 
  9.    Buffer(const std::string& name = "", size_t size = 16): 
  10.       _name(name), 
  11.       _size(size), 
  12.       _buffer(size? new T[size] : nullptr) 
  13.    {} 
  14.   
  15.    // copy constructor 
  16.    Buffer(const Buffer& copy): 
  17.       _name(copy._name), 
  18.       _size(copy._size), 
  19.       _buffer(copy._size? new T[copy._size] : nullptr) 
  20.    { 
  21.       T* source = copy._buffer.get(); 
  22.       T* dest = _buffer.get(); 
  23.       std::copy(source, source + copy._size, dest); 
  24.    } 
  25.   
  26.    // copy assignment operator 
  27.    Buffer& operator=(Buffer copy) 
  28.    { 
  29.        swap(*this, copy); 
  30.        return *this; 
  31.    } 
  32.   
  33.    // move constructor 
  34.    Buffer(Buffer&& temp):Buffer() 
  35.    { 
  36.       swap(*this, temp); 
  37.    } 
  38.   
  39.    friend void swap(Buffer& first, Buffer& second) noexcept 
  40.    { 
  41.        using std::swap; 
  42.        swap(first._name  , second._name); 
  43.        swap(first._size  , second._size); 
  44.        swap(first._buffer, second._buffer); 
  45.    } 
  46. }; 

结论

关于C++11还有很多要说的。本文只是各种入门介绍中的一个。本文展示了一系列C++开发者应当使用的核心语言特性与标准库函数。然而我建议你能更加深入地学习,至少也要再看看本文所介绍的特性中的部分。

 

 

 

Deleted和Defaulted函数

一个表单中的函数:

  1. struct A  
  2. {  
  3.  A()=default; //C++11  
  4.  virtual ~A()=default; //C++11  
  5. };  

被称为一个defaulted函数,“=default;”告诉编译器为函数生成默认的实现。Defaulted函数有两个好处:比手工实现更高效,让程序员摆脱了手工定义这些函数的苦差事。

与defaulted函数相反的是deleted函数:

  1. int func()=delete;

Deleted函数对防止对象复制很有用,回想一下C++自动为类声明一个副本构造函数和一个赋值操作符,要禁用复制,声明这两个特殊的成员函数=delete即可:

  1. struct NoCopy  
  2. {  
  3.     NoCopy & operator =( const NoCopy & ) = delete;  
  4.     NoCopy ( const NoCopy & ) = delete;  
  5. };  
  6. NoCopy a;  
  7. NoCopy b(a); //compilation error, copy ctor is deleted

 

 

委托构造函数

在C++11中,构造函数可以调用相同类中的其它构造函数:

  1. class M //C++11 delegating constructors  
  2. {  
  3.  int x, y;  
  4.  char *p;  
  5. public:  
  6.  M(int v) : x(v), y(0),  p(new char [MAX])  {} //#1 target  
  7.  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() 检查范围的属性的谓词:

  1. #include <algorithm>  
  2. //C++11 code  
  3. //are all of the elements positive?  
  4. all_of(first, first+n, ispositive()); //false  
  5. //is there at least one positive element?  
  6. any_of(first, first+n, ispositive());//true  
  7. // are none of the elements positive?  
  8. none_of(first, first+n, ispositive()); //false  

一种新型copy_n算法也可用了,使用copy_n()函数,复制一个包含5个元素的数组到另一个数组的代码如下:

  1. #include  
  2. int source[5]={0,12,34,50,80};  
  3. int target[5];  
  4. //copy 5 elements from source to target  
  5. copy_n(source,5,target);  

算法iota()创建了一个值顺序递增的范围,好像分配一个初始值给*first,然后使用前缀++使值递增,在下面的代码中,iota()分配连续值{10,11,12,13,14}给数组arr,并将{‘a’,’b’,’c’}分配给char数组c。

  1. include <numeric>  
  2. int a[5]={0};  
  3. char c[3]={0};  
  4. iota(a, a+5, 10); //changes a to {10,11,12,13,14}  
  5. 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...);
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!