社区应用 最新帖子 精华区 社区服务 会员列表 统计排行 银行

  • 25390阅读
  • 45回复

C++0x2011年8月10日获通过正式成为国际标准

级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 20楼 发表于: 2012-03-06
C++0X 右值引用

简介
C++0X是新一代C++标准的非官方名称。它将代替C++03成为C++最新的标准。C++0XC++的核心语言以及标准库经进行了部分更新。C++0X完全兼容目前的C++标准。
C++0X之中,语言本身添加了对于多线程的支持。一些细节得到了改进,例如支持统一初始化方式。对泛型编程的支持方面有了更边界的,同时对功能进行了改进。
核心语言功能
右值引用
C++03中,临时变量(右值,出现在等号右侧的变量。另一种解释是引用值,没有实际地址的值)和const & type没有实际的差别。而在C++0X标准中添加了一种新的引用类型。叫做右值引用。它的定义类似于typename &&。另外右值引用提供了一种转移语义。
动机
右值引用的出现使得一些基础概念或者effective C++中表述的一些条目需要重新编写。那么来看看右值引用出现的动机是什么。
来看一个例子
std::vector实际上内部就是一个C类型数组,并且提供了一些内存和性能优化管理的机制。那么在现行的C++03标准中,当一个vector临时变量或者函数返回一个vector类型对象的时候。这里无可避免、毫无疑问的将会产生一个新的vector变量,并且将所有右值中的成员变量拷贝到这个临时变量中。(当然有些编译器具有NRV的优化功能,但是这种优化功能的执行情况和在复杂函数中的优化效果值得商榷)并且在拷贝结束后这个临时变量将被销毁,完成它短暂而又神圣的使命。
vector<SpecialClass> func()
{
              …
                return vect;
}
使用右值引用,在以上场景中std::vector的转移构造函数会被调用。转移的过程是把右值vector中指向C类型数组的指针直接拷贝到新的vector对象中。然后把右值中的数组指针置空。这样就省去了数组拷贝过程,并且删除一个空的临时变量不会产生大量的内存操作。
那么我们需要将返回值声明为std::vector<>&&,将会大幅简化内存操作。
vector<SpecialClass>&& func()
{
              …
                return std::move(vect);
}

右值引用不能引用一个左值对象,为了可以对指定的左值对象进行右值引用操作。标准库提供了std::move()函数。通过该函数可以获得指定对象的左值。
为自己的类定制右值引用构造函数
SpecialClass(SpecialClass&& rhs)
{

return *this;
}
当调用右值引用的类并没有定义右值引用构造函数时,默认的拷贝构造函数会被调用,并且参数会以const &的形式传入。
为了便于在模板中进行参数推到,标准库提供了std::forward()函数通过此函数可以保证传入参数的左右值性质不变
template <class T, class A1>
inline
shared_ptr<T>
factory(A1&& a1)
{
    // If a1 is bound to an lvalue, it is forwarded as an lvalue
    // If a1 is bound to an rvalue, it is forwarded as an rvalue
    return shared_ptr<T>(new T(forward<A1>(a1)));
}

struct A
{
    ...
    A(const A&);  // lvalues are copied from
    A(A&&);       // rvalues are moved from
};

int main()
{
    A a;
    shared_ptr<A> sp1 = factory<A, A>(a);        // "a" copied from
    shared_ptr<A> sp2 = factory<A, A>(move(a));  // "a" moved from
}
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 21楼 发表于: 2012-03-06
C++ 0x 之左值与右值、右值引用、移动语义、传导模板
文 / 李博(光宇广贞)
    左值与右值
       左值与右值的概念要追溯到 C 语言,由 C++ 语言继承了上来。C++ 03 3.10/1 如是说:“Every expression is either an lvalue or an rvalue.”左值与右值是指表达式的属性,而非对像的属性。
       左值具名,对应指定内存域,可访问;右值不具名,不对应内存域,不可访问。临时对像是右值。左值可处于等号左边,右值只能放在等号右边。区分表达式的左右值属性有一个简便方法:若可对表达式用 & 符取址,则为左值,否则为右值。
       注意区分 ++x 与 x++。前者是左值表达式,后者是右值表达式。前者修改自身值,并返回自身;后者先创建一个临时对像,并用 x 的值赋之,后将修改 x 的值,最后返回临时对像。
       函数的返回值一般情况下是右值,C++ 03 5.2.2/10 如是说:“A function call is an lvalue if and only if the result type is a reference.”比如有 vector<int> v 对像,则 v[0] 即为左值,因为 vector 容器的 [] 算符重载函数的返回值为引用。
       左值与右值均可以声明为 const 和 non-const。

    拼接字串的问题
       上面提到函数返回值一般为右值,也即临时对像。对于内置类型(built-in type)来说,临时对像还是可忍的。但对于容器对像来说就是极大的浪费了。举一个 C++ 98/03 标准下最通俗的例子,拼接字符串

图一
       图一第 18 行,短短一句,背后动作极其复杂。我要把 string 对像和常量字串交替拼接起来,问题重点在于如何重载 + 算符。有如下几点需要考虑:
  1. 过程中分别出现 string 对像与常量字串的加法、string 对像与 string 对像的加法。因此需要重载多种 + 算符函数。(加法自左至右)
  2. 比如 string 对像与常量字串的加法,返回的将是一个新生成的 string 对像,因此必须返回这个对像的复本,是临时对像,是右值。又由于加法是连续运算的,下一个加法的重载函数为了接收这一右值,参数表只得写成传值的形式,也即将此临时对像再复制一次,才可传到函数体内操作。总的来说,就是临时对像由前一个函数体转到另一个函数体,需要深度复制两次
  3. 由第二点可知,仅仅由一个加号过渡到另一个加号,就要产生两个昙花一现、转瞬即逝的临时对像复本。若每个字串都很长,对像都很大,拼接个数又特别多,这要产生多少垃圾?为何不能把前一个函数返回时产生的临时对像不用复制,直接拿给下一个函数用呢?也就是从前一个函数“移动”到后一个函数体中。
  4. 对于第三点,C++ 98/03 不允许这么做。因为语义上不支持。由于缺乏“移动语义”,前一个函数产生的临时对像将在函数体退出时析构,外部要想获得只能使用其复本,本体已经不存在了。

    右值引用和移动语义
       针对上述拼接字串的问题,若说,函数返回时产生的临时对像需要复制出去还情有可原——毕竟人家的作用域到头儿了,本体的确不能传递到外部,只能由复本代劳(这是 C++ 与 C# 最大的不同之一);不过话又说回来,复本都复制出来了,为何传递到下一个函数体内还需要再复制一次呢?C++ 98/03 说得是理直气壮:
       “因为我规定了,右值不但不能取址,连引用都不能取!谁让丫传的是临时对像,是右值,传参只能传值!”
       话说得多气人呐!凭什么连引用都不能取?传值就意味着深度复制。C++ 标准委员会发现了这一问题,决定在 C++ 0x 新标准中补充“右值引用”和“移动语义”。
       移动语义:将对方掏空,实体吸收给我自己。见《测试 VS 2010 对 C++ 0x 标准的谨慎支持》。
       举一个临时对像由一个函数传往另一个函数的例子以说明问题。由例子可见,Sck 函数使用右值引用重载版本,接收 Fck 函数返回的临时对像。而在 Fck 函数返回时,完成了一次 Sb 对像的复制。如图:

图二
      关于右值引用和移动语义的更多例子,请参见微软 VC 官方博客:《Rvalue Reference》。

    右值引用重载函数几点
  1. 移动构造重载函数和移动赋值算符(assignment operators:=、^=、+=,etc.)重载函数绝不会隐式声明,必须自己定义。
  2. 默认构造函数会被用户自己显式定义的构造函数压制,包括用户自定义复制构造函数和移动构造函数。因故若用户已自定义复制和移动构造函数,且需要无参构造函数时,也需要自己定义。
  3. 隐式复制构造函数会被用户自己显式定义的复制构造函数覆盖,而不是自定义的移动构造函数。
  4. 隐式复制赋值重载函数会被用户自己显式定义的复制赋值重载函数覆盖,而不是自定义的移动赋值重载函数。
       总之一句话,一个类定义完了,程序员嘛也不管,默认构造函数、默认复制构造函数、默认复制赋值函数,编译器都会自动生成。而移动语义的构造函数和赋值函数,则必须由程序员自己显式定义方可使用。

    操作右值对像实现移动语义
       操作右值对像实现移动语义,须使用 std::move () 方法。无论是对类对像,还是对类对像的成员变量。使用 move 方法需要引用<utility> 头文件。详见下例:

图三

    外围函数向内部函数准确传参的问题
       见如下代码块:
void Outer ( params ) { Inner ( params ); }
       由 Outer 函数接收参数后,要准确无误地传递给 Inner 函数。所谓的准确无误包括 params 的左、右值属性和 const / non-const 属性。此也即“参数传导语义”。实现这一语义的目的是 Inner 函数的类型检查信息可以与 Outer 外部互通,因此由 Outer 到 Inner 之间的参数传导不能对参数属性有任何的改变。
       在 C++ 98/03 标准下,我们可以使用左值引用标识参数类型: T& params;但若我往里传常量呢?常量是右值,传不进去。好,那改成 const T& params 好了,这下左右值都可以传了;但若我要在函数体内修改 params 的值呢?……
       《C++ 0x 之 Lambda:贤妻与娇娃,你娶谁当老婆?听 FP 如何点化 C++》里说:C++ 是万能的。别以为 C++ 没辙了,我可以重载啊!一种版本我满足不了你的所有要求,我重载出满足你要求的所有版本的函数就好了呗!
       嗯……C++ 果真贤惠!好,我一个参数表有 64 个参数,你把所有版本都重载去吧!估计得有 2^64 个这么多……

    传导模板:forward<>
       话说 C++ 0x 之前的 C++ 在这方面表现得实在是太糙了,简直没法儿看……我们所期待的完美解决方案是只用一个模板即可处理所有情况,而重载函数再能用也不能这么用。好在 C++ 0x 的 <utility> 头文件中有 forward 模板:
       template < typename T > void Outer ( T&& t )
       {
              Inner ( std::forward<T> ( t ) );
       }

       不错,写这么一个就解决了。首先 Outer 函数参数表使用 T&& 类型接收参数。推导过程如下:
  • 若参数 t 为 Type& 型即左值引用,则 T&& 推导为 Type& &&,归化为 Type&,为左值引用。
  • 若参数 t 为 Type&& 型即右值引用,则 T&& 推导为 Type&& &&,归化为 Type&&,为右值引用。
  • 若参数 t 为 const Type&(&&) 型,即常左(右)值引用,则 T&& 推导为 const Type&(&&) &&,归化为 const Type&(&&)。
  • 若参数 t 为值类型,则 T&& 为右值引用,待传值型参数为右值。
       一句话,T&& 模板类型可以保留参数信息
       Outer 使用 T&& 是解释清楚了,那 forward<> 是如何保证由 Outer 到 Inner 的平稳过渡呢?若要知 std::move () 和 std::forward () 是如何实现的,请参见:《C++ 0x 之移动语义和传导模板如何实现》。

    题外话:C++的歧义算符
       参见:《C++ 的歧义算符
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 22楼 发表于: 2012-03-22
C++11并发—线程
C++11 引入一个全新的线程库,包含启动和管理线程的工具,提供了同步(互斥、锁和原子变量)的方法,我将试图为你介绍这个全新的线程库。


如果你要编译本文中的代码,你至少需要一个支持 C++11 的编译器,我使用的是 GCC 4.6.1,需要使用 -c++0x 或者 -c++11 参数来启用 C++11 的支持。


启动线程


在 C++11 中启动一个线程是非常简单的,你可以使用 std:thread 来创建一个线程实例,创建完会自动启动,只需要给它传递一个要执行函数的指针即可,请看下面这个 Hello world 代码:


01
#include <thread>
02
#include <iostream>
03
  
04
void hello(){
05
    std::cout << "Hello from thread " << std::endl;
06
}
07
  
08
int main(){
09
    std::thread t1(hello);
10
    t1.join();
11
  
12
    return 0;
13
}
所有跟线程相关的方法都在 thread 这个头文件中定义,比较有意思的是我们在上面的代码调用了 join() 函数,其目的是强迫主线程等待线程执行结束后才退出。如果你没写 join() 这行代码,可能执行的结果是打印了 Hello from thread 和一个新行,也可能没有新行。因为主线程可能在线程执行完毕之前就返回了。


线程标识


每个线程都有一个唯一的 ID 以识别不同的线程,std:thread 类有一个 get_id() 方法返回对应线程的唯一编号,你可以通过 std::this_thread 来访问当前线程实例,下面的例子演示如何使用这个 id:


01
#include <thread>
02
#include <iostream>
03
#include <vector>
04
  
05
void hello(){
06
    std::cout << "Hello from thread " << std::this_thread::get_id() << std::endl;
07
}
08
  
09
int main(){
10
    std::vector<std::thread> threads;
11
  
12
    for(int i = 0; i < 5; ++i){
13
        threads.push_back(std::thread(hello));
14
    }
15
  
16
    for(auto& thread : threads){
17
        thread.join();
18
    }
19
  
20
    return 0;
21
}
依次启动每个线程,然后把它们保存到一个 vector 容器中,程序执行结果是不可预测的,例如:


Hello from thread 140276650997504
Hello from thread 140276667782912
Hello from thread 140276659390208
Hello from thread 140276642604800
Hello from thread 140276676175616


也可能是:


Hello from thread Hello from thread Hello from thread 139810974787328Hello from thread 139810983180032Hello from thread
139810966394624
139810991572736
139810958001920


或者其他结果,因为多个线程的执行是交错的。你完全没有办法去控制线程的执行顺序(否则那还要线程干吗?)


使用 lambda 启动线程
当线程要执行的代码就一点点,你没必要专门为之创建一个函数,你可以使用 lambda 来定义要执行的代码,因此第一个例子我们可以改写为:


01
#include <thread>
02
#include <iostream>
03
#include <vector>
04
  
05
int main(){
06
    std::vector<std::thread> threads;
07
  
08
    for(int i = 0; i < 5; ++i){
09
        threads.push_back(std::thread([](){
10
            std::cout << "Hello from thread " << std::this_thread::get_id() << std::endl;
11
        }));
12
    }
13
  
14
    for(auto& thread : threads){
15
        thread.join();
16
    }
17
  
18
    return 0;
19
}
在这里我们使用了一个 lambda 表达式替换函数指针,而结果是一样的。


Next
下一节我们将学习如何用锁来保护代码
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 23楼 发表于: 2012-04-24
C11标准委员会成员解读C语言新标准

导读:C语言国际标准新的新草案之前已经公布,新标准提高了对C++的兼容性,并将新的特性增加到C语言中。此外支持多线程的功能也受到了开发者的关注,基于ISO/IEC TR 19769:2004规范下支持Unicode,提供更多用于查询浮点数类型特性的宏定义和静态声明功能。根据草案规定,最新发布的标准草案修订了许多特性,支持当前的编译器。(背景:C编程语言的标准化委员会(ISO/IEC JTC1/SC22/WG14)已完成了C标准的主要修订,该标准的早期版本于1999年完成,俗称为“C99”。新标准在去年年底完成,也被称为“C11”。)

本文作者Tom Plum是Plum Hall Inc.的技术工程副总监,也是C11和C++11标准委员会的成员,他在这篇文章里从语言集的atomic操作和线程原语开始探讨了C语言的新特性,在文章末尾还讨论了C11与C++兼容性问题。此外,在本文和它的姊妹篇里,作者还描述了C11的新功能、并发性、安全性和易用性等话题。

并发

C11的标准化了可能运行在多核平台上的多线程程序的语义,使用atomic变量让线程间通信更轻量。

头文件<threads.h>提供宏、类型以及支持多线程的函数。下面是宏、类型和枚举常量的摘要总结:

    宏:

  1. thread_local, ONCE_FLAG, TSS_DTOR_ITERATIONS cnd_t thrd_t, tss_t, mtx_t, tss_dtor_t, thrd_start_t, once_flag。 

    通过枚举常量:

  1. mtx_init: mtx_plain, mtx_recursive, mtx_timed。 

    线程枚举常量:

  1. thrd_timedout, thrd_success, thrd_busy, thrd_error, thrd_nomem。 

    条件变量的函数:

  1. call_once(once_flag *flag, void (*func)(void)); 
  2. cnd_broadcast(cnd_t *cond); 
  3. cnd_destroy(cnd_t *cond); 
  4. cnd_init(cnd_t *cond); 
  5. cnd_signal(cnd_t *cond); 
  6. cnd_timedwait(cnd_t *restrict cond, mtx_t *restrict mtx, const struct timespec *restrict ts); 
  7. cnd_wait(cnd_t *cond, mtx_t *mtx); 

    互斥函数:

  1. void mtx_destroy(mtx_t *mtx); 
  2. int mtx_init(mtx_t *mtx, int type); 
  3. int mtx_lock(mtx_t *mtx); 
  4. int mtx_timedlock(mtx_t *restrict mtx; 
  5. const struct timespec *restrict ts); 
  6. int mtx_trylock(mtx_t *mtx); 
  7. int mtx_unlock(mtx_t *mtx); 

    线程函数:

  1. int thrd_create(thrd_t *thr, thrd_start_t func, void *arg); 
  2. thrd_t thrd_current(void); 
  3. int thrd_detach(thrd_t thr); 
  4. int thrd_equal(thrd_t thr0, thrd_t thr1); 
  5. noreturn void thrd_exit(int res); 
  6. int thrd_join(thrd_t thr, int *res); 
  7. int thrd_sleep(const struct timespec *duration, struct timespec *remaining); 
  8. void thrd_yield(void); 

    特定于线程的存储功能:

  1. int tss_create(tss_t *key, tss_dtor_t dtor); 
  2. void tss_delete(tss_t key); 
  3. void *tss_get(tss_t key); 
  4. int tss_set(tss_t key, void *val); 

这些标准库函数可能更适合作为易用的API的基础而不是开发平台来使用。例如,使用这些低级库的函数时,很容易造成数据竞争,多个进程会不同步地对同一个位置的数据进行操作。C(和C++)标准允许任何行为,即使会发生争用同一个变量x,哪怕会导致严重的后果。例如,多字节值x可能被一个线程修改部分字节,而另一个线程又会修改别的部分(值撕裂),或者产生一些其它的副作用。

下面一个简单的示例程序,它包含一个数据竞争,其中64位的整数x会同时被两个线程改动。

  1. #include <threads.h> 
  2. #include <stdio.h> 
  3. #define N 100000 
  4. char buf1[N][99]={0}, buf2[N][99]={0}; 
  5. long long old1, old2, limit=N; 
  6. long long x = 0; 
  7.   
  8. static void do1()  { 
  9.    long long o1, o2, n1; 
  10.    for (long long i1 = 1; i1 < limit; ++i1) { 
  11.       old1 = x, x = i1; 
  12.       o1 = old1;  o2 = old2; 
  13.       if (o1 > 0) { // x was set by this thread 
  14.          if (o1 != i1-1) 
  15.             sprintf(buf1[i1], "thread 1: o1=%7lld, i1=%7lld, o2=%7lld"
  16.                      o1, i1, o2); 
  17.       } else {      // x was set by the other thread 
  18.          n1 = x, x = i1; 
  19.          if (n1 < 0 && n1 > o1) 
  20.             sprintf(buf1[i1], "thread 1: o1=%7lld, i1=%7lld, n1=%7lld"
  21.                      o1, i1, n1); 
  22.       }         
  23.    } 
  24.   
  25. static void do2()  { 
  26.    long long o1, o2, n2; 
  27.    for (long long i2 = -1; i2 > -limit; --i2) { 
  28.       old2 = x, x = i2; 
  29.       o1 = old1; o2 = old2; 
  30.       if (o2 < 0) { // x was set by this thread 
  31.          if (o2 != i2+1) 
  32.             sprintf(buf2[-i2], "thread 2: o2=%7lld, i2=%7lld, o1=%7lld"
  33.                      o2, i2, o1); 
  34.       } else {      // x was set by the other thread 
  35.          n2 = x, x = i2; 
  36.          if (n2 > 0 && n2 < o2) 
  37.             sprintf(buf2[-i2], "thread 2: o2=%7lld, i2=%7lld, n2=%7lld"
  38.                      o2, i2, n2); 
  39.       } 
  40.    } 
  41.   
  42. int main(int argc, char *argv[]) 
  43.    thrd_t thr1; 
  44.    thrd_t thr2; 
  45.    thrd_create(&thr1, do1, 0); 
  46.    thrd_create(&thr2, do2, 0); 
  47.    thrd_join(&thr2, 0); 
  48.    thrd_join(&thr1, 0); 
  49.    for (long long i = 0; i < limit; ++i) { 
  50.       if (buf1[0] != '\0'
  51.          printf("%s\n", buf1); 
  52.       if (buf2[0] != '\0'
  53.          printf("%s\n", buf2); 
  54.    } 
  55.    return 0; 
  56. }  

如果你的应用已经符合C11的标准,并且将它在一个32位的机器编译过了(在64位机器里long long类型会占用两倍以上的存储周期),你将会看到数据竞争的结果,得到像下面乱码一样的输出:

  1. thread 2: o2=-4294947504, i2=    -21, o1=  19792 

传统的解决方案是通过创建一个锁来解决数据竞争。然而,用atomic数据有时会更高效。加载和存储atomic类型循序渐进、始终如一。特别是如果线程1存储了一个值名为x的atomic类型变量,线程2读取该值时则可以看到之前在线程1中运行的所有其它存储(即使是非atomic对象)。(C11和C++11标准还提供其他内存一致性的模型,虽然专家提醒要远离它们。)

新的头文件<stdatomic.h>提供了很多命名类和函数来操作atomic数据的大集。例如,atomic_llong就是一个为操作atomic long long整数的类型。所有的整数类都提供了相似的命名。该标准包括一个ATOMIC_VAR_INIT(n)宏,用来初始化atomic整数,如下:

之前的数据竞争的例子可以通过定义x为一个atomic_llong类型的变量解决。简单地改变一下上述例子中声明x的那行语句:

  1. #include <stdatomic.h> 
  2. atomic_llong x = ATOMIC_VAR_INIT(0); 

通过使用这样的atomic变量,代码在运行中不会出现任何数据竞争的问题。

注意关键字

C委员会并不希望在用户命名空间里创建新的关键字,每个新的C版本都一直在避免出现不兼容之前版本C程序的问题。相比之下,C++委员会(WG21)更喜欢创造新的关键字,例如:C++11定义一个新的thread_local关键字来指定一个线程的本地静态存储,C11使用_Thread_local来代替,在C11新的头文件<threads.h>中有一个宏定义来提供normal-looking name:

  1. #define   thread_local    _Thread_local 

下面我将假设你已经引入例如适当的头文件,所以我会显示normal-looking name。

thread_local存储类

新thread_local存储类为每个线程提供一个单独的静态存储,并且在线程运行之前初始化。但有没有保障措施来防止你捕获一个thread_local变量的地址,并把它传递给其他线程,然后明确实现(即,不便携/不可移植),每个线程在thread_local都有它自己的errno存储副本。

线程可选

C11已指定为多种特色为可选功能,例如:如果它明确实现了一个名为 _ _STDC_NO_THREADS_ _ 的宏,就不会提供一个头名为<threads.h>的头文件,也不可能在其中定义任何函数。

设计准则

作为通则,WG21委托Bjarne Stroustrup整体设计和进化的责任,不明白的话可以在网上搜索“camel is a horse designed by committee”。然而,有一个WG14和WG21同样遵循的设计原则:不要给任何系统编程语言比我们(C/C++)更高效的机会!

一些与会者(称他们为“A组”)认为atomic数据仍将是一个很少使用的专业性功能,但其他人(称他们为“B组”)认为,atomic数据将成为一个重要的功能,至少会在一种系统编程语言中被应用。

在过去的几十年里,很多更高级别的语言都是基于C语言创建的(Java,C#,ObjectiveC,当然,也包括C++)和C++子集或超集(如D和嵌入式C++)。许多参与WG14和WG21的公司已经决定使用这些语言作为自己的应用的编程语言(称他们为“C组”)。这些公司之所以选择C++作为他们的上层应用程序的语言,通常是因为C足够稳定(或者说是因为WG21控制其标准化),而选择其他语言的公司(称他们为“D组”)通常认为C是他们所使用的高级语言非常重要的基石。

有了这些背景,我可以得出一个C11中atomic进化的理由。C++11中对atomic的设计充分运用了模板。比如atomic<T>是获得任何一种类型T的简单且常用的方法。即使T是一个类或结构,那么无论T*指向哪儿,atomic<T*>都会保存类型信息,但是,几年来,C语言的设计依然只用了几十种命名类型(如上所示的atomic_llong)。这样做的主要优点在于:它不需要编译器做任何改变。它能够实现一个仅库的解决方案,会在最低水平调用系统依赖的原生函数。然而命名类型的解决方案干挠了为C结构或者T*指针创建atomic(无论多么小)。主要因为B组和D组的意见,WG14决定要求C11编译器为任何类型的T识别atomicT。

后来也有一个WG14内部的关于编译器识别atomicT语法的争论。一种使用atomic-parenthesis的解决方案因为和C++良好的兼容性而被推动。Let _Atomic(T)成为了指定atomicT的语法。同样的源程序能够简单地在C++定义宏中编译。

  1. #define _Atomic(T)    atomic<T> 

另一种相反的观点认为应该创建新的类限制符(类似于C99的_Complex的解决方案);使用"atomic-space"语法,atomic T类型应该被写为“_Atomic T”。使用这种语法的程序型的程序无法立刻被当作C++来编译。(无兼容性宏,本质上看起来像atomic-parenthesis的方法)。

获取C11标准

新标准可以在webstore.ansi.org查看(或者搜索"ISO/IEC 9899:2011")。现在已经有了PDF文档,但是需要支付$285(和C++2011一样)才可以使用。一旦这些标准被ANSI采纳为美国国家标准,价格将下降至$30左右。

你可以定期检查此页面的部分,我及时检查草案的状态,如果您填写了这个Web表单,将会在C11和C++11被ANSI采用为标准时获得电子邮件通知。也可以通过tplum@plumhall.com联系我,非常感谢来自Pete Becker(C++2011标准的项目编辑)的建设性建议。

QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 24楼 发表于: 2012-05-14
9 个开始使用 C++11 的理由
如果你的代码工作正常并且表现良好,你可能会想知道为什么还要使用C++ 11。当然了,使用用最新的技术感觉很好,但是事实上它是否值得呢?

在我看来,答案毫无疑问是肯定的。我在下面给出了9个理由,它们分为两类:性能优势和开发效率。

获得性能优势

理由1:move语义(move semantics)。简单的说,它是优化复制的一种方式。有时候复制很显然是浪费的。如果你从一个临时的string对象复制内容,简单的复制指针到字符缓冲区将比创建一个新的缓冲区再复制要高效得多。他之所以能工作是因为源对象超出了范围。

然而,在这以前C++并没有判断源对象是不是临时对象的机制。move语义通过除了复制操作外还允许你有一个move构造函数(move constructor)和一个move赋值运算(move assignment)符来提供这个机制。

你知道吗?当你在Visual Studio 2010中使用标准库中的类如string或vector时,它们已经支持move语义了。这可以防止不必要的的复制从而改善性能。

通过在你的类中实现move语义你可以获得额外的性能提升,比如当你把它们存储到STL容器中时。还有,move语义不仅可以应用到构造函数,还可以应用到方法(如vector的push_back方法)。

理由2:通过使用类别属性(type traits,如is_floating_point)和模板元编程(template metaprogramming,如enable_if template),你可以为某些特定的类型定制模版,这可以实现优化。

理由3:哈希表现在已经是标准实现的了,它提供更快速的插入、删除和查找,这在处理大量数据时很有用。你现在可以随便使用unordered_map, unordered_multimap, unordered_set 和unordered_multiset这几种数据结构了。

提高效率

提高效率不仅都是在代码性能方面,开发时间也是宝贵的。C++ 11可以让你的代码更短、更清晰、和更易于阅读,这可以让你的效率更高。

理由4:auto关键字可以自动推断类型,所以下面的代码:

vector<vector<MyType>>::const_iterator it = v.begin()


现在可以很简单的写成:

auto it = v.cbegin()
尽管有些人会说,它隐藏了类型信息,在我看来它利大于弊,因为它减少了视觉混换并展示了代码的行为,还有它可以让你我少打很多字!

理由5:Lambda表达式提供了一种方法来定义匿名方法对象(实际上是闭包),这是代码更加线性和有规律可循。这在和STL算法结合使用时很方便:

bool is_fuel_level_safe()
{
    return all_of(_tanks.begin(), _tanks.end(),
        [this](Tank& t) { return t.fuel_level() > _min_fuel_level; });
}
理由6:新的智能指针(smart pointer)替换了有问题的auto_ptr,你可以不用担心内存的释放并移除相关释放内存的代码了。这让代码更清晰,并杜绝了内存泄露和查找内存泄露的时间。

理由7:把方法作为first class object是一个非常强大的特性,这让你的代码变得更灵活和通用了。C++的std::function提供了这方面的功能。方法提供一种包装和传递任何可调用的东西-函数指针, 仿函数(functor), lambda表达式等。

理由8:还有许多其它小的功能,如override、final关键字和nullptr让你的代码意图更明确。对我来说,减少视觉混乱和代码中能够更清楚地表达我的意图意味着更高兴、更高效。

另一个开发效率的方面是错误检测。如果你的错误在运行时发生,这意味着你至少需要运行软件,并可能得通过一系列步骤来重现错误,这需要时间。

C++ 11提供了一种方法来检查先决条件并尽早的在可能的时机捕获错误-编译过程中,在你运行代码前。这就是理由9。

这是通过静态断言(static_assert)和类别属性模版实现的。这种方法的另一个好处是,它不需要占用任何的运行时开销,没有什么性能损失!

现在开始掌握C++ 11

在C++ 11标准中除了上描述的还有更多的改动和新功能,它需要一整本数来描述。不过,我相信它们是值得你花时间去学习的。你将省去以往花在提高效率上的时间。很多主流的编译器已经开始支持C++ 11的一些标准了。还等什么?开始吧!

注:很多名词觉得翻译成了中文还不如看英文来的舒服,翻译成了中文的后面括号里备注了原英文单词。
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 25楼 发表于: 2012-05-15
C++11 中委派 (Delegates) 的实现
介绍
在 C++ 中通过一个全局函数来绑定到对象的成员函数是很有用的,这个特性也存在于其他语言中,例如 C#的委派。在 C++ 中相当于成员函数指针,但是并没有提供相应的特性。在这篇文章中,我想提出一个简单的 C++ 委派的实现,是用 C++ 成员函数指针和 C++11 的可变模板(variadic templates),目前这套实现方法仅支持 GNU C++ 4.7.0,在 Windows 下可使用 MinGW。


背景
在我的方法中奖提供一个 create_delegate 函数,可通过下面两种方法来调用:


create_delegate(&object, &member_function)
create_delegate(&function)
第一种方法创建一个对象并提供一个 operator() 成员函数,第二个方法生成一个函数指针,两种方法都兼容 type function<...>.


示例程序
首先我们定义一个包含多个方法的类:


01
class A
02
{
03
    int i;
04
public:  
05
    A(int k):i(k) {}
06

07
    auto get()const ->int { return i;}  
08
    auto set(int v)->void { i = v;}
09

10
    auto inc(int g)->int& { i+=g; return i;}
11
    auto incp(int& g)->int& { g+=i; return g;}
12

13
    auto f5 (int a1, int a2, int a3, int a4, int a5)const ->int
14
    {
15
        return i+a1+a2+a3+a4+a5;
16
    }
17

18
    auto set_sum4(int &k, int a1, int a2, int a3, int a4)->void
19
    {
20
        i+=a1+a2+a3+a4;
21
        k = i;
22
    }
23

24
    auto f8 (int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8) const ->int
25
    {
26
        return i+a1+a2+a3+a4+a5+a6+a7+a8;
27
    }  
28

29
    static auto sqr(double x)->double { return x*x; }
30
};
请注意你并不需要一定使用 C++ 的 auto 函数语法,你也可以使用传统的方法,然后我们使用下面方法创建一个类:


1
A a(11);
接下来我们创建委派:


1
auto set1 = create_delegate(&a,&A::set);
2
auto inc = create_delegate(&a,&A::inc);
3
std::function<int(int&)> incp = create_delegate(&a,&A::incp);
4
auto af5  = create_delegate(&a,&A::f5);
5
auto set_sum4= create_delegate(&a,&A::set_sum4);
6
auto af8  = create_delegate(&a,&A::f8);
7
auto sqr = create_delegate(&A::sqr); // static function </int(int&)>
为了演示,我们使用 auto 或者 function<...>. 然后我们开始调用委派:


01
set1(25);
02
int x = 5;
03
int k = inc(x);
04
k = incp(x);
05
std::cout << "a.get():" << a.get() << std::endl;
06
std::cout << "k: " << k << std::endl;
07
std::cout << "x: " << x << std::endl;
08
std::cout << "af5(1,2,3,4,5): " << af5(1,2,3,4,5) << std::endl;
09

10
set_sum4(x,1,2,3,20);
11
std::cout << "after set_sum4(x,1,2,3,20)" << std::endl;
12
std::cout << "a.get(): " << a.get() << std::endl;
13
std::cout << "x: " << x << std::endl;
14
std::cout << "af8(1,2,3,4,5,6,7,8): " << af8(1,2,3,4,5,6,7,8) << std::endl;
15
std::cout << "sqr(2.1): " << sqr(2.1) << std::endl;
执行上述程序的打印结果如下:


1
a.get():30
2
k: 35
3
x: 35
4
af5(1,2,3,4,5): 45
5
after set_sum4(x,1,2,3,20)
6
a.get(): 56
7
x: 56
8
af8(1,2,3,4,5,6,7,8): 92
9
sqr(2.1): 4.41
关键点
对于一个不是 volatile 和 const 的简单函数而言,实现是非常简单的,我们只需要创建一个类保存两个指针,一个是对象,另外一个是成员函数:
01
template <class T, class R, class ... P>
02
struct  _mem_delegate
03
{
04
    T* m_t;
05
    R  (T::*m_f)(P ...);
06
    _mem_delegate(T* t, R  (T::*f)(P ...) ):m_t(t),m_f(f) {}
07
    R operator()(P ... p)
08
    {
09
            return (m_t->*m_f)(p ...);
10
    }
11
};
可变模板 variadic template 允许定义任意个数和类型参数的 operator() 函数,而 create_function 实现只需简单返回该类的对象:


1
template <class T, class R, class ... P>
2
_mem_delegate<T,R,P ...> create_delegate(T* t, R (T::*f)(P ...))
3
{
4
    _mem_delegate<T,R,P ...> d(t,f);
5
    return d;
6
}
实际中,我们需要另外的三个实现用于覆盖 const、volatile 和 const volatile 三种成员函数,这也是为什么传统使用 #define 宏很便捷的原因,让你无需重写代码段,下面是完整的实现:


view sourceprint?
01
template <class F>
02
F* create_delegate(F* f)
03
{
04
    return f;
05
}
06
#define _MEM_DELEGATES(_Q,_NAME)\
07
template <class T, class R, class ... P>\
08
struct _mem_delegate ## _NAME\
09
{\
10
    T* m_t;\
11
    R  (T::*m_f)(P ...) _Q;\
12
    _mem_delegate ## _NAME(T* t, R  (T::*f)(P ...) _Q):m_t(t),m_f(f) {}\
13
    R operator()(P ... p) _Q\
14
    {\
15
        return (m_t->*m_f)(p ...);\
16
    }\
17
};\
18
\
19
template <class T, class R, class ... P>\
20
    _mem_delegate ## _NAME<T,R,P ...> create_delegate(T* t, R (T::*f)(P ...) _Q)\
21
{\
22
    _mem_delegate ##_NAME<T,R,P ...> d(t,f);\
23
    return d;\
24
}
25

26
_MEM_DELEGATES(,Z)
27
_MEM_DELEGATES(const,X)
28
_MEM_DELEGATES(volatile,Y)
29
_MEM_DELEGATES(const volatile,W)


QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 26楼 发表于: 2012-08-20
C++11各编译器支持情况对比
摘要:C++11标准在去年8月份获得一致通过,迄今为止已整整一年啦!这一年里C++11的发展情况如何呢?一起来看下C++11在VS11 (Visual Studio 2012)、g++ 4.7和Clang 3.1三大编译器支持情况。

C++11标准在去年8月份获得一致通过,这是自1998年后C++语言第一次大修订,对C++语言进行了改进和扩充。迄今为止已整整一年啦!想知道C++11在这一年里的发展情况如何吗?本文我们一起来看下C++11在VS11 (Visual Studio 2012)、g++ 4.7和Clang 3.1三大编译器支持情况。
注:这里我并没有详细描述非语言并发性变化,因为三大编译器对非语言并发性的支持情况依然有限。



Clang在大多数C++11功能实现上处于领先地位,而Visual Studio则稍显落后。当然,这三个编译器都有着不错的子集适用于跨平台开发。
你可以使用类型推断、移动语义、右值引用、nullptr,static_assert,range-based参考对比,同时你还可以使用最终和重写关键字来进行友好的控制。此外,你还可以通过Enums(例举)强类型和提前声明,这里有几个改进后的模板包括extern keyword。
然而,Visual Studio并不支持较多请求的可变参数模板。另一方面,可变参数宏在这三款编译器中只支持C99标准。继承构造函数和广义属性这些特性并不是在任何地方都能获得支持。本地线程存储是是支持情况最好的一部分(通过非关键字标准)。
总的来说,我认为C++11的发展还是很不错的,至少C++11的子集适用于跨平台项目开发。

QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 27楼 发表于: 2012-08-29
C++11(及现代C++风格)和快速迭代式开发
过去的一年我在微软亚洲研究院做输入法,我们的产品叫“英库拼音输入法” (下载Beta版),如果你用过“英库词典”(现已更名为必应词典),应该知道“英库”这个名字(实际上我们的核心开发团队也有很大一部分来源于英库团队的老成员)。整个项目是微软亚洲研究院的自然语言处理组、互联网搜索与挖掘组和我们创新工程中心,以及微软中国Office商务软件部(MODC)多组合作的结果。至于我们的输入法有哪些创新的feature,以及这些feature背后的种种有趣故事… 本文暂不讨论。虽然整个过程中我也参与了很多feature的设想和设计,但90%的职责还是开发,所以作为client端的核心开发人员之一,我想跟大家分享这一年来在项目中全面使用C++11以及现代C++风格(Elements of Modern C++ Style)来做开发的种种经验。


我们用的开发环境是VS2010 SP1,该版本已经支持了相当多的C++11的特性:lambda表达式,右值引用,auto类型推导,static_assert,decltype,nullptr,exception_ptr等等。C++曾经饱受“学院派”标签的困扰,不过这个标签着实被贴得挺冤,C++11的新feature没有一个是从学院派角度出发来设计的,以上提到的所有这些feature都在我们的项目中得到了适得其所的运用,并且带来了很大的收益。尤其是lambda表达式。


说起来我跟C++也算是有相当大的缘分,03年还在读本科的时候,第一篇发表在程序员上面的文章就是Boost库的源码剖析,那个时候Boost库在国内还真是相当的阳春白雪,至今已经快十年了,Boost库如今已经是写C++代码不可或缺的库,被誉为“准标准库”,C++的TR1基本就脱胎于Boost的一系列子库,而TR2同样也大量从Boost库中取材。之后有好几年,我在CSDN上的博客几乎纯粹是C++的前沿技术文章,包括从06年就开始写的“C++0x漫谈”系列。(后来写技术文章写得少了,也就把博客从CSDN博客独立了出来,便是现在的mindhacks.cn)。自从独立博客了之后我就没有再写过C++相关的文章(不过仍然一直对C++的发展保持了一定的关注),一方面我喜欢关注前沿的进展,写完了Boost源码剖析系列和C++0x漫谈系列之后我觉得这一波的前沿进展从大方面来说也都写得差不多了,所以不想再费时间。另一方面的原因也是我虽然对C++关注较深,但实践经验却始终绝大多数都是“替代经验”,即从别人那儿看来的,并非自己第一手的。而过去一年来深度参与的英库输入法项目弥补了这个缺憾,所以我就决定重新开始写一点C++11的实践经验。算是对努力一年的项目发布第一版的一个小结。


09年入职微软亚洲研究院之后,前两年跟C++基本没沾边,第一个项目倒是用C++的,不过是工作在既有代码基上,时间也相对较短。第二个项目为Bing Image Search用javascript写前端,第三个项目则给Visual Studio 2012写Code Clone Detection,用C#和WPF。直到一年前英库输入法这个项目,是我在研究院的第四个项目了,也是最大的一个,一年来我很开心,因为又回到了C++。


这个项目我们从零开始,,而client端的核心开发人员也很紧凑,只有3个。这个项目有很多特殊之处,对高效的快速迭代开发提出了很大的挑战(研究院所倡导的“以实践为驱动的研究(Deployment-Driven-Research)”要求我们迅速对用户的需求作出响应):


长期时间压力:从零开始到发布,只有一年时间,我们既要在主要feature上能和主流的输入法相较,还需要实现我们自己独特的创新feature,从而能够和其他输入法产品区分开来。
短期时间压力:输入法在中国是一个非常成熟的市场,谁也没法保证闷着头搞一年搞出来的东西就一炮而红,所以我们从第一天起就进入demo驱动的准迭代式开发,整个过程中必须不断有阶段性输出,抬头看路好过闷头走路。但工程师最头疼的二难问题之一恐怕就是短期与长远的矛盾:要持续不断出短期的成果,就必须经常在某些地方赶工,赶工的结果则可能导致在设计和代码质量上面的折衷,这些折衷也被称为Technical Debt(技术债)。没有任何项目没有技术债,只是多少,以及偿还的方式的区别。我们的目的不是消除技术债,而是通过不断持续改进代码质量,阻止技术债的滚雪球式积累。
C++是一门不容易用好的语言:错误的使用方式会给代码基的质量带来很大的损伤。而C++的误用方式又特别多。
输入法是个很特殊的应用程序,在Windows下面,输入法是加载到目标进程空间当中的dll,所以,输入法对质量的要求极高,别的软件出了错误崩溃了大不了重启一下,而输入法如果崩溃就会造成整个目标进程崩溃,如果用户的文档未保存就可能会丢失宝贵的用户数据,所以输入法最容不得崩溃。可是只要是人写的代码怎么可能没有bug呢?所以关键在于如何减少bug及其产生的影响和如何能尽快响应并修复bug。所以我们的做法分为三步:1). 使用现代C++技术减少bug产生的机会。2). 即便bug产生了,也尽量减少对用户产生的影响。3). 完善的bug汇报系统使开发人员能够第一时间拥有足够的信息修复bug。
至于为什么要用C++而不是C呢?对于我们来说理由很现实:时间紧任务重,用C的话需要发明的轮子太多了,C++的抽象层次高,代码量少,bug相对就会更少,现代C++的内存管理完全自动,以至于从头到尾我根本不记得曾遇到过什么内存管理相关的bug,现代C++的错误处理机制也非常适合快速开发的同时不用担心bug乱飞,另外有了C++11的强大支持更是如虎添翼,当然,这一切都必须建立在核心团队必须善用C++的大前提上,而这对于我们这个紧凑的小团队来说这不是问题,因为大家都有较好的C++背景,没有陡峭的学习曲线要爬。(至于C++在大规模团队中各人对C++的掌握良莠不齐的情况下所带来的一些包袱本文也不作讨论,呵呵,语言之争别找我。)


下面就说说我们在这个项目中是如何使用C++11和现代C++风格来开发的,什么是现代C++风格以及它给我们开发带来的好处。


资源管理


说到Native Languages就不得不说资源管理,因为资源管理向来都是Native Languages的一个大问题,其中内存管理又是资源当中的一个大问题,由于堆内存需要手动分配和释放,所以必须确保内存得到释放,对此一般原则是“谁分配谁负责释放”,但即便如此仍然还是经常会导致内存泄漏、野指针等等问题。更不用说这种手动释放给API设计带来的问题(例如Win32 API WideCharToMultiByte就是一个典型的例子,你需要提供一个缓冲区给它来接收编码转换的结果,但是你又不能确保你的缓冲区足够大,所以就出现了一个两次调用的pattern,第一次给个NULL缓冲区,于是API返回的是所需的缓冲区的大小,根据这个大小分配缓冲区之后再第二次调用它,别提多别扭了)。


托管语言们为了解决这个问题引入了GC,其理念是“内存管理太重要了,不能交给程序员来做”。但GC对于Native开发也常常有它自己的问题。而且另一方面Native界也常常诟病GC,说“内存管理太重要了,不能交给机器来做”。


C++也许是第一个提供了完美折衷的语言(不过这个机制直到C++11的出现才真正达到了易用的程度),即:既不是完全交给机器来做,也不是完全交给程序员来做,而是程序员先在代码中指定怎么做,至于什么时候做,如何确保一定会得到执行,则交由编译器来确定。


首先是C++98提供了语言机制:对象在超出作用域的时候其析构函数会被自动调用。接着,Bjarne Stroustrup在TC++PL里面定义了RAII(Resource Acquisition is Initialization)范式(即:对象构造的时候其所需的资源便应该在构造函数中初始化,而对象析构的时候则释放这些资源)。RAII意味着我们应该用类来封装和管理资源,对于内存管理而言,Boost第一个实现了工业强度的智能指针,如今智能指针(shared_ptr和unique_ptr)已经是C++11的一部分,简单来说有了智能指针意味着你的C++代码基中几乎就不应该出现delete了。


不过,RAII范式虽然很好,但还不足够易用,很多时候我们并不想为了一个CloseHandle, ReleaseDC, GlobalUnlock等等而去大张旗鼓地另写一个类出来,所以这些时候我们往往会因为怕麻烦而直接手动去调这些释放函数,手动调的一个坏处是,如果在资源申请和释放之间发生了异常,那么释放将不会发生,此外,手动释放需要在函数的所有出口处都去调释放函数,万一某天有人修改了代码,加了一处return,而在return之前忘了调释放函数,资源就泄露了。理想情况下我们希望语言能够支持这样的范式:


void foo()
{
    HANDLE h = CreateFile(...);


    ON_SCOPE_EXIT { CloseHandle(h); }


    ... // use the file
}
ON_SCOPE_EXIT里面的代码就像是在析构函数里面的一样:不管当前作用域以什么方式退出,都必然会被执行。


实际上,早在2000年,Andrei Alexandrescu 就在DDJ杂志上发表了一篇文章,提出了这个叫做ScopeGuard 的设施,不过当时C++还没有太好的语言机制来支持这个设施,所以Andrei动用了你所能想到的各种奇技淫巧硬是造了一个出来,后来Boost也加入了ScopeExit库,不过这些都是建立在C++98不完备的语言机制的情况下,所以其实现非常不必要的繁琐和不完美,实在是戴着脚镣跳舞(这也是C++98的通用库被诟病的一个重要原因),再后来Andrei不能忍了就把这个设施内置到了D语言当中,成了D语言特性的一部分(最出彩的部分之一)。


再后来就是C++11的发布了,C++11发布之后,很多人都开始重新实现这个对于异常安全来说极其重要的设施,不过绝大多数人的实现受到了2000年Andrei的原始文章的影响,多多少少还是有不必要的复杂性,而实际上,将C++11的Lambda Function和tr1::function结合起来,这个设施可以简化到脑残的地步:


class ScopeGuard
{
public:
    explicit ScopeGuard(std::function<void()> onExitScope)
        : onExitScope_(onExitScope), dismissed_(false)
    { }


    ~ScopeGuard()
    {
        if(!dismissed_)
        {
            onExitScope_();
        }
    }


    void Dismiss()
    {
        dismissed_ = true;
    }


private:
    std::function<void()> onExitScope_;
    bool dismissed_;


private: // noncopyable
    ScopeGuard(ScopeGuard const&);
    ScopeGuard& operator=(ScopeGuard const&);
};
这个类的使用很简单,你交给它一个std::function,它负责在析构的时候执行,绝大多数时候这个function就是lambda,例如:


HANDLE h = CreateFile(...);
ScopeGuard onExit([&] { CloseHandle(h); });
onExit在析构的时候会忠实地执行CloseHandle。为了避免给这个对象起名的麻烦(如果有多个变量,起名就麻烦大了),可以定义一个宏,把行号混入变量名当中,这样每次定义的ScopeGuard对象都是唯一命名的。


#define SCOPEGUARD_LINENAME_CAT(name, line) name##line
#define SCOPEGUARD_LINENAME(name, line) SCOPEGUARD_LINENAME_CAT(name, line)


#define ON_SCOPE_EXIT(callback) ScopeGuard SCOPEGUARD_LINENAME(EXIT, __LINE__)(callback)
Dismiss()函数也是Andrei的原始设计的一部分,其作用是为了支持rollback模式,例如:


ScopeGuard onFailureRollback([&] { /* rollback */ });
... // do something that could fail
onFailureRollback.Dismiss();
在上面的代码中,“do something”的过程中只要任何地方抛出了异常,rollback逻辑都会被执行。如果“do something”成功了,onFailureRollback.Dismiss()会被调用,设置dismissed_为true,阻止rollback逻辑的执行。


ScopeGuard是资源自动释放,以及在代码出错的情况下rollback的不可或缺的设施,C++98由于没有lambda和tr1::function的支持,ScopeGuard不但实现复杂,而且用起来非常麻烦,陷阱也很多,而C++11之后立即变得极其简单,从而真正变成了每天要用到的设施了。C++的RAII范式被认为是资源确定性释放的最佳范式(C#的using关键字在嵌套资源申请释放的情况下会层层缩进,相当的不能scale),而有了ON_SCOPE_EXIT之后,在C++里面申请释放资源就变得非常方便


Acquire Resource1
ON_SCOPE_EXIT( [&] { /* Release Resource1 */ })


Acquire Resource2
ON_SCOPE_EXIT( [&] { /* Release Resource2 */ })

这样做的好处不仅是代码不会出现无谓的缩进,而且资源申请和释放的代码在视觉上紧邻彼此,永远不会忘记。更不用说只需要在一个地方写释放的代码,下文无论发生什么错误,导致该作用域退出我们都不用担心资源不会被释放掉了。我相信这一范式很快就会成为所有C++代码分配和释放资源的标准方式,因为这是C++十年来的演化所积淀下来的真正好的部分之一。


错误处理


前面提到,输入法是一个特殊的东西,某种程度上他就跟用户态的driver一样,对错误的宽容度极低,出了错误之后可能造成很严重的后果:用户数据丢失。不像其他独立跑的程序可以随便崩溃大不了重启(或者程序自动重启),所以从一开始,错误处理就被非常严肃地对待。


这里就出现了一个两难问题:严谨的错误处理要求不要忽视和放过任何一个错误,要么当即处理,要么转发给调用者,层层往上传播。任何被忽视的错误,都迟早会在代码接下去的执行流当中引发其他错误,这种被原始错误引发的二阶三阶错误可能看上去跟root cause一点关系都没有,造成bugfix的成本剧增,这是我们项目快速的开发步调下所承受不起的成本。


然而另一方面,要想不忽视错误,就意味着我们需要勤勤恳恳地检查并转发错误,一个大规模的程序中随处都可能有错误发生,如果这种检查和转发的成本太高,例如错误处理的代码会导致代码增加,结构臃肿,那么程序员就会偷懒不检查。而一时的偷懒以后总是要还的。


所以细心检查是短期不断付出成本,疏忽检查则是长期付出成本,看上去怎么都是个成本。有没有既不需要短期付出成本,又不会导致长期付出成本的办法呢?答案是有的。我们的项目全面使用异常来作为错误处理的机制。异常相对于错误代码来说有很多优势,我曾经在2007年写过一篇博客《错误处理:为何、何时、如何》进行了详细的比较,但是异常对于C++而言也属于不容易用好的特性:


首先,为了保证当异常抛出的时候不会产生资源泄露,你必须用RAII范式封装所有资源。这在C++98中可以做到,但代价较大,一方面智能指针还没有进入标准库,另一方面智能指针也只能管内存,其他资源莫非还都得费劲去写一堆wrapper类,这个不便很大程度上也限制了异常在C++98下的被广泛使用。不过幸运的是,我们这个项目开始的时候VS2010 SP1已经具备了tr1和lambda function,所以写完上文那个简单的ScopeGuard之后,资源的自动释放问题就非常简便了。


其次,C++的异常不像C#的异常那样附带Callstack。例如你在某个地方通过.at(i)来取一个vector的某个元素,然后i越界了,你会收到vector内部抛出来的一个异常,这个异常只是说下标越界了,然后什么其他信息都木有,连个行号都没有。要是不抛异常直接让程序崩溃掉好歹还可以抓到一个minidump呢,这个因素一定程度上也限制了C++异常的被广泛使用。Callstack显然对于我们迅速诊断程序的bug有至关重要的作用,由于我们是一个不大的团队,所以我们对质量的测试很依赖于微软内部的dogfood用户,我们release给dogfood用户的是release版,倘若我们不用异常,用assert的话,固然是可以在release版也打开assert,但assert同样也只能提供很有限的信息(文件和行号,以及assert的表达式),很多时候这些信息是不足够理解一个bug的(更不用说还得手动截屏拷贝黏贴发送邮件才能汇报一个bug了),所以往往接下来还需要在开发人员自己的环境下试图重现bug。这就不够理想了。理想情况下,一个bug发生的时刻,程序应该自己具备收集一切必要的信息的能力。那么对于一个bug来说,有哪些信息是至关重要的呢?


Error Message本身,例如“您的下标越界啦!”少部分情况下,光是Error Message已经足够诊断。不过这往往是对于在开发的早期出现的一些简单bug,到中后期往往这类简单bug都被清除掉了,剩下的较为隐蔽的bug的诊断则需要多得多的信息。
Callstack。C++的异常由于性能的考虑,并不支持callstack。所以必须另想办法。
错误发生地点的上下文变量的值:例如越界访问,那么越界的下标的值是多少,而被越界的容器的大小又是多少,等等。例如解析一段xml失败了,那么这段xml是什么,当前解析到哪儿,等等。例如调用Win32 API失败了,那么Win32 Error Message是什么。
错误发生的环境:例如目标进程是什么。
错误发生之前用户做了什么:对于输入法来说,例如错误发生之前的若干个键敲击。
如果程序能够自动把这些信息收集并打包起来,发送给开发人员,那么就能够为诊断提供极大的帮助(当然,既便如此仍然还是会有难以诊断的bug)。而且这一切都要以不增加写代码过程中的开销的方式来进行,如果每次都要在代码里面做一堆事情来收集这些信息,那烦都得烦死人了,没有人会愿意用的。


那么到底如何才能无代价地尽量收集充足的信息为诊断bug提供帮助呢?


首先是callstack,有很多种方法可以给C++异常加上callstack,不过很多方法会带来性能损失,而且用起来也不方便,例如在每个函数的入口处加上一小段代码把函数名/文件/行号打印到某个地方,或者还有一些利用dbghelp.dll里面的StackWalk功能。我们使用的是没有性能损失的简单方案:在抛C++异常之前先手动MiniDumpWriteDump,在异常捕获端把minidump发回来,在开发人员收到minidump之后可以使用VS或windbg进行调试(但前提是相应的release版本必须开启pdb)。可能这里你会担心,minidump难道不是很耗时间的嘛?没错,但是既然程序已经发生了异常,稍微多花一点时间也就无所谓了。我们对于“附带minidump的异常”的使用原则是,只在那些真正“异常”的情况下抛出,换句话说,只在你认为应该使用的assert的地方用,这类错误属于critical error。另外我们还有不带minidump的异常,例如网络失败,xml解析失败等等“可以预见”的错误,这类错误发生的频率较高,所以如果每次都minidump会拖慢程序,所以这种情况下我们只抛异常不做minidump。


然后是Error Message,如何才能像assert那样,在Error Message里面包含表达式和文件行号?


最后,也是最重要的,如何能够把上下文相关变量的值capture下来,因为一方面release版本的minidump在调试的时候所看到的变量值未必正确,另一方面如果这个值在堆上(例如std::string的内部buffer就在堆上),那就更看不着了。


所有上面这些需求我们通过一个ENSURE宏来实现,它的使用很简单:


ENSURE(0 <= index && index < v.size())(index)(v.size());
ENSURE宏在release版本中同样生效,如果发现表达式求值失败,就会抛出一个C++异常,并会在异常的.what()里面记录类似如下的错误信息:


Failed: 0 <= index && index < v.size()
File: xxx.cpp Line: 123
Context Variables:
    index = 12345
    v.size() = 100
(如果你为stream重载了接收vector的operator <<,你甚至可以把vector的元素也打印到error message里头)


由于ENSURE抛出的是一个自定义异常类型ExceptionWithMinidump,这个异常有一个GetMinidumpPath()可以获得抛出异常的时候记录下来的minidump文件。


ENSURE宏还有一个很方便的feature:在debug版本下,抛异常之前它会先assert,而assert的错误消息正是上面这样。Debug版本assert的好处是可以让你有时间attach debugger,保证有完整的上下文。


利用ENSURE,所有对Win32 API的调用所发生的错误返回值就可以很方便地被转化为异常抛出来,例如:


ENSURE_WIN32(SHGetKnownFolderPath(rfid, 0, NULL, &p) == S_OK);
为了将LastError附在Error Message里面,我们额外定义了一个ENSURE_WIN32:


#define ENSURE_WIN32(exp) ENSURE(exp)(GetLastErrorStr())
其中GetLastErrorStr()会返回Win32 Last Error的错误消息文本。


而对于通过返回HRESULT来报错的一些Win32函数,我们又定义了ENSURE_SUCCEEDED(hr):


#define ENSURE_SUCCEEDED(hr) \
    if(SUCCEEDED(hr)) \
else ENSURE(SUCCEEDED(hr))(Win32ErrorMessage(hr))
其中Win32ErrorMessage(hr)负责根据hr查到其错误消息文本。


ENSURE宏使得我们开发过程中对错误的处理变得极其简单,任何地方你认为需要assert的,用ENSURE就行了,一行简单的ENSURE,把bug相关的三大重要信息全部记录在案,而且由于ENSURE是基于异常的,所以没有办法被程序忽略,也就不会导致难以调试的二阶三阶bug,此外异常不像错误代码需要手动去传递,也就不会带来为了错误处理而造成的额外的开发成本(用错误代码来处理错误的最大的开销就是错误代码的手工检查和层层传递)。


ENSURE宏的实现并不复杂,打印文件行号和表达式文本的办法和assert一样,创建minidump的办法(这里只讨论win32)是在__try中RaiseException(EXCEPTION_BREAKPOINT…),在__except中得到EXCEPTION_POINTERS之后调用MiniDumpWriteDump写dump文件。最tricky的部分是如何支持在后面capture任意多个局部变量(ENSURE(expr)(var1)(var2)(var3)…),并且对每个被capture的局部变量同时还得capture变量名(不仅是变量值)。而这个宏无限展开的技术也在大概十年前就有了,还是Andrei Alexandrescu写的一篇DDJ文章:Enhanced Assertions 。神奇的是,我的CSDN博客当年第一篇文章就是翻译的它,如今十年后又在自己的项目中用到,真是有穿越的感觉,而且穿越的还不止这一个,我们项目不用任何第三方库,包括boost也不用,这其实也没有带来什么不便,因为boost的大量有用的子库已经进入了TR1,唯一的不便就是C++被广为诟病的:没有一个好的event实现,boost.signal这种非常强大的工业级实现当然是可以的,不过对于我们的项目来说boost.signal的许多feature根本用不上,属于杀鸡用牛刀了,因此我就自己写了一个刚刚满足我们项目的特定需求的event实现(使用tr1::function和lambda,这个signal的实现和使用都很简洁,可惜variadic templates没有,不然还会更简洁一些)。我在03年写boost源码剖析系列的时候曾经详细剖析了boost.signal的实现技术,想不到十年前关注的技术十年后还会在项目中用到。


由于输入法对错误的容忍度较低,所以我们在所有的出口处都设置了两重栅栏,第一重catch所有的C++异常,如果是ExceptionWithMinidump类型,则发送带有dump的问题报告,如果是其他继承自std::exception的异常类型,则仅发送包含.what()消息的问题报告,最后如果是catch(…)收到的那就没办法了,只能发送“unknown exception occurred”这种消息回来了。


inline void ReportCxxException(std::exception_ptr ex_ptr)
{
    try
    {
        std::rethrow_exception(ex_ptr);
    }
    catch(ExceptionWithMiniDump& ex)
    {
        LaunchProblemReporter(…, ex.GetMiniDumpFilePath());
    }
    catch(std::exception& ex)
    {
        LaunchProblemReporter(…, ex.what());
    }
    catch(...)
    {
        LaunchProblemReporter("Unknown C++ Exception"));
    }
}
C++异常外面还加了一层负责捕获Win32异常的,捕获到unhandled win32 exception也会写minidump并发回。


考虑到输入法应该“能不崩溃就不崩溃”,所以对于C++异常而言,除了弹出问题报告程序之外,我们并不会阻止程序继续执行,这样做有以下几个原因:


很多时候C++异常并不会使得程序进入不可预测的状态,只要合理使用智能指针和ScopeGuard,该释放的该回滚的操作都能被正确执行。
输入法的引擎的每一个输入session(从开始输入到上词)理论上是独立的,如果session中间出现异常应该允许引擎被reset到一个可知的好的状态。
输入法内核中有核心模块也有非核心模块,引擎属于核心模块,云候选词、换肤、还有我们的创新feature:Rich Candidates(目前被译为多媒体输入,但其实没有准确表达出这个feature的含义,只不过第一批release的apps确实大多是输入多媒体的,但我们接下来会陆续更新一系列的Rich Candidates Apps就不止是多媒体了)也属于非核心模块,非核心模块即便出了错误也不应该影响内核的工作。因此对于这些模块而言我们都在其出口处设置了Error Boundary,捕获一切异常以免影响整个内核的运作。
另一方面,对于Native Language而言,除了语言级别的异常,总还会有Platform Specific的“硬”异常,例如最常见的Access Violation,当然这种异常越少越好(我们的代码基中鼓励使用ENSURE来检查各种pre-condition和post-condition,因为一般来说Access Violation不会是第一手错误,它们几乎总是由其他错误导致的,而这个“其他错误”往往可以用ENSURE来检查,从而在它导致Access Violation之前就抛出语言级别的异常。举一个简单的例子,还是vector的元素访问,我们可以直接v,如果i越界,会Access Violation,那么这个Access Violation便是由之前的第一手错误(i越界)所导致的二阶异常了。而如果我们在v之前先ENSURE(0 <= i && i < v.size())的话,就可以阻止“硬”异常的发生,转而成为汇报一个语言级别的异常,语言级别的异常跟平台相关的“硬”异常相比的好处在于:


语言级别异常的信息更丰富,你可以capture相关的变量的值放在异常的错误消息里面。
语言级别的异常是“同步”的,一个写的规范的程序可以保证在语言级别异常发生的情况下始终处于可知的状态。C++的Stack Unwind机制可以确保一切善后工作得到执行。相比之下当平台相关的“硬”异常发生的时候你既不会有机会清理资源回滚操作,也不能确保程序仍然处于可知的状态。所以语言级别的异常允许你在模块边界上设定Error Boundary并且在非核心模块失败的时候仍然保持程序运行,语言级别的异常也允许你在核心模块,例如引擎的出口设置Error Boundary,并且在出错的情况下reset引擎到一个干净的初始状态。简言之,语言级别的异常让程序更健壮。
理想情况下,我们应该、并且能够通过ENSURE来避免几乎所有“硬”异常的发生。但程序员也是人,只要是代码就会有疏忽,万一真的发生了“硬”异常怎么办?对于输入法而言,即便出现了这种很遗憾的情况我们仍然不希望你的宿主程序崩溃,但另一方面,由于“硬”异常使得程序已经处于不可知的状态,我们无法对程序以后的执行作出任何的保障,所以当我们的错误边界处捕获这类异常的时候,我们会设置一个全局的flag,disable整个的输入法内核,从用户的角度来看就是输入法不工作了,但一来宿主程序没有崩溃,二来你的所有键敲击都会被直接被宿主程序响应,就像没有打开输入法的时候一样。这样一来即便在最坏的情况之下,宿主程序仍然有机会去保存数据并体面退出。


所以,综上所述,通过基于C++异常的ENSURE宏,我们实现了以下几个目的:


极其廉价的错误检查和汇报(和assert一样廉价,却没有assert的诸多缺陷):尤其是对于快速开发来说,既不可忽视错误,又不想在错误汇报和处理这种(非正事)上消耗太多的时间,这种时候ENSURE是完美的方案。
丰富的错误信息。
不可忽视的错误:编译器会忠实负责stack unwind,不会让一个错误被藏着掖着,最后以二阶三阶错误的方式表现出来,给诊断造成麻烦。
健壮性:看上去到处抛异常会让人感觉程序不够健壮,而实际上恰恰相反,如果程序真的有bug,那么一定会浮现出来,即便你不用异常,也并没有消除错误本身,迟早错误会以其他形式表现出来,在程序的世界里,有错误是永远藏不住的。而异常作为语言级别支持的错误汇报和处理机制,拥有同步和自动清理的特点,支持模块边界的错误屏障,支持在错误发生的时候重置程序到干净的状态,从而最大限度保证程序的正常运行。如果不用异常而用error code,只要疏忽检查一点,迟早会导致“硬”异常,而一旦后者发生,基本剩下的也别指望程序还能正常工作了,能做得最负责任的事情就是别导致宿主崩溃。
另一方面,如果使用error code而不用异常来汇报和处理错误,当然也是可以达到上这些目的,但会给开发带来高昂的代价,设想你需要把每个函数的返回值腾出来用作HRESULT,然后在每个函数返回的时候必须check其返回错误,并且如果自己不处理必须勤勤恳恳地转发给上层。所以对于error code来说,要想快就必须牺牲周密的检查,要想周密的检查就必须牺牲编码时间来做“不相干”的事情(对于需要周密检查的错误敏感的应用来说,最后会搞到代码里面一眼望过去尽是各种if-else的返回值错误检查,而真正干活的代码却缩在不起眼的角落,看过win32代码的同学应该都会有这个体会)。而只有使用异常和ENSURE,才真正实现了既几乎不花任何额外时间、又不至于漏过任何一个第一手错误的目的。


最后简单提一下异常的性能问题,现代编译器对于异常处理的实现已经做到了在happy path上几乎没有开销,对于绝大多数应用层的程序来说,根本无需考虑异常所带来的可忽视的开销。在我们的对速度要求很敏感的输入法程序中,做performance profiling的时候根本看不到异常带来任何可见影响(除非你乱用异常,例如拿异常来取代正常的bool返回值,或者在loop里面抛接异常,等等)。具体的可以参考GoingNative2012@Channel9上的The Importance of Being Native的1小时06分处。


C++11的其他特性的运用


资源管理和错误处理是现代C++风格最醒目的标志,接下来再说一说C++11的其他特性在我们项目中的使用。


首先还是lambda,lambda除了配合ON_SCOPE_EXIT使用威力无穷之外,还有一个巨大的好处,就是创建on-the-fly的tasks,交给另一个线程去执行,或者创建一个delegate交给另一个类去调用(像C#的event那样)。(当然,lambda使得STL变得比原来易用十倍这个事情就不说了,相信大家都知道了),例如我们有一个BackgroundWorker类,这个类的对象在内部维护一个线程,这个线程在内部有一个message loop,不断以Thread Message的形式接收别人委托它执行的一段代码,如果是委托的同步执行的任务,那么委托(调用)方便等在那里,直到任务被执行完,如果执行过程中出现任何错误,会首先被BackgroundWorker捕获,然后在调用方线程上重新抛出(利用C++11的std::exception_ptr、std::current_exception()以及std::rethrow_exception())。BackgroundWorker的使用方式很简单:


bgWorker.Send([&]
{
.. /* do something */
});
有了lambda,不仅Send的使用方式像上面这样直观,Send本身的实现也变得很优雅:


bool Send(std::function<void()> action)
{
    HANDLE done = CreateEvent(NULL, TRUE, FALSE, NULL);
        
    std::exception_ptr  pCxxException;
    unsigned int        win32ExceptionCode = 0;
    EXCEPTION_POINTERS* win32ExceptionPointers = nullptr;


    std::function<void()> synchronousAction = [&]
    {
        ON_SCOPE_EXIT([&] {
            SetEvent(done);
        });


        AllExceptionsBoundary(
            action,
            [&](std::exception_ptr e)
                { pCxxException = e; },
            [&](unsigned int code, EXCEPTION_POINTERS* ep)
                { win32ExceptionCode = code;
                  win32ExceptionPointers = ep; });
    };


    bool r = Post(synchronousAction);


    if(r)
    {
        WaitForSingleObject(done, INFINITE);
        CloseHandle(done);


        // propagate error (if any) to the calling thread
        if(!(pCxxException == nullptr))
        {
            std::rethrow_exception(pCxxException);
        }


        if(win32ExceptionPointers)
        {
            RaiseException(win32ExceptionCode, ..);
        }
    }
    return r;
}
这里我们先把外面传进来的function wrap成一个新的lambda function,后者除了负责调用前者之外,还负责在调用完了之后flag一个event从而实现同步等待的目的,另外它还负责捕获任务执行中可能发生的错误并保存下来,留待后面在调用方线程上重新raise这个错误。


另外一个使用lambda的例子是:由于我们项目中需要解析XML的地方用的是MSXML,而MSXML很不幸是个COM组件,COM组件要求生存在特定的Apartment里面,而输入法由于是被动加载的dll,其主线程不是输入法本身创建的,所以主线程到底属于什么Apartment不由输入法来控制,为了确保万无一失,我们便将MSXML host在上文提到的一个专属的BackgroundWorker对象里面,由于BackgroundWorker内部会维护一个线程,这个线程的apartment是由我们全权控制的。为此我们给MSXML创建了一个wrapper类,这个类封装了这些实现细节,只提供一个简便的使用接口:


XMLDom dom;
dom.LoadXMLFile(xmlFilePath);


dom.Visit([&](std::wstring const& elemName, IXMLDOMNode* elem)
{
    if(elemHandlers.find(elemName) != elemHandlers.end())
    {
        elemHandlers[elemName](elem);
    }
});
基于上文提到的BackgroundWorker的辅助,这个wrapper类的实现也变得非常简单:


void Visit(TNodeVisitor const& visitor)
{
    bgWorker_.Send([&] {
        ENSURE(pXMLDom_ != NULL);
        
        IXMLDOMElement* root;
        ENSURE(pXMLDom_->get_documentElement(&root) == S_OK);


        InternalVisit(root, visitor);
    });
}
所有对MSXML对象的操作都会被Send到host线程上去执行。


另一个很有用的feature就是static_assert,例如我们在ENSURE宏的定义里面就有一行:


static_assert(std::is_same<decltype(expr), bool>::value, "ENSURE(expr) can only be used on bool expression");
避免调ENSURE(expr)的时候expr不是bool类型,确给隐式转换成了bool类型,从而出现很隐蔽的bug。


至于C++11的Move Semantics给代码带来的变化则是润物细无声的:你可以不用担心返回vector, string等STL容易的性能问题了,代码的可读性会得到提升。


最后,由于VS2010 SP1并没有实现全部的C++11语言特性,所以我们也并没有用上全部的特性,不过话说回来,已经被实现的特性已经相当有用了。


代码质量


在各种长期和短期压力之下写代码,当然代码质量是重中之重,尤其是对于C++代码,否则各种积累的技术债会越压越重。对于创新项目而言,代码基处于不停的演化当中,一开始的时候什么都不是,就是一个最简单的骨架,然后逐渐出现一点prototype的样子,随着不断的加进新的feature,再不断重构,抽取公共模块,形成concept和abstraction,isolate接口,拆分模块,最终prototype演变成product。关于代码质量的书很多,有一些写得很好,例如《The Art of Readable Code》,《Clean Code》或者《Implementation Patterns》。这里没有必要去重复这些书已经讲得非常好的技术,只说说我认为最重要的一些高层的指导性原则:


持续重构:避免代码质量无限滑坡的办法就是持续重构。持续重构是The Boy Scout Rule的一个推论。离开一段代码的时候永远保持它比上次看到的时候更干净。关于重构的书够多的了,细节的这里就不说了,值得注意的是,虽然重构有一些通用的手法,但具体怎么重构很多时候是一个领域相关的问题,取决于你在写什么应用,有些时候,重构就是重设计。例如我们的代码基当中曾经有一个tricky的设计,因为相当tricky,导致在后来的一次代码改动中产生了一个很隐蔽的regression,这使得我们重新思考这个设计的实现,并最终决定换成另一个(很遗憾仍然还是tricky的)实现,后者虽然仍然tricky(总会有不得已必须tricky的地方),但是却有一个好处:即便以后代码改动的过程中又涉及到了这块代码并且又导致了regression,那么至少所导致的regression将不再会是隐蔽的,而是会很明显。
KISS:KISS是个被说烂了的原则,不过由于”Simple”这个词的定义很主观,所以KISS并不是一个很具有实践指导意义的原则。我认为下面两个原则要远远有用得多: 1) YAGNI:You Ain’t Gonna Need It。不做不必要的实现,例如不做不必要的泛化,你的目的是写应用,不是写通用库。尤其是在C++里面,要想写通用库往往会触及到这门语言最黑暗的部分,是个时间黑洞,而且由于语言的不完善往往会导致不完备的实现,出现使用上的陷阱。2) 代码不应该是没有明显的bug,而应该是明显没有bug:这是一条很具有指导意义的原则,你的代码是否一眼看上去就明白什么意思,就确定没有bug?例如Haskell著名的quicksort就属于明显没有bug。为了达到这个目的,你的代码需要满足很多要求:良好的命名(传达意图),良好的抽象,良好的结构,简单的实现,等等。最后,KISS原则不仅适用于实现层面,在设计上KISS则更加重要,因为设计是决策的第一环,一个设计可能需要三四百行代码,而另一个设计可能只需要三四十行代码,我们就曾遇到过这样的情况。一个糟糕的设计不仅制造大量的代码和bug(代码当然是越少越好,代码越少bug就越少),成为后期维护的负担,侵入式的设计还会增加模块间的粘合度,导致被这个设计拖累的代码像滚雪球一样越来越多,所以code review之前更重要的还是要做design review,前面决策做错了后面会越错越离谱。
解耦原则:这个就不多说了,都说烂了。不过具体怎么解耦很多时候还是个领域相关的问题。虽然有些通用范式可循。
Best Practice Principle:对于C++开发来说尤其重要,因为在C++里面,同一件事情往往有很多不同的(但同样都有缺陷的)实现,而实现的成本往往还不低,所以C++社群多年以来一直在积淀所谓的Best Practices,其中的一个子集就是Idioms(惯用法),由于C++的学习曲线较为陡峭,闷头写一堆(有缺陷)的实现的成本很高,所以在一头扎进去之前先大概了解有哪些Idioms以及各自适用的场景就变得很有必要。站在别人的肩膀上好过自己掉坑里。
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 28楼 发表于: 2012-09-05
C++11新特性:右值引用和转移构造函数
问题背景
[cpp] view plaincopy
#include <iostream>  
  
using namespace std;  
  
vector<int> doubleValues (const vector<int>& v)  
{  
    vector<int> new_values( v.size() );  
    for (auto itr = new_values.begin(), end_itr = new_values.end(); itr != end_itr; ++itr )  
    {  
        new_values.push_back( 2 * *itr );  
    }  
    return new_values;  
}  
  
int main()  
{  
    vector<int> v;  
    for ( int i = 0; i < 100; i++ )  
    {  
        v.push_back( i );  
    }  
    v = doubleValues( v );  
}  


先来分析一下上述代码的运行过程。
[cpp] view plaincopy
vector<int> v;  
for ( int i = 0; i < 100; i++ )  
{  
    v.push_back( i );  
}  


以上5行语句在栈上新建了一个vector的实例,并在里面放了100个数。
[cpp] view plaincopy
v = doubleValues( v )  
这条语句调用函数doubleValues,函数的参数类型的const reference,常量引用,那么在实参形参结合的时候并不会将v复制一份,而是直接传递引用。所以在函数体内部使用的v就是刚才创建的那个vector的实例。
但是
[cpp] view plaincopy
vector<int> new_values( v.size() );  
这条语句新建了一个vector的实例new_values,并且复制了v的所有内容。但这是合理的,因为我们这是要将一个vector中所有的值翻倍,所以我们不应该改变原有的vector的内容。
[cpp] view plaincopy
v = doubleValues( v );  


函数执行完之后,new_values中放了翻倍之后的数值,作为函数的返回值返回。但是注意,这个时候doubleValue(v)的调用已经结束。开始执行 = 的语义。
赋值的过程实际上是将返回的vector<int>复制一份放入新的内存空间,然后改变v的地址,让v指向这篇内存空间。总的来说,我们刚才新建的那个vector又被复制了一遍。
但我们其实希望v能直接得到函数中复制好的那个vector。在C++11之前,我们只能通过传递指针来实现这个目的。但是指针用多了非常不爽。我们希望有更简单的方法。这就是我们为什么要引入右值引用和转移构造函数的原因。


左值和右值
在说明左值的定义之前,我们可以先看几个左值的例子。
[cpp] view plaincopy
int a;  
a = 1; // here, a is an lvalue  
上述的a就是一个左值。
临时变量可以做左值。同样函数的返回值也可以做左值。
[cpp] view plaincopy
int x;  
int& getRef ()  
{  
        return x;  
}  
  
getRef() = 4;  
以上就是函数返回值做左值的例子。


其实左值就是指一个拥有地址的表达式。换句话说,左值指向的是一个稳定的内存空间(即可以是在堆上由用户管理的内存空间,也可以是在栈上,离开了一个block就被销毁的内存空间)。上面第二个例子,getRef返回的就是一个全局变量(建立在堆上),所以可以当做左值使用。


与此相反,右值指向的不是一个稳定的内存空间,而是一个临时的空间。比如说下面的例子:
[cpp] view plaincopy
int x;  
int getVal ()  
{  
    return x;  
}  
getVal();  
这里getVal()得到的就是临时的一个值,没法对它进行赋值。
下面的语句就是错的。
[cpp] view plaincopy
getVal() = 1;//compilation error  
所以右值只能够用来给其他的左值赋值。


右值引用
在C++11中,你可以使用const的左值引用来绑定一个右值,比如说:
[cpp] view plaincopy
const int& val = getVal();//right  
int& val = getVal();//error  


因为左值引用并不是左值,并没有建立一片稳定的内存空间,所以如果不是const的话你就可以对它的内容进行修改,而右值又不能进行赋值,所以就会出错。因此只能用const的左值引用来绑定一个右值。


在C++11中,我们可以显示地使用“右值引用”来绑定一个右值,语法是"&&"。因为指定了是右值引用,所以无论是否const都是正确的。
[cpp] view plaincopy
const string&& name = getName(); // ok  
string&& name = getName(); // also ok  


有了这个功能,我们就可以对原来的左值引用的函数进行重载,重载的函数参数使用右值引用。比如下面这个例子:
[cpp] view plaincopy
printReference (const String& str)  
{  
        cout << str;  
}  
  
printReference (String&& str)  
{  
        cout << str;  
}  
可以这么调用它。
[cpp] view plaincopy
string me( "alex" );  
printReference(  me ); // 调用第一函数,参数为左值常量引用  
  
printReference( getName() ); 调用第二个函数,参数为右值引用。  


好了,现在我们知道C++11可以进行显示的右值引用了。但是我们如果用它来解决一开始那个复制的问题呢?
这就要引入与此相关的另一个新特性,转移构造函数和转移赋值运算符


转移构造函数和转移赋值运算符
假设我们定义了一个ArrayWrapper的类,这个类对数组进行了封装。
[cpp] view plaincopy
class ArrayWrapper  
{  
    public:  
        ArrayWrapper (int n)  
            : _p_vals( new int[ n ] )  
            , _size( n )  
        {}  
        // copy constructor  
        ArrayWrapper (const ArrayWrapper& other)  
            : _p_vals( new int[ other._size  ] )  
            , _size( other._size )  
        {  
            for ( int i = 0; i < _size; ++i )  
            {  
                _p_vals[ i ] = other._p_vals[ i ];  
            }  
        }  
        ~ArrayWrapper ()  
        {  
            delete [] _p_vals;  
        }  
    private:  
    int *_p_vals;  
    int _size;  
};  


我们可以看到,这个类的拷贝构造函数显示新建了一片内存空间,然后又对传进来的左值引用进行了复制。
如果传进来的实际参数是一个右值(马上就销毁),我们自然希望能够继续使用这个右值的空间,这样可以节省申请空间和复制的时间。
我们可以使用转移构造函数实现这个功能:
[cpp] view plaincopy
class ArrayWrapper  
{  
public:  
    // default constructor produces a moderately sized array  
    ArrayWrapper ()  
        : _p_vals( new int[ 64 ] )  
        , _size( 64 )  
    {}  
  
    ArrayWrapper (int n)  
        : _p_vals( new int[ n ] )  
        , _size( n )  
    {}  
  
    // move constructor  
    ArrayWrapper (ArrayWrapper&& other)  
        : _p_vals( other._p_vals  )  
        , _size( other._size )  
    {  
        other._p_vals = NULL;  
    }  
  
    // copy constructor  
    ArrayWrapper (const ArrayWrapper& other)  
        : _p_vals( new int[ other._size  ] )  
        , _size( other._size )  
    {  
        for ( int i = 0; i < _size; ++i )  
        {  
            _p_vals[ i ] = other._p_vals[ i ];  
        }  
    }  
    ~ArrayWrapper ()  
    {  
        delete [] _p_vals;  
    }  
  
private:  
    int *_p_vals;  
    int _size;  
};  


第一个构造函数就是转移构造函数。它先将other的域复制给自己。尤其是将_p_vals的指针赋值给自己的指针,这个过程相当于int的复制,所以非常快。然后将other里面_p_vals指针置成NULL。这样做有什么用呢?
我们看到,这个类的析构函数是这样的:
[cpp] view plaincopy
~ArrayWrapper ()  
    {  
        delete [] _p_vals;  
    }  
它会delete掉_p_vals的内存空间。但是如果调用析构函数的时候_p_vals指向的是NULL,那么就不会delte任何内存空间。
所以假设我们这样使用ArrayWrapper的转移构造函数:
[cpp] view plaincopy
ArrayWrapper *aw = new ArrayWrapper((new ArrayWrapper(5)));  
其中
[cpp] view plaincopy
(new ArrayWrapper(5)  
获得的实例就是一个右值,我们不妨称为r,当整条语句执行结束的时候就会被销毁,执行析构函数。
所以如果转移构造函数中没有
[cpp] view plaincopy
other._p_vals = NULL;  
的话,虽然aw已经获得了r的_p_vals的内存空间,但是之后r就被销毁了,那么r._p_vals的那片内存也被释放了,aw中的_p_vals指向的就是一个不合法的内存空间。所以我们就要防止这片空间被销毁。


右值引用也是左值


这种说法可能有点绕,来看一个例子:


我们可以定义MetaData类来抽象ArrayWrapper中的数据:
[cpp] view plaincopy
class MetaData  
{  
public:  
    MetaData (int size, const std::string& name)  
        : _name( name )  
        , _size( size )  
    {}  
  
    // copy constructor  
    MetaData (const MetaData& other)  
        : _name( other._name )  
        , _size( other._size )  
    {}  
  
    // move constructor  
    MetaData (MetaData&& other)  
        : _name( other._name )  
        , _size( other._size )  
    {}  
  
    std::string getName () const { return _name; }  
    int getSize () const { return _size; }  
    private:  
    std::string _name;  
    int _size;  
};  


那么ArrayWrapper类现在就变成这个样子
[cpp] view plaincopy
class ArrayWrapper  
{  
public:  
    // default constructor produces a moderately sized array  
    ArrayWrapper ()  
        : _p_vals( new int[ 64 ] )  
        , _metadata( 64, "ArrayWrapper" )  
    {}  
  
    ArrayWrapper (int n)  
        : _p_vals( new int[ n ] )  
        , _metadata( n, "ArrayWrapper" )  
    {}  
  
    // move constructor  
    ArrayWrapper (ArrayWrapper&& other)  
        : _p_vals( other._p_vals  )  
        , _metadata( other._metadata )  
    {  
        other._p_vals = NULL;  
    }  
  
    // copy constructor  
    ArrayWrapper (const ArrayWrapper& other)  
        : _p_vals( new int[ other._metadata.getSize() ] )  
        , _metadata( other._metadata )  
    {  
        for ( int i = 0; i < _metadata.getSize(); ++i )  
        {  
            _p_vals[ i ] = other._p_vals[ i ];  
        }  
    }  
    ~ArrayWrapper ()  
    {  
        delete [] _p_vals;  
    }  
private:  
    int *_p_vals;  
    MetaData _metadata;  
};  


同样,我们使用了转移构造函数来避免代码的复制。但是这里的转移构造函数对吗?
问题出在下面这条语句
[cpp] view plaincopy
_metadata( other._metadata )  
我们希望的是other._metadata是一个右值,然后就会调用MetaData类的转移构造函数来避免数据的复制。但是很可惜,右值引用是左值。
在前面已经说过,左值占用了内存上一片稳定的空间。而右值是一个临时的数据,离开了某条语句就会被销毁。other是一个右值引用,在ArrayWrapper类的转移构造函数的整个作用域中都可以稳定地存在,所以确实占用了内存上的稳定空间,所以是一个左值,因为上述语句调用的并非转移构造函数。所以C++标准库提供了如下函数来解决这个问题:
[cpp] view plaincopy
std::move  


这条语句可以将左值转换为右值


[cpp] view plaincopy
// 转移构造函数  
  ArrayWrapper (ArrayWrapper&& other)  
      : _p_vals( other._p_vals  )  
      , _metadata( std::move( other._metadata ) )  
  {  
      other._p_vals = NULL;  
  }  


这样就可以避免_metadata域的复制了。




函数返回右值引用


我们可以在函数中显示地返回一个右值引用


[cpp] view plaincopy
int x;  
  
int getInt ()  
{  
    return x;  
}  
  
int && getRvalueInt ()  
{  
    // notice that it's fine to move a primitive type--remember, std::move is just a cast  
    return std::move( x );  
}  
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 29楼 发表于: 2012-09-05
C++11新特性:自动类型推断和类型获取
转载请注明出处 http://blog.csdn.net/srzhz/article/details/7934483


自动类型推断
当编译器能够在一个变量的声明时候就推断出它的类型,那么你就能够用auto关键字来作为他们的类型:
[cpp] view plaincopy
auto x = 1;  


编译器当然知道x是integer类型的。所以你就不用int了。接触过泛型编程或者API编程的人大概可以猜出自动类型推断是做什么用的了:帮你省去大量冗长的类型声明语句。
比如下面这个例子:
在原来的C++中,你要想使用vector的迭代器得这么写:
[cpp] view plaincopy
vector<int> vec;  
vector<int>::iterator itr = vec.iterator();  
看起来就很不爽。现在你可以这么写了:
[cpp] view plaincopy
vector<int> vec;  
auto itr = vec.iterator();  
果断简洁多了吧。假如说自动类型推断只有这样的用法的话那未免也太naive了。在很多情况下它还能提供更深层次的便利。
比如说有这样的代码:
[cpp] view plaincopy
template <typename BuiltType, typename Builder>  
void  
makeAndProcessObject (const Builder& builder)  
{  
    BuiltType val = builder.makeObject();  
    // do stuff with val  
}  
这个函数的功能是要使用builder的makeObject产生的实例来进行某些操作。但是现在引入了泛型编程。builder的类型不同,那么makeObject返回的类型也不同,那么我们这里就得引入两个泛型。看起来很复杂是吧,所以这里就可以使用自动类型推断来简化操作:


[cpp] view plaincopy
template <typename Builder>  
void  
makeAndProcessObject (const Builder& builder)  
{  
    auto val = builder.makeObject();  
    // do stuff with val  
}  


因为在得之builder的类型之后,编译器就已经能知道makeObject的返回值类型了。所以我们能够让编译器自动去推断val的类型。这样一来就省去了一个泛型。
你以为自动类型推断只有这样的用法?那也太naive了。C++11还允许对函数的返回值进行类型推断


新的返回值语法和类型获取(Decltype)语句


在原来,我们声明一个函数都是这样的:
[cpp] view plaincopy
int temp(int a, double b);  


前面那个int是函数的返回值类型,temp是函数名,int a, double b是参数列表。
现在你可以将函数返回值类型移到到参数列表之后来定义:
[cpp] view plaincopy
auto temp(int a, double b) -> int;  


后置返回值类型可以有很多用处。比如有下列的类定义:
[cpp] view plaincopy
class Person  
{  
public:  
    enum PersonType { ADULT, CHILD, SENIOR };  
    void setPersonType (PersonType person_type);  
    PersonType getPersonType ();  
private:  
    PersonType _person_type;  
};  


那么在定义getPersonType函数的时候我们得这么写:
[cpp] view plaincopy
Person::PersonType Person::getPersonType ()  
{  
    return _person_type;  
}  


因为函数所在的类Person是声明在函数返回值之后的,所以在写返回值的时候编译器并不知道这个函数是在哪个类里面。由于PersonTYpe是Person类的内部声明的枚举,所以在看到PersonType的时候,编译器是找不到这个类型的。所以你就得在PersonTYpe前面加上Person::,告诉编译器这个类型是属于Person的。这看起来有点麻烦是吧。当你使用新的返回值语法的时候呢就可以这么写:
[cpp] view plaincopy
auto Person::getPersonType () -> PersonType  
{  
    return _person_type;  
}  


因为这次编译器看到返回值类型PersonType的时候已经知道这个函数属于类Person。所以它会到Person类中去找到这个枚举类型。


当然上述应用只能说是一个奇技淫巧而已。并没有帮我们多大的忙(代码甚至都没有变短)。所以还得引入C++11的另一个功能。


类型获取(Decltype)


既然编译器能够推断一个变量的类型,那么我们在代码中就应该能显示地获得一个变量的类型信息。所以C++介绍了另一个功能:decltype。(实在是不知道这个词该怎么翻译,姑且称之为类型获取)。


[cpp] view plaincopy
int x = 3;  
decltype(x) y = x; // same thing as auto y = x;  


上述代码就使用了类型获取功能。和函数返回值后置语法结合起来,可以有如下应用:
[cpp] view plaincopy
template <typename Builder>  
auto  
makeAndProcessObject (const Builder& builder) -> decltype( builder.makeObject() )  
{  
    auto val = builder.makeObject();  
    // do stuff with val  
    return val;  
}  


前面的例子中这个函数的返回值是void,所以不需要为返回值引入泛型。如果返回值是makeObject的返回值的话,那么这个函数就得引入两个泛型。现在又了类型获取功能,我们就能在返回值中自动推断makeObject的类型了。所以decltype确实为我们提供了很大的便利。


这个功能非常重要,在很多时候,尤其是引入了泛型编程的时候,你可能记不住一个变量的类型或者类型太过复杂以至于写不出来。你就要灵活使用decltype来获取这个变量的类型。


自动类型推断在Lambda表达式(匿名函数)中的作用
请参考http://blog.csdn.net/srzhz/article/details/7934652
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
描述
快速回复

您目前还是游客,请 登录注册
如果您提交过一次失败了,可以用”恢复数据”来恢复帖子内容