C++ 杂谈(2)

几个大牛认为最牛 b 的 feature

很出人意料的是“}”,这个本质上就是传说中的 RAII,一个 scope 结束意味着这个 scope 里面的对象生命的结束,资源的释放,这个资源不仅仅是狭义的内存,还包括各种其他的东西,如 lock、文件、socket 等等。正是因为这个 feature,C++ 不需要 Java 的 finally 以及后面各种对 try catch 的 hack。

千万别用 linked list

最经常比较的两个数据结构 array 和 linked list 其实并不像传统数据结构的书上所说后者占据很大的优势(常数 insertion、deletion),相反在实际应用中 array 在 performance 上占有绝对的优势,这主要依赖于硬件上对访问连续内存进行的优化,通常 L1/L2/L3 cache 的大小会极大的影响数据的访问速度,而每层 cache 之间存在的 prefetch 对容易判断出来的 pattern(正序逆序遍历)具有更好的判断正确性,这会导致 array 在这类访问中要比 linked list 有更好的表现,big O 表达的仅仅只是“渐进”的关系,也就是说 n 较小的时候这个关系可能不对,即便 n 较大,对应的常数可能也会很大。

字符串的传递

常见的写法是传递 const char* 或者 const string&,后者会出现某些 concern,比如实际传递的是 const char*,则会导致 string 的构造函数被调用,产生一定的 overhead。某些想法是为两者提供一个代理类,实际传递写代理类,我个人觉得这也不是个好办法。如果真心需要性能,传递 const char* 就行了啊,无非 string 用 c_str() 而已啊,弄个代理类一来 pollute 了整个 code base,还可能造成 inconsistent 的情况,第二还得提供这个代理类转另外两个类型的方法,因为总会有代码仍然使用这些类型(第三方的库、STL 等等)。兴许某些字符串函数通过它能够更加高效的传递信息,但是感觉这并不应该作为一个 universal 的要求。

大小最好用 int64/size_t

现在存储的尺寸大了,所以这个很明显用 64bit 要更安全一点,32bit 也就是 4T 而已。

临时变量的引用

在 C++11 的标准里面有个很诡异的说法,表示如果一个函数返回的临时对象被赋值给一个引用,则跟赋值给一个变量有类似的行为。理论上我们不应该返回局部变量的引用,但是现在却可以将这个返回的临时对象赋值给引用了,这总觉得有点怪怪的,但是实验了两个居然都没 seg fault,看来编译器真的要做得更加智能了,与此相关的问题是,那么如果返回的临时对象的一个部分的引用被赋值给一个引用,那么编译器是否能足够聪明到记住整个对象呢?

#include <iostream>

struct foo {
  std::string data;

  std::string& ref () {
    return data;
  }

  foo (const std::string& d) : data(d) {}
};

foo operator+ (const foo& a, const foo& b) {
  return foo(a.data + b.data);
}

int
main (int, char**) {
  std::string a = "hello ";
  std::string b = "world\n";

  const std::string& c = a + b;
  std::cout << c;

  const std::string& d = (foo(a) + foo(b)).ref ();
  std::cout << d;

  return 0;
}

非 POD 做 global 的问题

C++ 标准并没有定义这些类型的初始化顺序,因此我们应该尽量避免使用它们。那么如果不可避免的需要使用它们怎么解决他们的关系呢?一种策略是使用 lazy variable,这样仅仅在被使用的时候他们才会被实例化,其间的依赖关系决定了初始化的顺序。C++ 并没有 lazy variable,替代的技术就是使用 pointer,但是这类 pointer 必须是智能的 POD 的。

主要出现的问题是在于不同的编译单元的 static variable 初始化是没有定义的。这种 case 一般需要对整体机制有一定理解的人写一些 dirty code,将这种困难封装起来,让一般的程序员脱离这种问题。下面的程序展示了这个问题

// main.cpp
#include <iostream>

struct A;
struct B;

extern A a;
extern B b;

int
main(int, char**) {
  return 0;
}

// a.cpp
#include <iostream>
struct A {
  A () {
    std::cout << "A constructed\n";
  }
};

static A a;

// b.cpp
#include <iostream>
struct B {
  B () {
    std::cout << "B constructed\n";
  }
};

static B b;

连接的顺序如果不一致就会导致不同的结果

$ c++     main.cpp a.o b.o   -o main
$ ./main
A constructed
B constructed
$ c++     b.o a.o main.cpp  -o main
$ ./main
B constructed
A constructed

很明显如果 B 依赖于 A,那么 B 的构造在前面就会挂掉。控制文件的顺序似乎不是什么好办法,最好的办法还是依靠程序自己 figure out。

RVO

一般来说 RVO 的要求非常的严格,比如只能有一个 local variable,所以不管怎么说 move 还是很有必要的。

单一参数的构造函数尽量用 explicit

这个主要是避免无意间的类型转换。

STL 的 reserve

一般来说除非非常清楚最终的结果,那么只需要一次 reserve 即可;因为 vector 之类增长的方式决定了 amortized cost 其实是常数,人为的 reserve 反而会破坏内在的这种机制。

退出方式

常见的在 main 里面 return,通常 0 表示正常其他会返回操作系统一个 value,类似的可以使用 exit 方法,后者同时有一个所谓的 atexit 方法用来注册 exit 时的 handler,如果调用 abort 则不会使用 atexit 的 handler。C++ 程序往往默认会存在一个 handler 处理 global 对象的 destruction。与此类似的有 signal handler。

那么一般来说对于一个公司或者一个开发团队,写 native code 往往需要两种做法:

  • 写一个 library,它 take over main,然后要求其他的应用程序或者继承某个类从而和这部分 link 在一起或者实现某个方法,这样一来多数应用程序 level 的事情可以在这个环境里面以接近 main 的方式实现
  • 写一个函数/macro 要求所有的程序初始化在 main 里面必须调用它

在这层抽象里面往往解决的是一般性的 handler 的设置等等

——————
And he said unto him, What is thy name? And he said, Jacob

Advertisements
C++ 杂谈(2)

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s