代码改变世界

C++ Traps & Pitfalls

2012-12-02 14:36  robturtle  阅读(594)  评论(0编辑  收藏  举报

vector iterator bomb

仅仅为了随机访问的特性,究竟要在性能和安全性上牺牲多少?STL告诉了我们答案:很多。

考虑 vector 扩容的方法是申请新内存并拷贝,引用在原来内存地址的迭代器将全部失效。也就是说,在需要动态扩容的情形下,下标访问要更安全。

不要用register, 少用inline

个人认为,类似于这种的修饰符属于历史的遗留,是编译器的编写者把问题抛给用户的不负责任的做法——用户被迫需要处理更多的复杂度,而则多付出的劳动未必能带来好的回报,因为用户往往很难正确地使用这些优化手段,而且很多时候使用这种修饰符手动优化的程序还要比机器自动优化的糟糕很多。并且现代的编译器已经达到了相当的智能了,已经不再需要程序员提供什么额外的信息才能进行合适的优化了。正因为如此,许多现代c/c++的编译器实现就直接忽略掉上述的修饰符

istream operator>>() 与 get() 的混用

当使用>>方法时,回车是分界符之一,当istream遇到分界符时,将停止输入,并将分界符放回流中。当再次调用>>方法时,istream会自动跳过white characters,所以这个回车也被跳过了。因此连用>>方法不会出现问题。

但是get()方法并不会跳过任何字符,也就是说,>>方法使用后留下的回车符将导致get()方法立刻停止读取。

常用的方法是,在每一次cin读取完成后,调用eatline()方法,一般eatline()被定义如下:

void eatline() { while (std::cin.get() != '\n') continue; }

最近刚看完《C++编译调试密笈》,书中多次提到,凡是要求程序员每次干完A,就一定要干B的解决方案,就一定是不好的解决方案。书中对vector和array类型都进行了包装,使其拥有根据宏“_DEBUG”检测下标越界的断言开关。对于这个问题也可以类似解决。我们看到只有get()对去除头部回车有要求,我们可以简单地包装cin.get()方法。在自定义的命名空间中声明istream继承std::istream,重载istream::get()方法,使该方法先调用eatline(),再调用std::istream::get()。

类型自动推断

大部分情况下,auto关键字推断出的类型与常引用类型一致,如果需要将变量声明为非常量引用,则需要声明为引用类型。如:

typedef std::vector<boost::shared_ptr<int>> vs;
vs v(10);
int i = 0;

for (auto sp : v)
    sp = boost::make_shared<int>(++i);      // compile time error!

for (auto & sp : v)
    sp = boost::make_shared<int>(++i);      // correct

const, & 修饰符

  • iostream 中全局函数接受的输入流参数类型为istream的非常量引用

  • boost 中 finder_XXX() 不接受 const string &,而接受 string

如果使用了错误的类型声明,就会产生一大堆很难找到头绪的错误信息,称之为 pitfall 绝对不为过。

模板编译多文件支持

如果将模板源文件分成*.h和*.cpp,则必须在文件中声明所有可能用到的实例。由于目前大部分编译器器不支持模板多文件编译,因此,就不存在所谓的模板库文件(除非你声明了有限种可能类型)。 

括号

假设你有一个函数对象A,有一个接受另一个函数对象参数的构造函数:

using result_type = ...
using arg_type    = ...
using fun_t       = std::function<Signature>;

class A {
    fun_t;
public:
    A(fun_t f): f_(f) {}

    result_type operator() (arg_type arg)
    { ... }
};

class B {
public:
    result_type operator() (arg_type arg)
    { ... }
};

那么:

 

A a(B());
arg_type arg;
a(arg); // compile error: cannot convert 'arg_type' to 'B (*) ()' for argument ...
// This is because g++ think 'A a(B())' as a function declaration with signature 'A(B(*)())' !

B b();
A a(b); // compile error: cannot convert 'B (*) ()' to 'arg_type' for argument ...
// This is because g++ think 'B b()' as a function declaration with signature 'B (*) ()' !

B b;
A a(b); // OK

shared_ptr

错误的用法: 

void foo(shared_ptr<T>) { ... }

foo(make_shared<T>(...).get());

如果这个函数将在一个线程声明周期内使用这个指针对象,很难保证指针对象的生命周期长于线程的。

非法地址解引用

C++缺乏一种有效的手段来断言在非法地址的解引用,这包括了两个方面:对非法指针的解引用,以及对迭代器的解引用。

如果发生这种错误,不会有异常抛出,你只会得到冷冰冰的“command terminated (core dumped)”。

在检查 gnu c++ 4.7.2 源代码中同样发现了这点,比如在 bits/stl_algo.h 中对 __insertion_sort 的定义时,仅仅检查了两迭代器参数是否相等这种情况,而没有更多更有效的检查手段。

正因为此,不仅不推荐在程序设计中显式使用指针,同样不推荐显式使用迭代器。作为用户程序员,大部分功能往往都可以使用C++11的冒号for语句,以及for_each加lambda实现。