• 十二、 C++特性之 杂合


    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...);
    }
  • 相关阅读:
    EEPROM与FLASH
    Sublime Markdown预览插件安装流程
    挖矿算法比较(转)
    CPU based record
    MVC学习之数据库开发模式:三种开发模式总结:
    MVC学习之数据库开发模式:模型优先实例介绍
    MVC学习之数据库开发模式:代码优先实例介绍
    MVC学习之数据库开发模式:数据库优先实例介绍
    DataSet之将连个DataSet集合合并成一个DataSet集合
    索引和长度必须引用该字符串内的位置。参数名: length
  • 原文地址:https://www.cnblogs.com/zenseven/p/4188360.html
Copyright © 2020-2023  润新知