STL 悲剧之 for_each
这篇文章谈一个我使用 STL 中 for_each 的负面心得. 我对这个东西在当前 C++ 语法下约束是否能广泛使用持有怀疑态度, 至于能否替代所有的 for 循环, 我持完全否定观点! 如果您发现文中提及的这档子事情本质上是我不了解 STL 而自寻死路, 请不吝赐教, 在下方回复, 在下感激不尽. 在 Google 中输入 "for_each" 然后猛击 "I’m feeling lucky", 这时我幸运地跳转到大名鼎鼎的 sgi 的网站里, 页面中有个例子 template struct print : public unary_function { print(ostream& out) : os(out), count(0) {} void operator() (T x) { os << x << ' '; ++count; } ostream& os; int count; };
int main() { int A[] = {1, 4, 2, 8, 5, 7}; const int N = sizeof(A) / sizeof(int);
print P = for_each(A, A + N, print(cout)); cout << endl << P.count << " objects printed." << endl; }
从泛型党的视角看来, 这例子非常不错, 完备而规范地演示了 for_each 的用法. 从此泛型党成了大赢家, 除了 for_each 内部, 外面的代码再也不需要 for 这个关键字了. 而那些说写 C++ 代码像谈话一样简短, 基本上泛型编程完全不及格的程序员, 在写上面功能的时候一般会这样做 int main() { int A[] = {1, 4, 2, 8, 5, 7}; const int N = sizeof(A) / sizeof(int);
int i; for (i = 0; i < N; ++i) cout << A[i] << ' '; cout << endl << i << " objects printed." << endl; }
在喷这个改编版的丑陋之前, 不妨先淡定下来, 想想这个例子到底想做什么, 无非是给个数组 (或者其它什么容器) 把每个成员输出 (或者做个什么其它简单的操作), 并记录被操作的元素个数. 然而, 完成这个操作的代码量与它的功能相比, 臃肿得令人发指. 所以, 在用了几次 for_each 之后, 我完全丧失了当初的激情, 变得极其懒惰, 不愿意把简单的操作抽取到函数外部构造成一个对象然后传递给 for_each ; 在维护自己以前写的代码时, 也会偷偷把 for_each 改回 for 循环, 因为有的时候我实在不知道伙伴们把我弄出来的那个函数对象类型给维护到哪里去了. 当然, 我并不认为这是 for_each , 或者 STL, 或者泛型的错. 在我心目中, 泛型思想是风骚的, for_each 的设计是超前的, 问题出在 C++ 语法这个悲剧帝身上, 或者说, 硬要用 C++ 这个传统的面向过程加面向对象语言的语法来包装对象, 来实现类似高阶函数这种函数式编程中的特性, 是悲剧的根源所在. 不仅是 for_each , 连 find_if 等类似的 STL 函数都差不多一样的下场, 与其构造一个精妙绝伦的函数对象类型, 程序员还不如老老实实写个破循环来得实在. 一个振奋人心的消息是, C++ 新标准即将发布了, 它会支持闭包对象, 那样的话, for_each 以及其它函数就不会像现在这么处境尴尬了.
Created at 2010-04-17 22:17:47
Permanent Link:
/p/81/
|
Post tags:
C++
for_each
STL
|
STL set 悲剧指导
这篇文章谈一个我使用 STL 中 set 容器的负面心得. 是的, 我已经被这丫的杀得超神了, 快来人阻止它吧! 如果您发现文中提及的这档子事情本质上是我不了解 STL 而自寻死路, 请不吝赐教, 在下方回复, 在下感激不尽. 最坏对数时间插入, 删除, 查询, 迭代遍历, 这些听起来都无比诱人, 它们由 STL 中的 set 容器鼎力支持. 然而, set 的只读制度是非常龌龊的, 简而言之, 只要你敢往这个坑里面放, 你就得接受它们以后再也无法修改的命运. 比如下面这段例子 #include
#include
using std::set; using std::string;
struct student { string name; int grade;
student(string const& name_, int grade_) : name(name_) , grade(grade_) {}
bool operator<(student const& rhs) const { return name < rhs.name; }
bool operator==(student const& rhs) const { return name == rhs.name; } };
int main(void) { set students; students.insert(student("Li Lei", 5)); students.insert(student("Han Meimei", 5)); students.insert(student("Jim Green", 5));
for (set::iterator i = students.begin(); students.end() != i; ++i) { ++(i->grade); } return 0; }
student 类的 key 是 name , 跟 grade 没有关系, 原则上来说, 修改后者并不会破坏 set 的存储结构. 然而, 编译器一棒子打死, 不许改, 除非剩下的成员全部 mutable 修饰. 只读制度悲剧的根源在于, set 所谓的 key 撑死只是个假象, value_type 这玩意儿就是 key_type 本身. 伪 key 导致的不仅仅是不能改, 重要的是还不能查! 看看 set::find 函数的参数, 要的又是阴魂不散的 key_type . 这意味着什么? 意味着 Han MM 同学报出她名字的时候, 还查不出她几年级, 而必须要利用她的名字, 伪造一个充气娃娃放进去才能找到! 看到这里我就败了, 这明摆着就是不让我用 set , 让我转投 map 么? 一个也许可行的方案是 #include #include
using std::map; using std::string;
struct student_periphery { int grade; };
map students;
这样建模的话也是够狗血的, 如果哪个函数拿着一份 student_periphery 问怎么查出学生姓名, 那就更悲剧了.
Created at 2010-03-12 17:34:52
Permanent Link:
/p/79/
|
Post tags:
C++
set
STL
|
Makefile 与 C 语系头文件依赖自动解析
h1. 如何获得源文件依赖的头文件? 这一部分内容与 Makefile 毫无关系, 它由编译器完成. 以 g++ 为例, 可以尝试使用以下方法输出 .cpp 文件依赖的头文件 g++ -M [your cpp file]
乱糟糟的, 不是吗? 这是因为 g++ 把依赖的库文件也给列出来了. 忽略库文件, 则使用这样的命令 g++ -MM [your cpp file]
显然, 这些东西扔到标准输出是毫无用处的, 可以对其进行重定向 g++ -MM [your cpp file] > [some file]
但是这还是有问题, 只把依赖关系放进文件当然不行, 编译源文件的指令还没弄出来呢. 下面就得另外想办法了. h1. 怎么添加编译指令并运行它们? 添加指令本质上就是追加文件内容, 方法有难有简单, 最简单的无非是写个脚本 echo 并追加重定向到文件中. 在这篇粗陋的文章里就用这招了. 好, 写个 Makefile 脚本 [target.o]:[your cpp file] ....g++ -MM $< > Makefile.tmp ....echo "....g++ $< -c" >> Makefile.tmp ....make -f Makefile.tmp
这里的四个点号 (....) 表示一个制表符, 嗯, 是的, Makefile 只能用万恶的制表符来缩进, 而且还必须缩进; Makefile.tmp 一定不要是当前的 Makefile, 要不然就出现文件覆盖的悲剧了... 结果就是, 一旦 make 到这个目标, 它会生成一串依赖关系和构建方法到另一个 Makefile, 然后把编译这等脏活累活都甩给那个倒霉的家伙. h1. 怎么转移目标? 如果上述目标仍然写成 xxx.o : xxx.cpp , 那么如果仅仅是依赖的头文件被修改了, 这个目标仍然不会被 make 到. 一个很挫的方法是, 不要把目标定为 xxx.o 文件, 相反, 弄成一个永不会生成的目标比较好, 比如叫 xxx.d 这个名字. 下面上一个完整的例子. 这个例子中有个叫做 DEP 的 Makefile 变量没有被声明过, 嗯, 这将是你的事情. 请将所有需要编译并连接的 cpp 源文件列个表, 然后修改它们的后缀变成 d, 那么 DEP 的值就是这个表. Code Snippet 0-0
CXX=g++ CXXFLAGS=-Wall -pg MKTMP=Makefile.tmp
all:object
object:$(DEP) ....$(CXX) $(CXXFLAGS) *.o -o $@
%.d:%.cpp ....$(CXX) -MM $< > $(MKTMP) ....echo "....$(CXX) $< $(CXXFLAGS) -c" >> $(MKTMP) ....make -f $(MKTMP)
clean: ....rm -f *.o ....rm -f test.out ....rm -f $(MKTMP)
再次提醒, 制表符出没注意. (可利用编辑器的全文替换, 4 点换成 1 制表符) 假如现在目录下有 main.cpp, class0.cpp, class1.cpp, 那么定义 DEP=main.d class0.d class1.d
将这句话插进上述源码中的开头位置然后 make 即可. h1. 怎么收拾掉乱七八糟的输出? 接脏活的临时工经常会反馈出一些无聊的信息, 比如进入这个离开那个的, 如果希望保持控制台的清洁, 那么把不想看的都扔进垃圾桶吧 make > /dev/null
不要担心看不到 warning 和 error, 它们是通过 stderr 输出的, 而上述重定向只会干扰 stdout.
Created at 2009-12-07 20:34:35
Permanent Link:
/p/63/
|
Post tags:
C
C++
Makefile
|
编译期数值计算程序设计思路
阅读这篇文章需要了解基本的 C++ 模板语法规则, 以及模板的泛化, 偏特化, 当然还包括完全特化这些基本概念. 这篇文章将给出基于 C++ 模板的 Metaprogramming 程序设计中需要注意的事项以及渐进式设计思路. 0.0.0 基础这一段介绍 Metaprogramming 基础, 包括模板模式匹配规则, 使用模板实现循环或分支的基本方法. 对 Metaprogramming 已经有了解的同学可以跳过这一段. 本质上来说, 现代计算机的核心任务是数值计算, 周边任务是 I/O. 现代几个主流的编程语言, 如 C/C++/Java, 它们的语法组织方式与现代计算机的主流体系结构是 "平行" 的, 即, 语句的先后顺序表示它们的逻辑顺序, 而这种顺序与人的一贯思维顺序又是相同的 (在计算机中表达式的计算顺序会与书写顺序稍有不同, 但却很符合人类的思路); 可以说这是什么样的爹生什么样的崽, 什么样的人造什么样的语言和计算机. 然而, 在 C++ 模板世界中, 这个规则被打破了. 从分类上来说, Metaprogramming 更类似于函数式编程, 而不是传统的 C++ 程序设计. 在这个异世界中, 程序员不被允许使用循环或者分支语句, 包括函数返回语句都不允许: 在函数式编程中起码还有函数返回, 而在 Metaprogramming 中, 由于一切函数都没有被调用 (还在编译呢), 因此一切的值都谈不上返回, 它们只是存在于模板符号表中, 一旦编译结束便烟消云散. 当然, 没有循环, 分支这些编程语言中的基本元素, 并不意味着程序员只被允许设计顺序程序. 只不过, 在这里, 一切分流动作都被转化为了另一个语言特性: 模式匹配. (这一点与函数式编程非常相似) 下面一段代码展示了模式匹配的威力: Code Snippet 0-0
#include
template struct limit_to_2 { static unsigned const R = 2; };
template <> struct limit_to_2<1> { static unsigned const R = 1; };
template <> struct limit_to_2<0> { static unsigned const R = 0; };
int main(void) { std::cout << limit_to_2<0>::R << std::endl; std::cout << limit_to_2<1>::R << std::endl; std::cout << limit_to_2<2>::R << std::endl; std::cout << limit_to_2<3>::R << std::endl; return 0; }
模板 limit_to_2 为传入的整数设置了一个上限, 当传入模板的参数为 0 或者 1 时内部定义的 R 值与参数相同, 否则将 R 设置为 2. 这看起来很像是 switch-case 语句, 0 和 1 分别是两个 case, 而未特化的模板则是 default. 但是, 这样做有一些不方便, 如果所设的上限非常高, 那岂不是要为低于限制的情况写很多特化? 所以引入分支是非常必要的. 下面这个例子展示了如何利用模式匹配来实现编译期分支:
Created at 2009-12-02 12:00:11
Permanent Link:
/p/50/
|
Post tags:
C++
Metaprogramming
Template
|
泛型编程中的策略类
在编译期想要组合策略, 除开人肉内联不算, 有很多方式. 比如弄很多很多的 bool 参数 template
struct just_another_container;
这看起来就是一个参数灾难, 如果某个地方出点小的模板编译错误, 哼哼~ 当然, 另一种简单的替代方案就是把所有的 bool 整合在一起, 形成一个位段 template struct just_another_container;
这种情况实际上缺点更多, 比如维护位域会很麻烦, 有可能在这个声明之前有这些枚举 enum { ALLOW_DUP_MASK = 1, SORT_ELE_MASK = 2, CHECK_OUT_OF_RANGE_MASK = 4, };
考虑这个容器有个 insert 函数, 它很可能需要考虑是否允许重复元素 template void insert(element_type e);
而令人郁闷的是, 在 C++ 中不允许给函数偏特化, 也就是说这样写 template <> void insert<0>(element_type e); template <> void insert(element_type e);
是无法编译的! 要绕过这道坎, 得把 insert 函数放入一个模板结构体 template struct insert_s; template <> struct insert_s { static void insert(just_a_container& container, element_type& e); };
这样做代价高昂, 首先, insert 被当作孤儿一样的抛弃, 那么类中的任何非 public 成员都不再对它开放, 当然, 为了这么个简单的小玩意儿声明 friend 也是可行的, 但是多了不觉得烦吗? 另外, 在调用策略行为进行特化时一般只针对特定的位, 那么将该策略位从组合策略中分离出来, 代码中将充斥着这样的运算 Policy & ALLOW_DUP_MASK Policy & CHECK_OUT_OF_RANGE_MASK
void another_member_function() { element_type ele; // wanna call insert function of policy = Policy insert_s<*Policy & ALLOW_DUP_MASK*>::insert(*this, ele); // ... }
这样做非常麻烦, 如果某个地方漏了 & 或者跟错误的掩码进行了运算, 哼哼~ 不过, 泛型程序设计有自己的方法, 那就是, 多继承! 不要吓到了, 是的, 在 C++ 远古时代, 多继承被认定为獠牙猛兽. 不过那都是跟 C++ 运行时多态相关的. 步入泛型新纪元的 C++ 又迎来了多继承第二春, 它可以显著改善上述问题. 将上面的声明编程 template <*typename Policy*> struct just_another_container;
struct policy_base {}; struct allow_dup : public policy_base {}; struct sort_ele : public policy_base {}; struct check_out_of_range : public policy_base {};
组合策略, 只需要声明一个策略类型继承想要的策略, 并把它传入容器就能达到目的, 比如 struct *my_policy* : public allow_dup, public sort_ele {}; just_another_container<*my_policy*> my_container;
你可能想到, 还是那个 insert , 使用策略不是一样麻烦吗? 假如要写出这样的代码
Created at 2009-11-22 17:56:30
Permanent Link:
/p/36/
|
Post tags:
C++
Generic Programming
Template
|
1
Page 2
|