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

  • 27820阅读
  • 45回复

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

级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 30楼 发表于: 2012-09-05
C++11新特性:Lambda函数(匿名函数)
C++11终于知道要在语言中加入匿名函数了。匿名函数在很多时候可以为编码提供便利,这在下文会提到。很多语言中的匿名函数,如C++,都是用Lambda表达式实现的。Lambda表达式又称为lambda函数。我在下文中称之为Lambda函数。
为了明白Lambda函数的用处,请务必先搞明白C++中的自动类型推断:http://blog.csdn.net/srzhz/article/details/7934483


基本的Lambda函数


我们可以这样定义一个Lambda函数:


[cpp] view plaincopy
#include <iostream>  
  
using namespace std;  
  
int main()  
{  
    auto func = [] () { cout << "Hello world"; };  
    func(); // now call the function  
}  


其中func就是一个lambda函数。我们使用auto来自动获取func的类型,这个非常重要。定义好lambda函数之后,就可以当这场函数来使用了。
其中 [ ] 表示接下来开始定义lambda函数,中括号中间有可能还会填参数,这在后面介绍。之后的()填写的是lambda函数的参数列表{}中间就是函数体了。
正常情况下,只要函数体中所有return都是同一个类型的话,编译器就会自行判断函数的返回类型。也可以显示地指定lambda函数的返回类型。这个需要用到函数返回值后置的功能,比如这个例子:
[cpp] view plaincopy
[] () -> int { return 1; }  


所以总的来说lambda函数的形式就是:


[cpp] view plaincopy
[captures] (params) -> ret {Statments;}  


Lambda函数的用处


假设你设计了一个地址簿的类。现在你要提供函数查询这个地址簿,可能根据姓名查询,可能根据地址查询,还有可能两者结合。要是你为这些情况都写个函数,那么你一定就跪了。所以你应该提供一个接口,能方便地让用户自定义自己的查询方式。在这里可以使用lambda函数来实现这个功能。
[cpp] view plaincopy
#include <string>  
#include <vector>  
  
class AddressBook  
{  
    public:  
    // using a template allows us to ignore the differences between functors, function pointers  
    // and lambda  
    template<typename Func>  
    std::vector<std::string> findMatchingAddresses (Func func)  
    {  
        std::vector<std::string> results;  
        for ( auto itr = _addresses.begin(), end = _addresses.end(); itr != end; ++itr )  
        {  
            // call the function passed into findMatchingAddresses and see if it matches  
            if ( func( *itr ) )  
            {  
                results.push_back( *itr );  
            }  
        }  
        return results;  
    }  
  
    private:  
    std::vector<std::string> _addresses;  
};  


从上面代码可以看到,findMatchingAddressses函数提供的参数是Func类型,这是一个泛型类型。在使用过程中应该传入一个函数,然后分别对地址簿中每一个entry执行这个函数,如果返回值为真那么表明这个entry符合使用者的筛选要求,那么就应该放入结果当中。那么这个Func类型的参数如何传入呢?


[cpp] view plaincopy
AddressBook global_address_book;  
  
vector<string> findAddressesFromOrgs ()  
{  
    return global_address_book.findMatchingAddresses(  
        // we're declaring a lambda here; the [] signals the start  
        [] (const string& addr) { return addr.find( ".org" ) != string::npos; }  
    );  
}  


可以看到,我们在调用函数的时候直接定义了一个lambda函数。参数类型是
[cpp] view plaincopy
const string& addr  
返回值是bool类型。
如果用户要使用不同的方式查询的话,只要定义不同的lambda函数就可以了。


Lambda函数中的变量截取




在上述例子中,lambda函数使用的都是函数体的参数和它内部的信息,并没有使用外部信息。我们设想这样的一个场景,我们从键盘读入一个名字,然后用lambda函数定义一个匿名函数,在地址簿中查找有没有相同名字的人。那么这个lambda函数势必就要能使用外部block中的变量,所以我们就得使用变量截取功能(Variable Capture)。
[cpp] view plaincopy
// read in the name from a user, which we want to search  
string name;  
cin>> name;  
return global_address_book.findMatchingAddresses(  
    // notice that the lambda function uses the the variable 'name'  
    [&] (const string& addr) { return name.find( addr ) != string::npos; }  
);  
从上述代码看出,我们的lambda函数已经能使用外部作用域中的变量name了。这个lambda函数一个最大的区别是[]中间加入了&符号。这就告诉了编译器,要进行变量截取。这样lambda函数体就可以使用外部变量。如果不加入任何符号,编译器就不会进行变量截取。


下面是各种变量截取的选项:
[] 不截取任何变量
[&} 截取外部作用域中所有变量,并作为引用在函数体中使用
[=] 截取外部作用域中所有变量,并拷贝一份在函数体中使用
[=, &foo]   截取外部作用域中所有变量,并拷贝一份在函数体中使用,但是对foo变量使用引用
[bar]   截取bar变量并且拷贝一份在函数体重使用,同时不截取其他变量
[this]            截取当前类中的this指针。如果已经使用了&或者=就默认添加此选项。
Lambda函数和STL




lambda函数的引入为STL的使用提供了极大的方便。比如下面这个例子,当你想便利一个vector的时候,原来你得这么写:
[cpp] view plaincopy
vector<int> v;  
v.push_back( 1 );  
v.push_back( 2 );  
//...  
for ( auto itr = v.begin(), end = v.end(); itr != end; itr++ )  
{  
    cout << *itr;  
}  
现在有了lambda函数你就可以这么写
[cpp] view plaincopy
vector<int> v;  
v.push_back( 1 );  
v.push_back( 2 );  
//...  
for_each( v.begin(), v.end(), [] (int val)  
{  
    cout << val;  
} );  
而且这么写了之后执行效率反而提高了。因为编译器有可能使用”循环展开“来加速执行过程(计算机系统结构课程中学的)。
http://www.nwcpp.org/images/stories/lambda.pdf 这个PPT详细介绍了如何使用lambda表达式和STL
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 31楼 发表于: 2012-11-12
微软更新了 C++ 编译器,支持 C++11
微软更新了 Visual Studio 12 的 C++ 编译器。开发者将可以利用 C++11 的最新特性。


微软之前已经发布了编译器的 CTP(community technology preview) 版本,支持 C++ 的最新特性,包括 variadic templates, uniform initialization 和 initializer_lists,delegating constructors,raw string literals等等。


C++ 标准组织正在研究下一代 C++ 语言 C++17,预计在2017年公布。
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 32楼 发表于: 2012-12-26
C++11 标准新特性:Defaulted 和 Deleted 函数
简介:


本文将介绍 C++11 标准的两个新特性:defaulted 和 deleted 函数。对于 defaulted 函数,编译器会为其自动生成默认的函数定义体,从而获得更高的代码执行效率,也可免除程序员手动定义该函数的工作量。对于 deleted 函数, 编译器会对其禁用,从而避免某些非法的函数调用或者类型转换,从而提高代码的安全性。本文将通过代码示例详细阐述 defaulted 和 deleted 函数的用法及益处。


Defaulted 函数


背景问题


C++ 的类有四类特殊成员函数,它们分别是:默认构造函数、析构函数、拷贝构造函数以及拷贝赋值运算符。这些类的特殊成员函数负责创建、初始化、销毁,或者拷贝 类的对象。如果程序员没有显式地为一个类定义某个特殊成员函数,而又需要用到该特殊成员函数时,则编译器会隐式的为这个类生成一个默认的特殊成员函数。例 如:


清单 1
class X{
private:
  int a;
};


X x;


在清单 1 中,程序员并没有定义类X的默认构造函数,但是在创建类X的对象x的时候,又需要用到类X的默认构造函数,此时,编译器会隐式的为类X生成一个默认构造函数。该自动生成的默认构造函数没有参数,包含一个空的函数体,即X::X(){ }。虽然自动生成的默认构造函数仅有一个空函数体,但是它仍可用来成功创建类X的对象x,清单 1 也可以编译通过。


但是,如果程序员为类 X 显式的自定义了非默认构造函数,却没有定义默认构造函数的时候,清单 2 将会出现编译错误:




清单 2
class X{
public:
  X(int i){
    a = i;
  }    
private:
  int a;
};


X x;  // 错误 , 默认构造函数 X::X() 不存在


清单 2 编译出错的原因在于类X已经有了用户自定义的构造函数,所以编译器将不再会为它隐式的生成默认构造函数。如果需要用到默认构造函数来创建类的对象时,程序员必须自己显式的定义默认构造函数。例如:


清单 3
class X{
public:
  X(){};  // 手动定义默认构造函数
  X(int i){
    a = i;
  }    
private:
  int a;
};


X x;   // 正确,默认构造函数 X::X() 存在


从清单 3 可以看出,原本期望编译器自动生成的默认构造函数需要程序员手动编写了,即程序员的工作量加大了。此外,手动编写的默认构造函数的代码执行效率比编译器自 动生成的默认构造函数低。类的其它几类特殊成员函数也和默认构造函数一样,当存在用户自定义的特殊成员函数时,编译器将不会隐式的自动生成默认特殊成员函 数,而需要程序员手动编写,加大了程序员的工作量。类似的,手动编写的特殊成员函数的代码执行效率比编译器自动生成的特殊成员函数低。


Defaulted 函数的提出


为了解决如清单 3 所示的两个问题:1. 减轻程序员的编程工作量;2. 获得编译器自动生成的默认特殊成员函数的高的代码执行效率,C++11 标准引入了一个新特性:defaulted 函数。程序员只需在函数声明后加上“=default;”,就可将该函数声明为 defaulted 函数,编译器将为显式声明的 defaulted 函数自动生成函数体。例如:


清单 4
class X{
public:
  X()= default;
  X(int i){
    a = i;
  }    
private:
  int a;
};


X x;


在清单 4 中,编译器会自动生成默认构造函数X::X(){},该函数可以比用户自己定义的默认构造函数获得更高的代码效率。


Defaulted 函数定义语法


Defaulted 函数是 C++11 标准引入的函数定义新语法,defaulted 函数定义的语法如图 1 所示:


图 1. Defaulted 函数定义语法图

Defaulted 函数的用法及示例


Defaulted 函数特性仅适用于类的特殊成员函数,且该特殊成员函数没有默认参数。例如:


清单 5
class X {
public:
  int f() = default;      // 错误 , 函数 f() 非类 X 的特殊成员函数
  X(int) = default;       // 错误 , 构造函数 X(int, int) 非 X 的特殊成员函数
  X(int = 1) = default;   // 错误 , 默认构造函数 X(int=1) 含有默认参数
};


Defaulted 函数既可以在类体里(inline)定义,也可以在类体外(out-of-line)定义。例如:


清单 6
class X{
public:  
   X() = default; //Inline defaulted 默认构造函数
   X(const X&);
   X& operator = (const X&);
   ~X() = default;  //Inline defaulted 析构函数
};


X::X(const X&) = default;  //Out-of-line defaulted 拷贝构造函数
X& X::operator = (const X&) = default;     //Out-of-line defaulted  
     // 拷贝赋值操作符


在 C++ 代码编译过程中,如果程序员没有为类X定义析构函数,但是在销毁类X对象的时候又需要调用类X的析构函数时,编译器会自动隐式的为该类生成一个析构函数。该自动生成的析构函数没有参数,包含一个空的函数体,即X::~X(){ }。例如:


清单 7
class X {
private:
  int x;
};


class Y: public X {
private:
  int y;
};


int main(){
  X* x = new Y;
  delete x;
}


在清单 7 中,程序员没有为基类 X 和派生类 Y 定义析构函数,当在主函数内 delete 基类指针 x 的时候,需要调用基类的析构函数。于是,编译器会隐式自动的为类 X 生成一个析构函数,从而可以成功的销毁 x 指向的派生类对象中的基类子对象(即 int 型成员变量 x)。


但是,这段代码存在内存泄露的问题,当利用delete语句删除指向派生类对象的指针x时,系统调用的是基类的析构函数,而非派生类Y类的析构函数,因此,编译器无法析构派生类的int型成员变量 y。


因此,一般情况下我们需要将基类的析构函数定义为虚函数,当利用 delete 语句删除指向派生类对象的基类指针时,系统会调用相应的派生类的析构函数(实现多态性),从而避免内存泄露。但是编译器隐式自动生成的析构函数都是非虚函数,这就需要由程序员手动的为基类X定义虚析构函数,例如:


清单 8
class X {
public:
  virtual ~X(){};     // 手动定义虚析构函数
private:
  int x;
};


class Y: public X {
private:
  int y;
};


int main(){
  X* x = new Y;
  delete x;
  }


在清单 8 中,由于程序员手动为基类X定义了虚析构函数,当利用delete语句删除指向派生类对象的基类指针x时,系统会调用相应的派生类Y的析构函数(由编译器隐式自动生成)以及基类X的析构函数,从而将派生类对象完整的销毁,可以避免内存泄露。


但是,在清单 8 中,程序员需要手动的编写基类的虚构函数的定义(哪怕函数体是空的),增加了程序员的编程工作量。更值得一提的是,手动定义的析构函数的代码执行效率要低于编译器自动生成的析构函数。


为了解决上述问题,我们可以将基类的虚析构函数声明为 defaulted 函数,这样就可以显式的指定编译器为该函数自动生成函数体。例如:


清单 9
class X {
public:
  virtual ~X()= defaulted; // 编译器自动生成 defaulted 函数定义体
private:
  int x;
};


class Y: public X {
private:
  int y;
};


int main(){
  X* x = new Y;
  delete x;


}


在清单 9 中,编译器会自动生成虚析构函数virtual X::X(){},该函数比用户自己定义的虚析构函数具有更高的代码执行效率。


Deleted 函数


背景问题


对于 C++ 的类,如果程序员没有为其定义特殊成员函数,那么在需要用到某个特殊成员函数的时候,编译器会隐式的自动生成一个默认的特殊成员函数,比如拷贝构造函数,或者拷贝赋值操作符。例如:


清单 10
class X{
public:
  X();
};


int main(){
  X x1;
  X x2=x1;   // 正确,调用编译器隐式生成的默认拷贝构造函数
  X x3;
  x3=x1;     // 正确,调用编译器隐式生成的默认拷贝赋值操作符
}


在清单 10 中,程序员不需要自己手动编写拷贝构造函数以及拷贝赋值操作符,依靠编译器自动生成的默认拷贝构造函数以及拷贝赋值操作符就可以实现类对象的拷贝和赋值。 这在某些情况下是非常方便省事的,但是在某些情况下,假设我们不允许发生类对象之间的拷贝和赋值,可是又无法阻止编译器隐式自动生成默认的拷贝构造函数以 及拷贝赋值操作符,那这就成为一个问题了。


Deleted 函数的提出


为了能够让程序员显式的禁用某个函数,C++11 标准引入了一个新特性:deleted 函数。程序员只需在函数声明后加上“=delete;”,就可将该函数禁用。例如,我们可以将类X的拷贝构造函数以及拷贝赋值操作符声明为 deleted 函数,就可以禁止类X对象之间的拷贝和赋值。


清单 11
class X{            
     public:
       X();
       X(const X&) = delete;  // 声明拷贝构造函数为 deleted 函数
       X& operator = (const X &) = delete; // 声明拷贝赋值操作符为 deleted 函数
     };


int main(){
  X x1;
  X x2=x1;   // 错误,拷贝构造函数被禁用
  X x3;
  x3=x1;     // 错误,拷贝赋值操作符被禁用
}


在清单 11 中,虽然只显式的禁用了一个拷贝构造函数和一个拷贝赋值操作符,但是由于编译器检测到类X存在用户自定义的拷贝构造函数和拷贝赋值操作符的声明,所以不会再隐式的生成其它参数类型的拷贝构造函数或拷贝赋值操作符,也就相当于类X没有任何拷贝构造函数和拷贝赋值操作符,所以对象间的拷贝和赋值被完全禁止了。


Deleted 函数定义语法


Deleted 函数是 C++11 标准引入的函数定义新语法,deleted 函数定义的语法如图 2 所示:


图 2. Deleted 函数定义语法图

Deleted 函数的用法及示例


Deleted 函数特性还可用于禁用类的某些转换构造函数,从而避免不期望的类型转换。在清单 12 中,假设类X只支持参数为双精度浮点数 double 类型的转换构造函数,而不支持参数为整数 int 类型的转换构造函数,则可以将参数为 int 类型的转换构造函数声明为 deleted 函数。


清单 12
class X{
public:
  X(double);              
  X(int) = delete;    
};


int main(){
  X x1(1.2);        
  X x2(2); // 错误,参数为整数 int 类型的转换构造函数被禁用          
}


Deleted 函数特性还可以用来禁用某些用户自定义的类的new操作符,从而避免在自由存储区创建类的对象。例如:


清单 13
#include <cstddef>
using namespace std;


class X{
public:
  void *operator new(size_t) = delete;
  void *operator new[](size_t) = delete;
};


int main(){
  X *pa = new X;  // 错误,new 操作符被禁用
  X *pb = new X[10];  // 错误,new[] 操作符被禁用
}


必须在函数第一次声明的时候将其声明为 deleted 函数,否则编译器会报错。即对于类的成员函数而言,deleted 函数必须在类体里(inline)定义,而不能在类体外(out-of-line)定义。例如:


清单 14
class X {
public:  
  X(const X&);
};


X::X(const X&) = delete;   // 错误,deleted 函数必须在函数第一次声明处声明


虽然 defaulted 函数特性规定了只有类的特殊成员函数才能被声明为 defaulted 函数,但是 deleted 函数特性并没有此限制。非类的成员函数,即普通函数也可以被声明为 deleted 函数。例如:


清单 15
int add (int,int)=delete;


int main(){
    int a, b;
    add(a,b); // 错误,函数 add(int, int) 被禁用
     }


值得一提的是,在清单 15 中,虽然add(int, int)函数被禁用了,但是禁用的仅是函数的定义,即该函数不能被调用。但是函数标示符add仍是有效的,在名字查找和函数重载解析时仍会查找到该函数标示符。如果编译器在解析重载函数时,解析结果为 deleted 函数,则会出现编译错误。例如:


清单 16
#include <iostream>  
using namespace std;  
int add(int,int) = delete;    
  double add(double a,double b){
  return a+b;
}  
int main(){  
  cout << add(1,3) << endl;    // 错误,调用了 deleted 函数 add(int, int)
  cout << add(1.2,1.3) << endl;
  return 0;
}


结束语


本文详细介绍了 C++11 新特性 defaulted 和 deleted 函数。该特性巧妙地对 C++ 已有的关键字 default 和 delete 的语法进行了扩充,引入了两种新的函数定义方式:在函数声明后加 =default 和 =delete。通过将类的特殊成员函数声明为 defaulted 函数,可以显式指定编译器为该函数自动生成默认函数体。通过将函数声明为 deleted 函数,可以禁用某些不期望的转换或者操作符。Defaulted 和 deleted 函数特性语法简单,功能实用,是对 C++ 标准的一个非常有价值的扩充。


原文出处:IBM developerWorks
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 33楼 发表于: 2012-12-26
右值引用(或“move语意与完美转发”)
《C++0x漫谈》系列导言

这个系列其实早就想写了,断断续续关注C++0x也大约有两年余了,其间看着各个重要proposals一路review过来:rvalue-references、concepts、memory-model、variadic-templates、template-aliases、auto/decltype、GC、initializer-lists…

总的来说C++09跟C++98相比的变化是极其重大的。这个变化体现在三个方面,一个是形式上的变化,即在编码形式层面的支持,也就是对应我们所谓的编程范式(paradigm)。C++09不会引入新的编程范式,但在对泛型编程(GP)这个范式的支持上会得到质的提高:concepts、variadic-templates、auto/decltype、template-aliases、initializer-lists皆属于这类特性。另一个是内在的变化,即并非代码组织表达方面的,memory-model、GC属于这一类。最后一个是既有形式又有内在的,r-value references属于这类。

这个系列如果能够写下去,会陆续将C++09的新特性介绍出来。鉴于已经有许多牛人写了很多很好的tutor(这里,这里,还有C++标准主页上的一些introductive的proposals,如这里,此外C++社群中老当益壮的Lawrence Crowl也在google做了非常漂亮的talk)。所以我就不作重复劳动了:),我会尽量从一个宏观的层面,如特性引入的动机,特性引入过程中经历的修改,特性本身的最具代表性的使用场景,特性对编程范式的影响等方面进行介绍。至于细节,大家可以见每篇介绍末尾的延伸阅读。


右值引用导言

右值引用(及其支持的Move语意和完美转发)是C++0x将要加入的最重大语言特性之一,这点从该特性的提案在C++ - State of the Evolution列表上高居榜首也可以看得出来。从实践角度讲,它能够完美解决C++中长久以来为人所诟病的临时对象效率问题。从语言本身讲,它健全了C++中的引用类型在左值右值方面的缺陷。从库设计者的角度讲,它给库设计者又带来了一把利器。从库使用者的角度讲,不动一兵一卒便可以获得“免费的”效率提升…

Move语意

返回值效率问题——返回值优化((N)RVO)——mojo设施——workaround——问题定义——Move语意——语言支持

大猴子Howard Hinnant写了一篇挺棒的tutorial(a.k.a. 提案N2027),此外最初的关于rvalue-reference的若干篇提案的可读性也相当强。因此要想了解rvalue-reference的话,或者去看C++标准委员会网站上的系列提案(见文章末尾的参考文献)。或者阅读本文。

源起
《大史记》总看过吧?

故事,素介个样子滴…一天,小嗖风风的吹着,在一个伸手不见黑夜的五指…(哎哟,谁人扔滴板砖?!%$@)

我用const引用来接受参数,却把临时变量一并吞掉了。我用非const引用来接受参数,却把const左值落下了。于是乎,我就在标准的每个角落寻找解决方案,我靠!我被8.5.3打败了!…

设想这样一段代码(既然大同小异,就直接从Andrei那篇著名的文章里面拿来了):

std::vector<int> v = readFile();

readFile()的定义是这样的:

std::vector<int> readFile()
{
  std::vector<int> retv;
  … // fill retv
  return retv;
}

这段代码低效的地方在于那个返回的临时对象。一整个vector得被拷贝一遍,仅仅是为了传递其中的一组int,当v被构造完毕之后,这个临时对象便烟消云散。

这完全是公然的浪费!

更糟糕的是,原则上讲,这里有两份浪费。一,retv(retv在readFile()结束之后便烟消云散)。二,返回的临时对象(返回的临时变量在v拷贝构造完毕之后也随即香消玉殒)。不过呢,对于上面的简单代码来说,大部分编译器都已经能够做到优化掉这两个对象,直接把那个retv创建到接受返回值的对象,即v中去。

实际上,临时对象的效率问题一直是C++中的一个被广为诟病的问题。这个问题是如此的著名,以至于标准不惜牺牲原本简洁的拷贝语意,在标准的12.8节悍然下诏允许优化掉在函数返回过程中产生的拷贝(即便那个拷贝构造函数有副作用也在所不惜!)。这就是所谓的“Copy Elision”。

为什么(N)RVO((Named) Return Value Optimization)几乎形同虚设
还是按照Andrei的说法,只要readFile()改成这样:

… readFile()
{
if(/* err condition */) return std::vector<int>();
if(/* yet another err condition */) return std::vector<int>(1, 0);
std::vector<int> retv;
… // fill retv
return retv;
}

出现这种情况,编译器一般都会乖乖放弃优化。

但对编译器来说这还不是最郁闷的一种情况,最郁闷的是:

std::vector<int> v;
v = readFile(); // assignment, not copy construction

这下由拷贝构造,变成了拷贝赋值。眼睛一眨,老母鸡变鸭。编译器只能缴械投降。因为标准只允许在拷贝构造的情况下进行(N)RVO。

为什么库方案也不是生意经
C++鬼才Andrei Alexandrescu以对C++标准的深度挖掘和利用著名,早在03年的时候(当时所谓的临时变量效率问题已经在新闻组上闹了好一阵子了,相关的语言级别的解决方案也已经在02年9月份粉墨登场)就在现有标准(C++98)下硬是折腾出了一个能100%解决问题的方案来。

Andrei把这个框架叫做mojo,就像一层爽身粉一样,把它往现有类上面一洒,嘿嘿…猜怎么着,不,不是“痱子去无踪”:P,是该类型的临时对象效率问题就迎刃而解了!

Mojo的唯一的问题就是使用方法过于复杂。这个复杂度,很大程度上来源于标准中的一个措辞问题(C++标准就是这样,鬼知道哪个角落的一句话能够带出一个brilliant的解决方案来,同时,鬼知道哪个角落的一句话能够抹杀一个原本简洁的解决方案)。这个问题就是我前面提到过的8.5.3问题,目前已经由core language issue 391解决。

对于库方案来说,解决问题固然是首要的。但一个侵入性的,外带使用复杂性的方案必然是走不远的。因此虽然大家都不否认mojo是一个天才的方案,但实际使用中难免举步维艰。这也是为什么mojo并没有被工业化的原因。

为什么改用引用传参也等于痴人说梦
void readFile(vector<int>& v){ … // fill v }

这当然可以。

但是如果遇到操作符重载呢?

string operator+(string const& s1, string const& s2);

而且,就算是对于readFile,原先的返回vector的版本支持

BOOST_FOREACH(int i, readFile()){
  … // do sth. with i
}

改成引用传参后,原本优雅的形式被破坏了,为了进行以上操作不得不引入一个新的名字,这个名字的存在只是为了应付被破坏的形式,一旦foreach操作结束它短暂的生命也随之结束:

vector<int> v;
readFile(v);

BOOST_FOREACH(int I, v){
}

// v becomes useless here

还有什么问题吗?自己去发现吧。总之,利用引用传参是一个解决方案,但其能力有限,而且,其自身也会带来一些其它问题。终究不是一个优雅的办法。

问题是什么
《你的灯亮着吗?》里面漂亮地阐述了定义“问题是什么”的重要性。对于我们面临的临时对象的效率问题,这个问题同样重要。

简而言之,问题可以描述为:

C++没有区分copy和move语意。

什么是move语意?记得auto_ptr吗?auto_ptr在“拷贝”的时候其实并非严格意义上的拷贝。“拷贝”是要保留源对象不变,并基于它复制出一个新的对象出来。但auto_ptr的“拷贝”却会将源对象“掏空”,只留一个空壳——一次资源所有权的转移。

这就是move。

Move语意的作用——效率优化
举个具体的例子,std::string的拷贝构造函数会做两件事情:一,根据源std::string对象的大小分配一段大小适当的缓冲区。二,将源std::string中的字符串拷贝过来。

// just for illustrating the idea, not the actual implementation
string::string(const string& o)
{
this->buffer_ = new buffer[o.length() + 1];
copy(o.begin(), o.end(), buffer_);
}

但是假设我们知道o是一个临时对象(比如是一个函数的返回值),即o不会再被其它地方用到,o的生命期会在它所处的full expression的结尾结束的话,我们便可以将o里面的资源偷过来:

string::string(temporary string& o)
{
// since o is a temporary, we can safely steal its resources without causing any problem

this->buffer_ = o.buffer_;
o.buffer_ = 0;
}

这里的temporary是一个捏造的关键字,其作用是使该构造函数区分出临时对象(即只有当参数是一个临时的string对象时,该构造函数才被调用)。

想想看,如果存在这样一个move constructor(搬移式构造函数)的话,所有源对象为临时对象的拷贝构造行为都可以简化为搬移式(move)构造。对于上面的string例子来说,move和copy construction之间的效率差是节省了一次O(n)的分配操作,一次O(n)的拷贝操作,一次O(1)的析构操作(被拷贝的那个临时对象的析构)。这里的效率提升是显而易见且显著的。

最后,要实现这一点,只需要我们具有判断左值右值的能力(比如前面设想的那个temporary关键字),从而针对源对象为临时对象的情况进行“偷”资源的行动。

Move语意的作用——使能(enabling)
再举一个例子,std::fstream。fstream是不可拷贝的(实际上,所有的标准流对象都是不可拷贝的),因而我们只能通过引用来访问一开始建立的那个流对象。但是,这种办法有一个问题,如果我们要从一个函数中返回一个流对象出来就不行了:

// how do we make this happen?
std::fstream createStream()
{ … }

当然,你可以用auto_ptr来解决这个问题,但这就使代码非常笨拙且难以维护。

但如果fstream是moveable的,以上代码就是可行的了。所谓“moveable”即是指(当源对象是临时对象时)在对象拷贝语法之下进行的实际动作是像auto_ptr那样的资源所有权转移:源对象被掏空,所有资源都被转移到目标对象中——好比一次搬家(move)。move操作之后,源对象虽然还有名有姓地存在着,但实际上其“实质”(内部拥有的资源)已经消失了,或者说,源对象从语意上已经消失了。

对于moveable但并非copyable的fstream对象来说,当发生一次move时(比如在上面的代码中,当一个局部的fstream对象被move出createStream()函数时),不会出现同一对象的两个副本,取而代之的是,move的源对象的身份(Identity)消失了,这个身份由返回的临时fstream对象重新持有。也就是说,fstream的唯一性(不可拷贝性——non-copyable)得到了尊重。

你可能会问,那么被搬空了的那个源对象如果再被使用的话岂不是会引发问题?没错。这就是为什么我们应该仅当需要且可以去move一个对象的时候去move它,比如在函数的最后一行(return)语句中将一个局部的vector对象move出来(return std::move(v)),由于这是最后一行语句,所以后面v不可能再被用到,对它来说所剩下的操作就是析构,因此被掏空从语意上是完全恰当的。

最初的例子——完美解决方案
在先前的那个例子中

vector<int> v = readFile();

有了move语意的话,readFile就可以简单的改成:

std::vector<int> readFile()
{
std::vector<int> retv;
… // fill retv
return std::move(retv); // move retv out
}

std::move以后再介绍。目前你只要知道,std::move就可以把retv掏空,即搬移出去,而搬家的最终目的地是v。这样的话,从内存分配的角度讲,只有retv中进行的内存分配,在从retv到返回的临时对象,再从后者到目的地v的“move”过程中,没有任何的内存分配(我是指vector内的缓冲区分配),取而代之的是,先是retv内的缓冲区被“转移”到返回值临时对象中,然后再从临时对象中转移到v中。相比于以前的两次拷贝而言,两次move操作节省了多少工作量呢?节省了两次new操作两次delete操作,还有两次O(n)的拷贝操作,这些操作整体的代价正比于retv这个vector的大小。难怪人们说临时对象效率问题是C++的肿瘤(wart)之一,难怪C++标准都要不惜代价允许(N)RVO。

如何支持move语意
根据前面的介绍,你想必已经知道。实现move语意的最关键环节在于能够在编译期区分左值右值(也就是说识别出临时对象)。

现在,回忆一下,在文章的开头我曾经提到:

我用const引用来接受参数,却把临时变量一并吞掉了。我用非const引用来接受参数,却把const左值落下了。于是乎,我就在标准的每个角落寻找解决方案,我靠!我被8.5.3打败了!…

为什么这么说?

现行标准(C++03)下的方案
要想区分左值右值,只有通过重载:

void foo(X const&);
void foo(X&);

这样的重载显然是行不通的。因为X const&会把non-const临时对象一并吞掉。

这种做法的问题在于。X&是一个non-const引用,它只能接受non-const左值。然而,C++里面的值一共有四种组合:

        const    non-const
lvalue
rvalue

常量性(const-ness)与左值性(lvalue-ness)是正交的。

non-const引用只能绑定到其中的一个组合,即non-const lvalue。还剩下const左值,const右值,以及我们最关心的——non-const右值。而只有最后一种——non-const右值——才是可以move的。

剩下的问题便是如何设计重载函数来搞定const左值和const右值。使得最后只留下non-const右值。

所幸的是,我们可以借助强大的模板参数推导机制:

// catch non-const lvalues
void foo(X&);

// catch const lvalues and const rvalues
template<typename T>
void foo(T&, enable_if_same<T, const X>::type* = 0);

void foo( /* what goes here? */);

注意,第二个重载负责接受const左值和const右值。经过第一第二个foo重载之后剩下来的便是non-const rvalue了。

问题是,我们怎么捕获这些non-const rvalue呢?根据C++03,const-const rvalue只能绑定到const引用。但如果我们用const引用的话,就会越俎代庖把const左右值一并接受了(因为在模板函数(第二个重载)和非模板函数(第三个重载)之间编译器总是会偏好非模板)。

那除了用const引用,难道还有什么办法来接受一个non-const rvalue吗?

有。

假设你的类型为X,那么只要在X里面加入一点料:

struct ref_x
{
ref_x(X* p) : p_(p) {}
X* p_;
};

struct X
{
// original stuff


// added stuff, for move semantic
operator ref_x()
{
return ref_x(this);
}
};

这样,我们的第三个重载函数便可以写成:

void foo(ref_x rx); // accept non-const temporaries only!

Bang! 我们成功地在C++03下识别出了moveable的non-const临时对象。不过前提是必须得在moveable的类型里加入一些东西。这也正是该方案的最大弊病——它是侵入式的(姑且不说它利用了语言的阴暗角落,并且带来了很大的编码复杂度)。

C++09的方案
实际上,刚才讲的这个利用重载的方案做成库便是Andrei的mojo框架。mojo框架固然精巧,但复杂性太大,使用成本太高,不够优雅直观。所以语言级别的支持看来是必然选择(后面你还会看到,为了支持move语意而引入的新的语言特性同时还支持了另一个广泛的问题——完美转发)。

C++03之所以让人费神就是因为它没有一个引用类型来绑定到右值,而是用const左值引用来替代,事实证明这个权宜之计并不是长远之道,时隔10年,终归还是要健全引用的左右值语意。

C++09加入一个新的引用类型——右值引用。右值引用的特点是优先绑定到右值。其语法是&&(注意,不读作“引用的引用”,读作“右值引用”)。有了右值引用,我们前面的方案便可以简单的修改为:

void foo(X const& x);
void foo(X&& x);

这样一来,左值以及const右值都被绑定到了第一个重载版本。剩下的non-const右值被绑定到第二个重载版本。

对于你的moveable的类型X,则是这样:

struct X
{
X();
X(X const& o); // copy constructor
X(X&& o); // move constructor
};

X source();

X x = source(); // #1

在#1处,调用的将会是X::X(X&& o),即所谓的move constructor,因为source()返回的是一个临时对象(non-const右值),重载决议会选中move constructor。

扩展阅读
由于本文的意图是一个指南,因此关于move语意的其它林林种种的细节可参见下面列的参考文献。
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 34楼 发表于: 2012-12-26
右值引用(或“move语意与完美转发”)
完美转发

完美转发问题——不完美解决方案——模板参数推导规则——完美转发

动机
关于“完美转发”这个特性,其实提案N1385已经讲得非常清楚了,诸位可以直接去看N1385,如果实在还是觉得迷糊就再回来听我唠叨吧:-)

在泛型编码中经常出现的一个问题是(这个问题在实际中出现的场景很多,我们留到文章末尾再提,目前我们将这个特定的问题先提取孤立出来考虑):

如何将一组参数原封不动地转发给另一个函数

注意,这里所谓“原封不动”就是指,如果参数是左值,那么转发给的那个函数也要接受到一个左值,如果参数是右值,那么后者要接受到一个右值;同理,如果参数是const的,那么转发给的那个函数也要接受到一个const的值,如果是non-const的,那么后者也要接受到一个non-const的值。

总之一句话:

保持参数的左值/右值、const/non-const属性不变

听上去很简单吗?不妨试试看。

(不完美的)解决方案
假设我们要写一个泛型转发函数f,f要将它的参数原封不动地转发给g(不管g的参数类型是什么):

template<typename T>
void f(/*what goes here?*/ t)
{
g(t);
}

上面的代码中,f的参数t的类型是什么?T?T&?const T&?

我们一个个来分析。

Value
如果t的类型是T,即:

// take #1
template<typename T>
void f(T t)
{
g(t);
}

那么很显然,不能满足如下情况:

void g(int& i) { ++i; }

int myInt = 0;

f(myInt); // error, the value g() has incremented is a local value(a.k.a. f’s argument ‘t’)

即,不能将左值转发为左值。

Const&
如果t的类型为const T&,即:

// take #2
template<typename T>
void f(const T& t)
{
g(t);
}

则刚才的情况还是不能满足。因为g接受的参数类型为non-const引用。

Non-const&
那如果t的类型是T&呢?

// take #3
template<typename T>
void f(T& t)
{
g(t);
}

我们惊讶地发现,这时,如果参数是左值,那么不管是const左值还是non-const左值,f都能正确转发,因为对于const左值,T将会被推导为const U(U为参数的实际类型)。并且,对于const右值,f也能正确转发(因为const引用能够绑定到右值)。只有对non-const右值不能完美转发(因为这时T&会被推导为non-const引用,而后者不能绑定到右值)。

即四种情况里面有三种满足了,只有以下这种情况失败:

void g(const int& i);

int source();

f(source()); // error

如果f是完美转发的话,那么f(source())应该完全等价于g(source()),后者应该通过编译,因为g是用const引用来接受参数的,后者在面对一个临时的int变量的时候应该完全能够绑定。

而实际上以上代码却会编译失败,因为f的参数是T&,当面对一个non-const的int型右值(source()的返回值)时,会被推导为int&,而non-const引用不能绑定到右值。

好,现在的问题就变成,如何使得non-const右值也被正确转发,用T&作f的参数类型是行不通的,唯一能够正确转发non-const右值的办法是用const T&来接受它,但前面又说过,用const T&行不通,因为const T&不能正确转发non-const左值。

Const& + non-const&
那两个加起来如何?

template<typename T>
void f(T& t)
{
g(t);
}

template<typename T>
void f(const T& t)
{
g(t);
}

一次重载。我们来分析一下。

对于non-const左值,重载决议会选中T&,因为绑定到non-const引用显然优于绑定到const引用(const T&)。

对于const左值,重载决议会选中const T&,因为显然这是个更specialized的版本。

对于non-const右值,T&根本就行不通,因此显然选中const T&。

对于const右值,选中const T&,原因同第二种情况。

可见,这种方案完全保留了参数的左右值和const/non-const属性。

值得注意的是,对于右值来说,由于右值只能绑定到const引用,所以虽然const T&并非“(non-)const右值”的实际类型,但由于C++03只能用const T&来表达对右值的引用,所以这种情况仍然是完美转发。

组合爆炸
你可能会觉得上面的这个方案(const& + non-const&)已经是完美解决方案了。没错,对于单参的函数来说,这的确是完美方案了。

但是如果要转发两个或两个以上的参数呢?

对于每个参数,都有const T&和T&这两种情况,为了能够正确转发所有组合,必须要2的N次方个重载

比如两个参数的:

template<typename T1, typename T2>
void f(T1& t1, T2& t2) { g(t1, t2); }

template<typename T1, typename T2>
void f(const T1& t1, T2& t2) { g(t1, t2); }

template<typename T1, typename T2>
void f(T1& t1, const T2& t2) { g(t1, t2); }

template<typename T1, typename T2>
void f(const T1& t1, const T2& t2) { g(t1, t2); }

(完美的)解决方案
理想情况下,我们想要:

template<typename T1, typename T2, … >
void f(/*what goes here?*/ t1, /**/ t2, … )
{
  g(t1, t2);
}

填空处应该填入一些东西,使得当t1对应的实参是non-const/const的左/右值时,t1的类型也得是non-const/const的左/右值。目前的C++03中,non-const/const属性已经能够被正确推导出来(通过模板参数推导),但左右值属性还不能。

明确地说,其实问题只有一个:

对于non-const右值来说,模板参数推导机制不能正确地根据其右值属性确定T&的类型(也就是说,T&会被编译器不知好歹地推导为左值引用)。

修改T&对non-const右值的推导规则是可行的,比如对这种情况:

template<typename T>
void f(T& t);

f(1);

规定T&推导为const int&。

但这显然会破坏既有代码。

很巧的是,右值引用能够拯救世界,右值引用的好处就是,它是一种新的引用类型,所以对于它的规则可以任意制定而不会损害既有代码,设想:

template<typename T >
void f(T&& t){ g(t); }

我们规定:

如果实参类型为右值,那么T&&就被推导为右值引用。
如果实参类型为左值,那么T&&就被推导为左值引用。

Bingo!问题解决!为什么?请允许我解释。

f(1); // T&& 被推导为 int&&,没问题,右值引用绑定到右值。
f(i); // T&& 被推导为 int&,没问题,通过左值引用完美转发左值。

等等,真没问题吗?对于f(1)的情况,t的类型虽然为int&&(右值引用),但那是否就意味着t本身是右值呢?既然t已经是具名(named)变量了,因此t就有被多次move(关于move语意参考上一篇文章)的危险,如:

void dangerous(C&& c)
{
C c1(c); // would move c to c1 should we allow treating c as a rvalue
c.f(); // disaster
}

在以上代码中,如果c这个具名变量被当成右值的话,就有可能先被move掉,然后又被悄无声息的非法使用(比如再move一次),编译器可不会提醒你。这个邪恶的漏洞是因为c是有名字的,因此可以被多次使用。

解决方案是把具名的右值引用作为左值看待。

但这就使我们刚才的如意算盘落空了,既然具名的右值引用是左值的话,那么f(1)就不能保持1的右值属性进行转发了,因为f的形参t的类型(T&&)虽然被推导为右值引用(int&&),但t却是一个左值表达式,也就是说f(1)把一个右值转发成了左值。

最终方案
通过严格修订对于T&&的模板参数推导原则,以上问题可以解决。

修订后的模板参数推导规则为:

如果实参是左值,那么T就被推导为U&(其中U为实参的类型),于是T&& = U& &&,而U& &&则退化为U&(理解为:左值引用的右值引用仍然是左值引用)。

如果实参是右值,那么T就被推导为U,于是T&& = U&&(右值引用)。

如此一来就可以这样解决问题:

template<typename T>
void f(T&& t)
{
  g(static_cast<T&&>(t));
}

想想看,如果实参为左值,那么T被推导为U&,T&&为U& &&,也就是U&,于是static_cast<T&&>也就是static_cast<U&>,转发为左值。

如果实参为右值,那么T被推导为U,T&&为U&&,static_cast<T&&>也就是static_cast<U&&>,不像t这个具名的右值引用被看作左值那样,static_cast<U&&>(t)这个表达式由于产生了一个新的无名(unnamed)值,因而是被看作右值的。于是右值被转发为了右值。
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 35楼 发表于: 2012-12-26
C++11中的右值引用及move语义编程

C++0x中加入了右值引用,和move函数。右值引用出现之前我们只能用const引用来关联临时对象(右值)(造孽的VS可以用非const引用关联临时对象,请忽略VS),所以我们不能修临时对象的内容,右值引用的出现就让我们可以取得临时对象的控制权,终于可以修改临时对象了!而且书上说配合move函数,可以大大提高现有C++的效率。那么是怎样提高它的效率的呢?看段代码先!



#include <iostream>

#include <utility>

#include <vector>

#include <string>

int main()

{

    std::string str = "Hello";

    std::vector<std::string> v;

 

    // uses the push_back(const T&) overload, which means 

    // we'll incur the cost of copying str

    v.push_back(str);

    std::cout << "After copy, str is \"" << str << "\"\n";

 

    // uses the rvalue reference push_back(T&&) overload, 

    // which means no strings will copied; instead, the contents

    // of str will be moved into the vector.  This is less

    // expensive, but also means str might now be empty.

    v.push_back(std::move(str));

    std::cout << "After move, str is \"" << str << "\"\n";

 

    std::cout << "The contents of the vector are \"" << v[0]

                                         << "\", \"" << v[1] << "\"\n";

}


Output:


After copy, str is "Hello"

After move, str is ""

The contents of the vector are "Hello", "Hello"

 


看完大概明白一点儿了,加上move之后,str对象里面的内容被"移动"到新的对象中并插入到数组之中了,同时str被清空了。这样一来省去了对象拷贝的过程。所以说在str对象不再使用的情况下,这种做法的效率更高一些!但问题是str的内容在什么地方被移走的呢?move函数到底是干啥的?扣一下stl源码吧,下面是move模板的源码:



// TEMPLATE FUNCTION move

    template<class _Ty> inline

    typename tr1::_Remove_reference<_Ty>::_Type&&

        move(_Ty&& _Arg)

    {    // forward _Arg as movable

    return ((typename tr1::_Remove_reference<_Ty>::_Type&&)_Arg);

    }


 


好吧,看过了这段,可能有人又迷惑了,不是说有名左指变量不能绑定到右值引用上面么?为什么move函数的参数是右值引用却可以接受左值变量作为参数?难道STL错了么?事实上,C++0x在引入右值引用的时候对函数模板自动推导也加入了新的规则,简单的说,像例子中的这种情况,模板参数是_Ty而函数的参数是_Ty&&(右值引用),同时_Arg是string的左值对象的情况下,会触发一个特殊规则,_Ty会推导成string&,也就是说此事推导出来的函数与move<string&>一致。那么move(_Ty&& _Arg) 得到的应该是move(string& && _Arg)这个时候根据引用折叠原则,会变成这个样子move(string& _Arg)。详细的描述参见白云飘飘翻译的vc技术文档(http://www.cppblog.com/kesalin/archive/2009/06/05/86851.html)。函数的返回值嘛,就好说了,就是返回所持有类型的右值引用了。所以,move函数的作用很简单,不管你给什么参数,都返回对应类型的右值引用!那么,上面例子中str的不是在move函数中被移走的。综上,我们猜测str内容肯定是在构造新对象的过程中被新对象偷走的,也就是在string的参数为右值引用的构造函数中被偷走的!翻看string的源码(来自VS实现的STL),果然如此啊!如下:



        basic_string(_Myt&& _Right)

        : _Mybase(_STD forward<_Alloc>(_Right._Alval))

        {    // construct by moving _Right

        _Tidy();

        assign(_STD forward<_Myt>(_Right));

        }


        _Myt& assign(_Myt&& _Right)

        {    // assign by moving _Right

        if (this == &_Right)

            ;

        else if (get_allocator() != _Right.get_allocator()

            && this->_BUF_SIZE <= _Right._Myres)

            *this = _Right;

        else

            {    // not same, clear this and steal from _Right

            _Tidy(true);

            if (_Right._Myres < this->_BUF_SIZE)

                _Traits::move(this->_Bx._Buf, _Right._Bx._Buf,

                    _Right._Mysize + 1);

            else

                {    // copy pointer

                this->_Bx._Ptr = _Right._Bx._Ptr;

                _Right._Bx._Ptr = 0;

                }

            this->_Mysize = _Right._Mysize;

            this->_Myres = _Right._Myres;


            _Right._Tidy();

            }

        return (*this);

        }


 


所以,我们知道了,C++0x在STL模板库中加入了参数为右值引用的构造函数,用于把参数所关联对象中的数据移动到新对象当中,避免了深度拷贝,增加了效率。再详细翻看源码,可以发现除了构造函数,operator=也重载了一个参数为右值引用的函数,用途和构造函数类似。所以我们自定义中的类也应该增加参数为右值引用的构造函数和重载赋值运算符!原因是啥,看例子!


未定义参数为右值引用的构造函数:



#include <iostream>

#include <utility>

#include <vector>

#include <string>


using namespace std;


class MyPoint{

public:

    MyPoint()

        :comment(""), x(0), y(0)

    {

    }


    MyPoint(const MyPoint& p)

       :comment(p.comment),x(p.x),y(p.y) 

    {}


    //MyPoint(MyPoint&& p)

    //    :comment(move(p.comment)), x(p.x), y(p.y)

    //{

    //    p.x = 0;

    //    p.y = 0;

    //}


    string toString()

    {

        char buf[100];

        sprintf(buf, "%s: %d %d", comment.c_str(), x, y);


        return buf;

    }


    string comment;

    int x;

    int y;


};


int main()

{

    MyPoint p;

    p.comment = "First point";

    p.x = 9;

    p.y = 7;


    vector<MyPoint> v;

 

    v.push_back(p);


    cout << "After copy, str is \"" << p.toString() << "\"\n";

 

    v.push_back(move(p));

    cout << "After move, str is \"" << p.toString() << "\"\n";

 

    cout << "The contents of the vector are \"" << v[0].toString()

                                         << "\", \"" << v[1].toString() << "\"\n";



    cin.get();

}


结果:


After copy, str is "First point: 9 7"

After move, str is "First point: 9 7"

The contents of the vector are "First point: 9 7", "First point: 9 7"

 


定义了参数为右值引用的构造函数之后:


After copy, str is "First point: 9 7"

After move, str is ": 0 0"

The contents of the vector are "First point: 9 7", "First point: 9 7"

 


综上所述,C++0x中的move语义编程,不仅仅是在应用的时候使用参数中加上move,对于自定义类需要增加参数为右值引用的构造函数和赋值运算符,这种构造函数我们称为move构造函数!公司里面的c++标准已经更新,要求在定义copy构造函数的同时定义move构造函数,虽然现在这种编程方法没有流行,但是我相信以后这将成为另外一个媲美引用的优化运行速度的编程方法,我们拭目以待!

QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 36楼 发表于: 2012-12-26
c++中的左值和右值
本文大部分内容参考MSDN中关于左值和右值的介绍。基于vs2010,右值的语义在c++0x标准才支持。记录本人针对该知识点的一些了解,很多翻译的内容请参考原文。
http://blogs.msdn.com/b/vcblog/archive/2009/02/03/rvalue-references-c-0x-features-in-vc10-part-2.aspx


每一个c++表达式不是左值就是右值,一个左值用一个名字引用一个对象,所有的变量,包括const变量都是左值。右值是一个临时的值,它不能够持久存在除了在表达式中使用。
例如:
  #include <iostream>
using namespace std;
int main()
{
  int x = 3 + 4;
  cout << x <<endl;
}
在该例子中,x是一个左值,3+4是一个右值。


右值引用中的move semantics和perfect forwarding:
右值引用支持实现move semantics,move语义能够显著提高应用的性能,move语义能够将资源(例如动态分配的内存)从一个对象传输到另外一个,move语义能够从并不能在程序的任何地方进行引用临时对象进行资源转换。为了实现move语义,你只需在类里面提供一个move构造函数和operator=。copy和赋值操作都是利用move语义。不像默认构造函数,编译器不会提供一个默认的move构造函数。
vs2010中将move语义写入了STL,例如,string类实现了move语义的操作,下面的代码中
  #include <iostream>
#include <string>
using namespace std;
int main()
{
  string s = string("h") + "e" + "ll" + "o";
  cout << s << endl;
}


在visual c++ 2010之前,每次都会调用operator+,并且返回一个临时的string对象(右值),operator+不能够将一个string append到另外一个string是因为它并不知道source strings是左值或者右值,如果source strings都是左值,它们可能在程序的任何地方被引用,因此它们必须不能被修改。使用右值,operator+能够使用右值进行修改,并且不能在程序的任何地方进行修改。因此,operator+能够将string 添加到另外一个string后面。这样极大的减少了临时内存的分配,string就能提高性能。
如果在vector对象中插入一个元素,如果vector的容量已经满了,vector对象必须重新分配内存,将每一个元素拷贝到新的存储空间中。当一个插入操作拷贝一个元素,它先建立一个新的元素,调用拷贝构造函数从前一个元素拷贝元素到新的元素,然后销毁前一个元素,move语义能够直接完成这些功能而不需要调用拷贝操作和内存分配。


c++中的右值引用和左值引用一样可以进行重载,例如


#include <iostream>
#include <ostream>
#include <string>
using namespace std;


void function(string& s)
{
cout << "function(string&): "<< s << endl;
}


void function(const string& s)
{
cout << "function(const string&): "<< s << endl;
}


void function(string&& s)
{
cout << "function(string&&): " << s << endl;
}


void function(const string&& s)
{
cout << "function(const string&&): "<< s << endl;
}


string strange()
{
return "strange()";
}


const string charm()
{
return "charm()";
}


int main(){
string up("up");
const string down("down");

function(up);  //调用第一个function函数
function(down); //调用第二个function函数,因为down为const类型
function(strange()); //调用第三个function函数,strange()返回值为右值类型
function(charm()); //调用第四个function函数,charm()返回为const的右值类型
}


但是以下代码会出现另外一种结果
#include <iostream>
#include <ostream>
#include <string>
using namespace std;

void purr(const string& s) {
    cout << "purr(const string&): " << s << endl;
}

void purr(string&& s) {
    cout << "purr(string&&): " << s << endl;
}

string strange() {
    return "strange()";
}

const string charm() {
    return "charm()";
}

int main() {
    string up("up");
    const string down("down");

    purr(up); //初始化规则指出可能是purr(const string&)或者purr(string&&). up是一个左值,因此优先绑定左值引用purr(const   // string&),up可以进行修改,因此会第二选择与可修改的purr(string&&) 进行结合。
    purr(down); //由于const修饰,初始化规则否决purr(string&&),因此默认只能是purr(const string&)
    purr(strange()); //string()是右值,首先优先右值绑定引用purr(string&&),返回的strange为可修改的,所以会优先绑定非const     //的引用purr(string&&)。
    purr(charm()); // 由于charm返回的值是const,虽然返回的是右值,但是会调用purr(const string&)
}
输出结果:
purr(const string&): up
purr(const string&): down
purr(string&&): strange()
purr(const string&): charm()
输出这样的结果是由于基于以下三种规则
1.初始化规则拥有否决权
2.左值优先绑定左值引用,右值优先绑定右值引用
3.可修改的表达式优先绑定可修改的引用


Move语义:模式
以下是一个简单的类,remote_integer。该类存储了一个指针指向动态分配内存的数据int。拥有默认构造函数,只有一个参数的构造函数,拷贝构造函数,拷贝赋值操作符,析构函数。另外不同的是给出了一个move语义的构造函数和赋值符。采用条件编译指令来控制。
#include <stddef.h>
#include <iostream>
#include <ostream>
using namespace std;

class remote_integer {
public:
    remote_integer() {
        cout << "Default constructor." << endl;
        m_p = NULL;
    }

    //只有一个参数的构造函数,采用关键字explicit 来修饰,不能够使用remote_integer ri = 10形式来进行对象初始化
    explicit remote_integer(const int n) {  
        cout << "Unary constructor." << endl;
        m_p = new int(n);
    }

    remote_integer(const remote_integer& other) {
        cout << "Copy constructor." << endl;

        if (other.m_p) {
            m_p = new int(*other.m_p);
        } else {
            m_p = NULL;
        }
    }

#ifdef MOVABLE
    remote_integer(remote_integer&& other) {
        cout << "MOVE CONSTRUCTOR." << endl;

        m_p = other.m_p;
        other.m_p = NULL;
    }
#endif // #ifdef MOVABLE

    remote_integer& operator=(const remote_integer& other) {
        cout << "Copy assignment operator." << endl;

        if (this != &other) {
            delete m_p;

            if (other.m_p) {
                m_p = new int(*other.m_p);
            } else {
                m_p = NULL;
            }
        }

        return *this;
    }

#ifdef MOVABLE
    remote_integer& operator=(remote_integer&& other) {
        cout << "MOVE ASSIGNMENT OPERATOR." << endl;

        if (this != &other) {
            delete m_p;

            m_p = other.m_p;
            other.m_p = NULL;
        }

        return *this;
    }
#endif // #ifdef MOVABLE

    ~remote_integer() {
        cout << "Destructor." << endl;

        delete m_p;
    }

    int get() const {
        return m_p ? *m_p : 0;
    }

private:
    int * m_p;
};

remote_integer square(const remote_integer& r) {
    const int i = r.get();

    return remote_integer(i * i);
}

int main() {
    remote_integer a(8);
    cout << a.get() << endl;
    remote_integer b(10);
    cout << b.get() << endl;
    b = square(a);
    cout << b.get() << endl;
}
该类中有几个地方需要注意
1.拷贝和赋值函数,赋值操作符都被重载了,如果存在赋值b = square(a) ,会自动选择采用move语义的赋值操作来完成
2.代替动态的内存分配,采用move语义的构造函数和采用move语义的赋值操作符能够很简单的完成该功能。当拷贝other指针完成后,赋予NULL值,当other销毁后,析构函数不会做任何事情
3.拷贝和采用move语义的赋值操作都需要自赋值检查,以防止自己给自己赋值,编译器不会完成该检查,所以代码必须完成该检查,在c++0x中,像std::sort()算法采用move来代替copy。


注意:
1.采用move语义的构造函数和采用move语义的赋值符不会由编译器自动生成。
2.用户声明的构造函数约束隐式生成的默认构造函数,包括拷贝构造函数和采用move语义的构造函数
3.用户声明的拷贝构造函数越是隐式生成的拷贝构造函数,,但是不是用户声明的采用move语义的构造函数,采用move语义的赋值符也一样


move语义:从左值移动数据
按照move赋值符写move构造函数的时候需要注意一些地方
例如:
#include <stddef.h>
#include <iostream>
#include <ostream>
using namespace std;

class remote_integer {
public:
    remote_integer() {
        cout << "Default constructor." << endl;

        m_p = NULL;
    }

    explicit remote_integer(const int n) {
        cout << "Unary constructor." << endl;

        m_p = new int(n);
    }

    remote_integer(const remote_integer& other) {
        cout << "Copy constructor." << endl;

        m_p = NULL;
        *this = other;
    }

#ifdef MOVABLE
    remote_integer(remote_integer&& other) {
        cout << "MOVE CONSTRUCTOR." << endl;

        m_p = NULL;
        *this = other; // 错误
    }
#endif // #ifdef MOVABLE

    remote_integer& operator=(const remote_integer& other) {
        cout << "Copy assignment operator." << endl;

        if (this != &other) {
            delete m_p;

            if (other.m_p) {
                m_p = new int(*other.m_p);
            } else {
                m_p = NULL;
            }
        }

        return *this;
    }

#ifdef MOVABLE
    remote_integer& operator=(remote_integer&& other) {
        cout << "MOVE ASSIGNMENT OPERATOR." << endl;

        if (this != &other) {
            delete m_p;

            m_p = other.m_p;
            other.m_p = NULL;
        }

        return *this;
    }
#endif // #ifdef MOVABLE

    ~remote_integer() {
        cout << "Destructor." << endl;

        delete m_p;
    }

    int get() const {
        return m_p ? *m_p : 0;
    }

private:
    int * m_p;
};

remote_integer frumple(const int n) {
    if (n == 1729) {
        return remote_integer(1729);
    }

    remote_integer ret(n * n);

    return ret;
}

int main() {
    remote_integer x = frumple(5);
    cout << x.get() << endl;
    remote_integer y = frumple(1729);
    cout << y.get() << endl;
}


在Move构造函数中标记了“错误”字样调用了拷贝赋值操作符,能够编译运行,但是这不是move构造函数的真实目的,在c++98/03中被称为左值引用的都是左值(int& r = *p,所以r是左值),不称为左值引用的也成为左值(如vector<int> v(10,1729), v[0]返回int&,一个取到地址的非左值引用),右值引用的行为有点不同:
a:称为右值引用的都是左值
b:不称为右值引用的都是右值
称为右值引用是指一个左值,因为它能够被重复提到,并且能够在上面完成多个操作,如果代替它的都是右值,那么,第一次操作的完成是从它拷贝过来的值,将会影响接下来的操作,因此是禁止的。另外一方面,命名为右值的引用不能够被重复提及,所以他们能够保护他们的右值。
如果你准备使用move赋值操作符实现你的move构造函数,你必须会从左值的移动使用右值来操作,在visual stdio 2010中已经实现。
#include <stddef.h>
#include <iostream>
#include <ostream>
using namespace std;

template <typename T> struct RemoveReference {
     typedef T type;
};

template <typename T> struct RemoveReference<T&> {
     typedef T type;
};

template <typename T> struct RemoveReference<T&&> {
     typedef T type;
};

template <typename T> typename RemoveReference<T>::type&& Move(T&& t) {
    return t;
}

class remote_integer {
public:
    remote_integer() {
        cout << "Default constructor." << endl;

        m_p = NULL;
    }

    explicit remote_integer(const int n) {
        cout << "Unary constructor." << endl;

        m_p = new int(n);
    }

    remote_integer(const remote_integer& other) {
        cout << "Copy constructor." << endl;

        m_p = NULL;
        *this = other;
    }

#ifdef MOVABLE
    remote_integer(remote_integer&& other) {
        cout << "MOVE CONSTRUCTOR." << endl;

        m_p = NULL;
        *this = Move(other); // RIGHT
    }
#endif // #ifdef MOVABLE

    remote_integer& operator=(const remote_integer& other) {
        cout << "Copy assignment operator." << endl;

        if (this != &other) {
            delete m_p;

            if (other.m_p) {
                m_p = new int(*other.m_p);
            } else {
                m_p = NULL;
            }
        }

        return *this;
    }

#ifdef MOVABLE
    remote_integer& operator=(remote_integer&& other) {
        cout << "MOVE ASSIGNMENT OPERATOR." << endl;

        if (this != &other) {
            delete m_p;

            m_p = other.m_p;
            other.m_p = NULL;
        }

        return *this;
    }
#endif // #ifdef MOVABLE

    ~remote_integer() {
        cout << "Destructor." << endl;

        delete m_p;
    }

    int get() const {
        return m_p ? *m_p : 0;
    }

private:
    int * m_p;
};

remote_integer frumple(const int n) {
    if (n == 1729) {
        return remote_integer(1729);
    }

    remote_integer ret(n * n);

    return ret;
}

int main() {
    remote_integer x = frumple(5);

    cout << x.get() << endl;

    remote_integer y = frumple(1729);

    cout << y.get() << endl;
}

运行结果:
Unary constructor.
MOVE CONSTRUCTOR.
MOVE ASSIGNMENT OPERATOR.
Destructor.
25
Unary constructor.
1729
Destructor.
Destructor.


std::move()方法和上面代码中的Move()是可互换的,因此他们的实现是一致的。那么std::move()方法是怎么工作的呢,此刻,我只能说这是“魔术的调用”。(这下面有很好的解释,其实也不复杂,但是它包含了模板参数的推论和引用的解除,这将会在完美转发中看到),我省略了这个魔术而集中在该例子上来,给一个string类型的左值,就像上面重载方案的例子中的up,std::move(up)调用string&& std::move(string&),这将返回一个未命名的右值引用,这就是右值。给一个string类型的右值,就是像上面的strange(),std::move(stringe())调用string&& std::move(string&&)。同样,这将返回返回一个未命名的右值引用的右值。
std::move()方法不仅仅在实现根据move赋值操作实现move构造函数有很大的用处,而且在其它方面也很有用。你可以使用std::move(你的左值表达式)来激活move语义。


move语义:可移动的move成员


c++ 0x的标准类(例如vector,string,regex)拥有move语义的构造函数和move赋值操作符,并且我们看到在我们的类中怎么样去实现它们来管理资源(例如remote_integer),但是关于类包含移动的数据成员(例如vector,string,regex,remote_integer)?编译器不会自动为我们产生move构造函数和move赋值操作符,因此,我们必须手动的完成它,幸运的是,使用std::move(),这个变得很方便。
#include <stddef.h>
#include <iostream>
#include <ostream>
using namespace std;

template <typename T> struct RemoveReference {
     typedef T type;
};

template <typename T> struct RemoveReference<T&> {
     typedef T type;
};

template <typename T> struct RemoveReference<T&&> {
     typedef T type;
};

template <typename T> typename RemoveReference<T>::type&& Move(T&& t) {
    return t;
}

class remote_integer {
public:
    remote_integer() {
        cout << "Default constructor." << endl;

        m_p = NULL;
    }

    explicit remote_integer(const int n) {
        cout << "Unary constructor." << endl;

        m_p = new int(n);
    }

    remote_integer(const remote_integer& other) {
        cout << "Copy constructor." << endl;

        if (other.m_p) {
            m_p = new int(*other.m_p);
        } else {
            m_p = NULL;
        }
    }

    remote_integer(remote_integer&& other) {
        cout << "MOVE CONSTRUCTOR." << endl;

        m_p = other.m_p;
        other.m_p = NULL;
    }

    remote_integer& operator=(const remote_integer& other) {
        cout << "Copy assignment operator." << endl;

        if (this != &other) {
            delete m_p;

            if (other.m_p) {
                m_p = new int(*other.m_p);
            } else {
                m_p = NULL;
            }
        }

        return *this;
    }

    remote_integer& operator=(remote_integer&& other) {
        cout << "MOVE ASSIGNMENT OPERATOR." << endl;

        if (this != &other) {
            delete m_p;

            m_p = other.m_p;
            other.m_p = NULL;
        }

        return *this;
    }

    ~remote_integer() {
        cout << "Destructor." << endl;

        delete m_p;
    }

    int get() const {
        return m_p ? *m_p : 0;
    }

private:
    int * m_p;
};

class remote_point {
public:
    remote_point(const int x_arg, const int y_arg)
        : m_x(x_arg), m_y(y_arg) { }

    remote_point(remote_point&& other)
        : m_x(Move(other.m_x)),
          m_y(Move(other.m_y)) { }

    remote_point& operator=(remote_point&& other) {
        m_x = Move(other.m_x);
        m_y = Move(other.m_y);
        return *this;
    }

    int x() const { return m_x.get(); }
    int y() const { return m_y.get(); }

private:
    remote_integer m_x;
    remote_integer m_y;
};

remote_point five_by_five() {
    return remote_point(5, 5);
}

remote_point taxicab(const int n) {
    if (n == 0) {
        return remote_point(1, 1728);
    }

    remote_point ret(729, 1000);

    return ret;
}

int main() {
    remote_point p = taxicab(43112609);

    cout << "(" << p.x() << ", " << p.y() << ")" << endl;

    p = five_by_five();

    cout << "(" << p.x() << ", " << p.y() << ")" << endl;
}
运行结果:
Unary constructor.
Unary constructor.
MOVE CONSTRUCTOR.
MOVE CONSTRUCTOR.
Destructor.
Destructor.
(729, 1000)
Unary constructor.
Unary constructor.
MOVE ASSIGNMENT OPERATOR.
MOVE ASSIGNMENT OPERATOR.
Destructor.
Destructor.
(5, 5)
Destructor.
Destructor.


从上面代码看到,成员移动写得很琐碎。注意到remote_point's move赋值操作符不需要自赋值的检查,因为remote_integer已经做到了,remote_point隐式声明了拷贝构造函数,拷贝复制操作符和析构函数。


现在,你应该消化你熟悉的move语义,测试你新获取的知识能量,解决operator+()的拷贝构造函数问题。


建议:不管什么时候,你应该在你的类中实现你的move构造函数和move赋值操作符。因为你的编译器不会为你做这些事情,STL容器和算法类都优先实现了move语义来代替昂贵的拷贝操作。
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 37楼 发表于: 2013-02-28
C++强大背后
在31年前(1979年),一名刚获得博士学位的研究员,为了开发一个软件项目发明了一门新编程语言,该研究员名为Bjarne Stroustrup,该门语言则命名为——C with classes,四年后改称为C++。C++是一门通用编程语言,支持多种编程范式,包括过程式、面向对象(object-oriented programming, OP)、泛型(generic programming, GP),后来为泛型而设计的模版,被发现及证明是图灵完备的,因此使C++亦可支持模版元编程范式(template metaprogramming, TMP)。C++继承了C的特色,既为高级语言,又含低级语言功能,可同时作为系统和应用编程语言。


C++广泛应用在不同领域,使用者以数百万计。根据近十年的调查,C++的流行程度约稳定排行第3位(于C/Java之后)。 C++经历长期的实践和演化,才成为今日的样貌。1998年,C++标准委员会排除万难,使C++成为ISO标准(俗称C++98),当中含非常强大的标准模版库(standard template library, STL)。之后委员会在2005年提交了有关标准库的第一个技术报告(简称TR1),并为下一个标准C++0x而努力。可惜C++0x并不能在200x年完成,各界希望新标准能于2011年内出台。


流行的C++编译器中,微软Visual C++ 2010已实现部分C++0x语法并加入TR1扩充库,而gcc对C++0x语法和库的支持比VC2010更多。


应否选择C++
哪些程序适宜使用C++?


C++并非万能丹,我按经验举出一些C++的适用时机。


C++适合构造程序中需求较稳定的部分,需求变化较大的部分可使用脚本语言;
程序须尽量发挥硬件的最高性能,且性能瓶颈在于CPU和内存;
程序须频繁地与操作系统或硬件沟通;
程序必须使用C++框架/库,如大部分游戏引擎(如Unreal/Source)及中间件(如Havok/FMOD),虽然有些C++库提供其他语言的绑定,但通常原生的API性能最好、最新;
项目中某个目标平台只提供C++编译器的支持。
按应用领域来说,C++适用于开发服务器软件、桌面应用、游戏、实时系统、高性能计算、嵌入式系统等。


使用C++还是C?


C++和C的设计哲学并不一样,两者取舍不同,所以不同的程序员和软件项目会有不同选择,难以一概而论。与C++相比,C具备编译速度快、容易学习、显式描述程序细节、较少更新标准(后两者也可同时视为缺点)等优点。在语言层面上,C++包含绝大部分C语言的功能(例外之一,C++没有C99的变长数组VLA),且提供OOP和GP的特性。但其实用C也可实现OOP思想,亦可利用宏去实现某程度的GP,只不过C++的语法能较简洁、自动地实现OOP/GP。C++的RAII(resource acquisition is initialization,资源获取就是初始化)特性比较独特,C/C#/Java没有相应功能。回顾历史,Stroustrup开发的早期C++编译器Cpre/Cfront是把C++源代码翻译为C,再用C编译器编译的。由此可知,C++编写的程序,都能用等效的C程序代替,但C++在语言层面上提供了OOP/GP语法、更严格的类型检查系统、大量额外的语言特性(如异常、RTTI等),并且C++标准库也较丰富。有时候C++的语法可使程序更简洁,如运算符重载、隐式转换。但另一方面,C语言的API通常比C++简洁,能较容易供其他语言程序调用。因此,一些C++库会提供C的API封装,同时也可供C程序调用。相反,有时候也会把C的API封装成C++形式,以支持RAII和其他C++库整合等。


为何C++性能可优于其他语言?


相对运行于虚拟机语言(如C#/Java),C/C++直接以静态形式把源程序编译为目标平台的机器码。一般而言,C/C++程序在编译及链接时可进行的优化最丰富,启动时的速度最快,运行时的额外内存开销最少。而C/C++相对动态语言(如Python/Lua)也减少了运行时的动态类型检测。此外,C/C++的运行行为是确定的,且不会有额外行为(例如C#/Java必然会初始化变量),也不会有如垃圾收集(GC)而造成的不确定性延迟,而且C/C++的数据结构在内存中的布局也是确定的。有时C++的一些功能会使程序性能优于C,当中以内联和模版最为突出,这两项功能使C++标准库的sort()通常比C标准库的qsort()快多倍(C可用宏或人手编码去解决此问题)。另一方面,C/C++能直接映射机器码,之间没有另一层中间语言,因此可以做底层优化,例如使用内部(intrinsic)函数和嵌入汇编语言。然而,许多C++的性能优点并非免费午餐,代价包括较长的编译链接时间和较易出错,因而增加开发时间和成本,这点稍后补充。


我进行了一个简单全局渲染性能测试(512x512像素,每像素10000个采样),C++ 1小时36分、Java 3小时18分、Python约18天、Ruby约351天。评测方式和其他语言的结果详见博文。


C++常见问题
C++源代码跨平台吗?


C++有不错的跨平台能力,但由于直接映射硬件,因性能优化的关系,跨平台能力不及Java及多数脚本语言。然而,实践跨平台的C++软件还是可行的,但须注意以下问题:


C++标准没有规定原始数据类型(如int)的大小,需要特定大小的类型时,可自订类型(如int32_t),同时对任何类型使用sizeof()而不假设其大小;
字节序(byte order)按CPU有所不同,特别要注意二进制输入输出、reinterpret_cast法;
原始数据和结构类型的地址对齐有差异;
编译器提供的一些编译器或平台专用扩充指令;
避免作应用二进制接口(application binary interface, ABI)的假设,例如调用函数时参数的取值顺序在C/C++中没定义,在C++中也不可随便假设RTTI/虚表等实现方式。
总括而言,跨平台C++软件可在头文件中用宏检测编译器和平台,再用宏、typedef、自定平台相关实现等方法去实践跨平台,C++标准不会提供这类帮助。


C++程序容易崩溃?


和许多语言相比,C/C++提供不安全的功能以最优化性能,有可能造成崩溃。但要注意,很多运行时错误,如向空指针/引用解引用、数组越界、堆栈溢出等,其他语言也会报错或抛出异常,这些都是程序问题,而不是语言本身的问题。有些意见认为,出现这类运行时错误,应该尽量写入日志并立即崩溃,不该让程序继续运行,以免造成更大的影响(例如程序继续把内存中错误的数据覆写文件)。若要容错,可按业务把程序分割为多进程,像Chrome或使用fork()的形式。然而,C++有许多机制可以减少错误,例如以string代替C字符串;以vector或array(TR1)代替原始数组(有些实现可在调试模式检测越界);使用智能指针也能减少一些原始指针的问题。另外,我最常遇到的Bug,就是没有初始化成员变量,有时会导致崩溃,而且调试版和发行版的行为可能不同。


C++要手动做内存管理?


C++同时提供在堆栈上的自动局部变量,以及从自由存储(free store)分配的对象。对于后者,程序员需手动释放,或使用不同的容器和智能指针。 C++程序员经常进一步优化内存,自定义内存分配策略以提升效能,例如使用对象池、自定义的单向/双向堆栈区等。虽然C++0x还没加入GC功能,但也可以自行编写或使用现成库。此外,C/C++也可以直接使用操作系统提供的内存相关功能,例如内存映射文件、共享内存等。


使用C++常要重造轮子?


我曾参与的C++项目,都会重造不少标准库已提供的功能,此情况在其他语言中较少出现。我试图分析个中原因。首先,C++标准库相对很多语言来说是贫乏的,各开发者便会重复地制造自订库。从另一个角度看,C++标准库是用C++编写的(很多其他语言不用自身而是用C/C++去编写库),在能力和性能上,自订库和标准库并无本质差别;另外,标准库为通用而设,对不同平台及多种使用需求作取舍,性能上有所影响,例如EA公司就曾发表自制的EASTL规格,描述游戏开发方面对STL的性能及功能需求的特点;此外,多个C++库一起使用,经常会因规范不同而引起冲突,又或功能重叠,所以项目可能须自行开发,或引入其他库的概念或实现(如Boost/TR1/Loki),改写以符合项目规范。


C++编译速度很慢?


错,是非常慢。我认为C++可能是实用程序语言中编译速度最慢的。此问题涉及C++沿用C的编译链接方式,又加入了复杂的类/泛型声明和内联机制,使编译时间倍增。在C++对编译方法改革之前(如module提案),可使用以下技巧改善:第一,使用pimpl手法,因性能损耗应用于调用次数不多的类;第二,仅包含必要头文件,并尽量使用及提供前置声明版本的头文件(如iosfwd);第三采用基于接口的设计,但须注意虚函数调用成本;第四,采用unity build,即把多个cpp文件结合在一个编译单元进行编译;第五,采用分布式生成系统如IncrediBuild。


C++缺乏什么功能?


虽然C++已经非常复杂,但仍缺少很多常见功能。 C++0x作出了不少改善,例如语言方面加入Lambda函数、闭包、类型推导声明等,而库方面则加入正则表达式、采用哈希表的unordered_set/unordered_map、引用计数智能指针shared_ptr/weak_ptr等。但最值得留意的是C++0x引入多线程的语法和库功能,这是C++演进的一大步。然而,模组、GC、反射机制等功能虽有提案,却未加进C++0x。


C++使用建议
为应用挑选特性集


我同意Stroustrup关于使用C++各种技术的回应:“你可以做,不意味着你必须这么做。(Just because you can do it, doesn't mean that you have to.)” C++充满丰富的特性,但同时带来不同问题,例如过分复杂、编译及运行性能的损耗。一般可考虑是否使用多重继承、异常、RTTI,并调节使用模版及模版元编程的程度。使用过分复杂的设计和功能,可能会令部分团队成员更难理解和维护。


为团队建立编程规范


C++的编码自由度很高,容易编写风格迥异的代码,C++本身也没有定义一些标准规范。而且,C++的源文件物理构成,较许多语言复杂。因此,除了决定特性集,每个团队应建立一套编程规范,包括源文件格式(可使用文件模版)、花括号风格。


尽量使用C++风格而非C风格


由于C++有对C兼容的包袱,一些功能可以使用C风格实现,但最好使用C++提供的新功能。最基本的是尽量以具名常量、内联函数和泛型取代宏,只把宏用在条件式编译及特殊情况。旧式的C要求局部变量声明在作用域开端,C++则无此限制,应把变量声明尽量置于邻近其使用的地方,for()的循环变量声明可置于for的括号内。 C++中能加强类型安全的功能应尽量使用,例如避免“万能”指针void *,而使用个别或泛型类型;用bool而非int表示布尔值;选用4种C++ cast关键字代替简单的强制转换。


结合其他语言


如前文所述,C++并非适合所有应用情境,有时可以混合其他语言使用,包括用C++扩展其他语言,或在C++程序中嵌入脚本语言引擎。对于后者,除了使用各种脚本语言的专门API,还可使用Boost或SWIG作整合。


C++学习建议
C++缺点之一,是相对许多语言复杂,而且难学难精。许多人说学习C语言只需一本K&R《C程序设计语言》即可,但C++书籍却是多不胜数。我是从C进入C++,皆是靠阅读自学。在此分享一点学习心得。个人认为,学习C++可分为4个层次:


第一层次,C++基础:挑选一本入门书籍,如《C++ Primer》、《C++大学教程》、或Stroustrup撰写的经典《C++程序设计语言》或他一年半前的新作《C++程序设计原理与实践》,而一般C++课程也止于此,另外《C++ 标准程序库》及《The C++ Standard Library Extensions》可供参考;
第二层次,正确高效地使用C++:此层次开始必须自修,阅读过《(More)Effective C++》、《(More)Exceptional C++》、《Effective STL》及《C++编程规范》等,才适宜踏入专业C++开发之路;
第三层次,深入了解C++:关于全局问题可读《深入探索C++对象模型》、《Imperfect C++》、《C++沉思录》、《STL源码剖析》,要挑战智商,可看关于模版及模版元编程的书籍如《C++ Templates》、《C++设计新思维》、《C++模版元编程》;
第四层次,研究C++:阅读《C++语言的设计和演化》、《编程的本质》(含STL设计背后的数学根基)、C++标准文件《ISO/IEC 14882:2003》、C++标准委员会的提案书和报告书、关于C++的学术文献。
由于我主要是应用C++,大约只停留于第二、三个层次。然而,C++只是软件开发的一环而已,单凭语言并不能应付业务和工程上的问题。建议读者不要强求几年内“彻底学会C++的知识”,到达第二层左右便从工作实战中汲取经验,有兴趣才慢慢继续学习更高层次的知识。虽然学习C++有难度,但也是相当有趣且有满足感的。


数十年来,C++虽有起伏,但她依靠其使用者而不断得到顽强的生命力,相信在我退休之前都不会与她分离,也希望更进一步了解她,与她走进未来。
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 38楼 发表于: 2013-02-28
C++会王者归来吗?
因为又有人邀请我去Quora的C2C网站去回答问题去了,这回是 关于 @laiyonghao 的这篇有点争议的博文《2012 不宜进入的三个技术点》ActionScript,Thread 和 C++, C++争议的争议最大。(要我说,.NET比C++更需要慎重进入,呵)。我就在这里回复一下这个问题吧。


正好我一个月前看到一个视频,这个演讲视频还比较著名,这个演讲者是Exceptional C++ 和 C++ Coding Standards 的作者,还是ISO C++ 委员会的Chair,C++/CLI首席架构师,还是Microsoft的软件架构师,他叫Herb Sutter,他的这个演讲视频是 C++ and Beyond 2011上的一次公开演讲,题目是——Why C++? (如果你觉得那里的视频比较慢,你可以看优酷上的视频)(英文听力好的同学可以看一样,因为都没有中文字幕)


我觉得这篇文章就足够可以说明很多问题了,所以,我把Herb的演讲幻灯片截了几页放到这里,并做上一些注释,算是一个演讲内容摘要吧。


1) 为什么C++?因为 Performance per $,也就是说performance 就是钱,这个分成三个方面,


耗电,芯片的耗电量,移动设备的耗电量,家用电脑的耗电量都和钱有关系。
资源,家用电脑和移动设备上的处理器资源有限,因为要让一般消费者买的起。
体验,在更小的设备上会有更好的体验,有更好的体验就可以挣更多的钱。
移动设备上的耗电量相信用过智能手机的人都知道吧,Android手机的耗电量实在是太大了。就算是iPhone在开启Wifi和3G的情况下耗电量也很快。








2)C++的进化分成三个时代:


1979 – 1989:研究C的对象能力。主要是为C++做准备
1989 – 1999:C++成了主流。
1999 – 2009:Coffee-based语言(Java, .NET)出现了,极大的提高了开发生产力。
对于第三个时代,Herb说了很多,他说这个并没有什么错,因为这个时候我们非常关注开发的生产力,这个非常重要,这就是为什么C++一下就失去优势的地方。但是是否这些Coffee-Based的语言可以做任重要的事呢?不行,很多时候,这是一个Trade-Off的事,也就是生产力不是免费的是需要你用别的东西去交换的。






3)第四个时期。


Herb认为,2009-2019是第四个时期,因为我们又喜欢Native Code了,C++从被驱逐后又被请回来了。因为网站的性能越来越是个问题,移动端的设备非常流行。但主要是因为Performance就是钱,因为前面的三个因素,性能影响的是dollar,不尊重性能的公司都会发现花钱的速度太快了。(比如去年大家热炒的京东促销和12306.cn的问题,12306给整个社会造成了巨大的金钱浪费)


Herb把这个时期比做 The Return of the King。(指环王的第三部:王者归来) 性能为王!


这就好像我在“软件开发的三重门”里说的,开垦时代需要的是快和生产力,而开垦完后就得保证其稳定性。






4)Herb还给了一张幻灯片问,“The World is built on ….”,后面例出了多个语言。然后Herb说,世界是由C和C++构成的。






5)Herb给了一张表格,这张表可相当形像。如果把我们的对编程语言的需求总结为四个:效率,灵活,抽象,生产率。那么,C语言玩的是前两个,而C++玩的是前三个,Java和C#玩的是后两个(抽象和生产率)


任保一种设计都不可能让你什么都要的,这就是Trade-Off——什么事都需要交换的。






6)Herb举了一个微软内的例子,用C++ 和 ATL 来开发IE工具条的报告,意思是你可以用脚本在IE的工具条上加按钮,但是作者建议使用C++,因为用.NET或是脚本有重大的limitation,尤其是性能上的问题。






7)接下来,我们来看看移动设备。


下图中,第一个是iOS,第二个是Android,第三个是WinPhone。Herd说了几个事:


a)比Web APP,人们更喜欢Native的APP,这个在用移动设备上可以得到验证。


b)iOS也好,Android也好,WinPhone也好,他们不是在搞操作系统,而是在搞应用,为的是让智能手机更好。手机就是一个App。


c)这三个手机在第一版出来时都不支持C++,而第二版出来时都支持C++了。因为他们要兼顾性能和一定程度上的开发效率。WinPhone还没有到第二版,让我们拭目以待。(我以前写过一篇调侃Android支持C++开发的文章,这也只是一年前的事,说明C++全面回归了)






8)如果你还是不相信的话,我们可以看看为什么Apple和Google都在搞C++的编译器,因为他们觉得g++性能不行。所以,基于LLVM的编译器正在领导潮流,因为我们关注Natvie Code的性能优化。






9)接下来,Herb说了一下数据中心,你知道数据中心最花钱的是什么吗?三个事:


57% 花在了硬件上。
18% 花在了配电和降温上。
13% 花在了耗电上。
88%的钱花在了硬件和电力上。这可是很大一笔费用啊。(还有人说硬件比软件便宜吗?)我记得我上一个公司的数据中心每年要花的电费就在百万美元以上。






10)昨天在微博上有个笑话,说是某咨询师要求程序员把代码打印出来走查,程序员问是不是要用彩打?哈哈。我说,这至少不环保嘛。消耗太大了。是的,C++是可以省电的,以及于C++之父都在YouTube 说C++是可以减轻全球变暖的问题。哇,C++开始真正造福人类了。






11)我还需要重温一下老大的这句话——


My contribution to the fight against global warming is C++’s efficiency: Just think if Google had to have twice as many server farms! Each uses as much energy as a small town. And it’s not just a factor of two… Efficiency is not just running fast or running bigger programs, it’s also running using less resources.


Bjarne Stroustrup, June 2011


最后一句说的非常好!效率不仅仅只是跑得,跑得多,更是可以使用更少的资源。


12)下面让我们再来看一张表,一张把钱投到哪里的表格,这样我们可以看到一些趋势。


70年代80年代,资源不够,主要是把钱投在性能上。
80年代到90代,主要是90年代开始有一半的投次到了抽象和生产率上。
00年代,完全都在抽象和生产率上。
10年代,80%的钱都要回头来解决性能问题。这就是C/C++的王者归来。




13)当然,不是C++不注重 开发效率,看看C++0X的标准引入了多少东西我们就知道了。但是本质上,C++还是致力于性能和抽象的完全平衡。






那么,我们还会觉得C++要被淘汰了,不适合进入了吗?看完这个演讲,你应该有答案的。


后面讲了C++的文艺复兴,你可以在Google 搜索 “C++ Renaissance”看看。另外,该视频的讲议可以在这里下载。


(全文完)
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 39楼 发表于: 2013-03-08
C++11快速迭代开发
过去的一年我在微软亚洲研究院做输入法,我们的产品叫“英库拼音输入法” (下载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
描述
快速回复

您目前还是游客,请 登录注册
如果您在写长篇帖子又不马上发表,建议存为草稿