Effective STL - 06 - Programming by STL

Programming by STL

条款43:尽量用算法调用代替手写循环

  • 如果你要做的是算法已经提供了的,或者非常接近于它提供的,调用泛型算法更清晰。
  • 如果循环里要做的事非常简单,但调用算法时却需要使用绑定和适配器或者需要独立的仿函数类,你恐怕还是写循环比较好。
  • 最后,如果你在循环里做的事相当长或相当复杂,天平再次倾向于算法。

条款44:尽量用成员函数代替同名的算法

关联容器提供了count、find、lower_bound、upper_bound和equal_range,而list提供了remove、remove_if、unique、sort、merge和reverse。大多数情况下,你应该用成员函数代替算法。这样做有两个理由。首先,成员函数更快。其次,比起算法来,它们与容器结合得更好(尤其是关联容器)。那是因为同名的算法和成员函数通常并不是是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
set<int> s;
for (int i = 0; i < 1'000'000; i++) s.insert(i);

// 1ms
// 使用 rand 避免缓存
benchmark([&s] () {
auto it = s.find(rand() % s.size());
});

// 1809ms
benchmark([&s] () {
auto it = find(s.begin(), s.end(), rand() % s.size());
});

成员函数使用了与 关联容器 一致的等价算法,而 find 使用的是 相等 算法。因此,如果使用关联容器的话,你应该尽量使用成员函数形式的find、count、lower_bound等等,而不是同名的算法,因为这些成员函数版本提供了和其它成员函数一致的行为。

list成员函数的行为和它们的算法兄弟的行为经常不相同。正如条款32所解释的,如果你真的想从容器中清除对象的话,调用remove、remove_if和unique算法后,必须紧接着调用erase函数;但list的remove、remove_if和unique成员函数真的去掉了元素,后面不需要接着调用erase。

条款45:注意count、find、binary_search、lower_bound、upper_bound和equal_range的区别

你想知道的 在无序区间 在有序区间 在set或map上(成员函数) 在multiset或multimap上(成员函数)
期望值是否存在? find binary_search count find
期望值是否存在?如果有,第一个等于这个值的对象在哪里? find equal_range find find或lower_bound(参见下面)
第一个不在期望值之前的对象在哪里? find_if lower_bound lower_bound lower_bound
第一个在期望值之后的对象在哪里? find_if upper_bound upper_bound upper_bound
有多少对象等于期望值? count equal_range,然后 distance count count
等于期望值的所有对象在哪里? find(迭代) equal_range equal_range equal_range
  • 对于无序区间,大部分都是使用 find 或者 find_if
  • 对于有序区间,可以使用 binary_search、lower_bound、upper_bound、equal_range
  • 对于关联式容器,也可以用本身的成员函数

条款46:考虑使用函数对象代替函数作算法的参数

如果一个函数对象的operator()函数被声明为内联(不管显式地通过inline或者隐式地通过定义在它的类定义中),编译器就可以获得那个函数的函数体,而且大部分编译器喜欢在调用算法的模板实例化时内联那个函数。在上面的例子中,greater::operator()是一个内联函数,所以编译器在实例化sort时内联展开它。

它传了一个doubleGreater的指针,而指针的调用存在开销。

不过我实际测试来看,没有什么区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for (int i = 0; i < n; i++)  v.push_back(rand() % (n / 2));

// 6985ms
benchmark<100>([&v] () {
sort(v.begin(), v.end(), greater<int>());
});

// 6843ms
for (int i = 0; i < n; i++) v[i] = rand() % (n / 2);
benchmark<100>([&v] () {
sort(v.begin(), v.end(), [] (auto& lhs, auto& rhs) {
return lhs > rhs;
});
});

// 7134ms
for (int i = 0; i < n; i++) v[i] = rand() % (n / 2);
benchmark<100>([&v] () {
sort(v.begin(), v.end(), cmp);
});

把函数对象作为算法的参数所带来的不仅是巨大的效率提升。在让你的代码可以编译方面,它们也更稳健。当然,真函数很有用,但是当涉及有效的STL编程时,函数对象经常更有用。

条款47:避免产生只写代码

代码的读比写更经常,这是软件工程的真理。也就是说软件的维护比开发花费多得多的时间。不能读和理解的软件不能被维护,不能维护的软件几乎没有不值得拥有。你用STL越多,你会感到它越来越舒适,而且你会越来越多的使用嵌套函数调用和即时(on the fly)建立函数对象。这没有什么错的,但永远记住你今天写的代码会被某个人——也可能是你——在未来的某一天读到。为那天做准备吧

条款48:总是#include适当的头文件

  • 几乎所有的容器都在同名的头文件里,比如,vector在中声明,list在中声明等。例外的是声明了set和multiset,声明了map和multimap。
  • 除了四个算法外,所有的算法都在中声明。例外的是accumulate(参见条款37)、inner_product、adjacent_difference和partial_sum。这些算法在中声明。
  • 特殊的迭代器,包括istream_iterators和istreambuf_iterators(参见条款29),在中声明。
  • 标准仿函数(比如less)和仿函数适配器(比如not1、bind2nd)在中声明。

条款49:学习破解有关STL的编译器诊断信息

这里有一些应该能帮助你理解有关STL的编译器消息的其它提示:

  • 对于vector和string,迭代器有时是指针,所以如果你用迭代器犯了错误,编译器诊断信息可能会提及涉及指针类型。例如,如果你的源代码涉及vector::iterator,编译器消息有时会提及double指针。(一个值得注意的例外是当你使用来自STLport的STL实现,而且你运行在调试模式。那样的话,vector和string的迭代器干脆不是指针。对STLport和它调试模式的更多信息,转向条款50。)

  • 提到back_insert_iterator、front_insert_iterator或insert_iterator的消息经常意味着你错误调用了back_inserter、front_inserter或inserter,一一对应,(back_inserter返回back_insert_iterator类型的对象,front_inserter返回front_insert_iterator类型的对象,而inserter返回insert_iterator类型的对象。关于使用这些inserter的信息,参考条款30。)如果你没有调用这些函数,你(直接或间接)调用的一些函数做了。

  • 类似地,如果你得到的一条消息提及binder1st或binder2nd,你或许错误地使用了bind1st或bind2nd。(bind1st返回binder1st类型的对象,而bind2nd返回binder2nd类型的对象。)

  • 输出迭代器(例如ostream_iterator、ostreambuf_iterators(参见条款29),和从back_inserter、front_inserter和inserter返回的迭代器)在赋值操作符内部做输出或插入工作,所以如果你错误使用了这些迭代器类型之一,你很可能得到一条消息,抱怨在你从未听说过的一个赋值操作符里的某个东西。为了明白我的意思,试着编译这段代码:

    1
    2
    3
    vector<string> v; // 试图打印一个
    copy(v.begin(), v.end(), // string*指针的容器,
    ostream_iterator<string>(cout, "\n")); // 被当作string对象
  • 你得到一条源于STL算法实现内部的错误信息(即,源代码引发的错误在中),也许是你试图给那算法用的类型出错了。例如,你可能传了错误种类的迭代器。要看看这样的用法错误是怎样报告的,通过把这段代码喂给你的编译器来启发(并愉快!)自己:

    1
    2
    list<int>::iterator i1, i2; // 把双向迭代器
    sort(i1, i2); // 传给一个需要 // 随机访问迭代器的算法
  • 你使用常见的STL组件比如vector、string或for_each算法,而编译器说不知道你在说什么,你也许没有#include一个需要的头文件。正如条款48的解释,这问题会降临在长期以来都可以顺利编译而刚移植到新平台的代码

有经验的STL程序员发展出一项类似的技能。他们可以不假思索地在内部把比如std::basic_string<char, struct std::char_traits, class std::allocator >翻译为string。你也要发展这项技能,但在你能做到之前,记得你总是可以通过用更短的记忆术替换冗长的基于模板的类型名字来把编译器诊断信息降低到可以理解的东西。

条款50:让你自己熟悉有关STL的网站


Effective STL - 06 - Programming by STL
http://hebangwen.github.io/2024/03/17/Programming-by-STL/
作者
何榜文
发布于
2024年3月17日
许可协议