TAG / c++

C/C++ 中遍历 Lua table 完整版

在 C/C++ 中遍历一个 Lua table用 lua_next 来实现,这个大家都知道。然而,我却看到很多文章在示范 lua_next 时都只是点到为止,或绝口不提如何获取 key 值,或直接定义该 table 的 key 都是非匿名的,从而简单粗暴地使用 lua_tostring 来获取值。

仔细看看,Lua manual 里对 lua_next 的说明中最后有一句很重要的话:

While traversing a table, do not call lua_tolstring directly on a key, unless you know that the key is actually a string. Recall that lua_tolstring changes the value at the given index; this confuses the next call to lua_next.

遍历 table 的过程中不要直接对处于 -2 位置的 key 做 lua_tostring 操作(还记得这个函数说了它会原地修改栈上的值的吧),除非你确信现在这个 key 就是字符串类型,否则下一次执行 lua_next 就等着意外吧。

好吧,来个完整版,其实不就是把 key 拷贝一份到栈上,然后对这份拷贝进行取值么[1]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <lauxlib.h>
#include <lua.h>
 
void traverse_table(lua_State *L, int index)
{
    lua_pushnil(L); 
    // 现在的栈:-1 => nil; index => table
    while (lua_next(L, index))
    {
        // 现在的栈:-1 => value; -2 => key; index => table
        // 拷贝一份 key 到栈顶,然后对它做 lua_tostring 就不会改变原始的 key 值了
        lua_pushvalue(L, -2);
        // 现在的栈:-1 => key; -2 => value; -3 => key; index => table
        const char* key = lua_tostring(L, -1);
        const char* value = lua_tostring(L, -2);
 
        printf("%s => %s\n", key, value);
 
        // 弹出 value 和拷贝的 key,留下原始的 key 作为下一次 lua_next 的参数
        lua_pop(L, 2);
        // 现在的栈:-1 => key; index => table
    }
    // 现在的栈:index => table (最后 lua_next 返回 0 的时候它已经把上一次留下的 key 给弹出了)
    // 所以栈已经恢复到进入这个函数时的状态
}
 
int main()
{
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);
 
    // 假设现在栈上已经有了一个 talbe,内容为 {one=1,[2]='two',three=3},位置在 top
 
    traverse_table(L, -1);
 
    return 0;
}

[1] Iterate through Lua Table


神奇的 bool

今天跟同事一起debug,发现一个神奇的事情,在 VC10 的调试器里,两个 bool 变量都是 true,但是程序执行时却走了它们不相等的路径。来看代码:

1
2
3
4
5
6
7
8
9
10
class Foo
{
public:
    Foo(void) {}
 
    bool Bar(bool flag) { return flag == _flag; }
 
private:
    bool _flag;
}

断点下在 Bar() 函数内,调试器显示两个变量都是 true,但是返回的值却是 false!没错!程序 rebuild 过了,眼镜也擦过了!

我会告诉你 Foo::_flag 忘了初始化了么?

噢,对啊。但是,当时调试的时候,他们都是 true 啊!!为什么不相等?让我们来翻看 C++ 标准文档。

关于 bool 类型,3.9.1/7 里是这么说的:

Types bool, char, char16_t, char32_t, wchar_t, and the signed and unsigned integer types are collectively called integral types.

说白了,bool 也是个数。(我一直以为 bool 类型会什么特殊优化)

关于 sizeof(),5.3.3/1 里是这么说的:

sizeof(char), sizeof(signed char) and sizeof(unsigned char) are 1. The result of sizeof applied to any other fundamental type (3.9.1) is implementation-defined. [ Note: in particular, sizeof(bool), sizeof(char16_t), sizeof(char32_t), and sizeof(wchar_t) are implementation-defined. *74 ]
*74: sizeof(bool) is not required to be 1.

除了 char 的大小,其他所有基础类型的大小都是由实现决定。由!实!现!决!定!(C++ 你就把我往死里坑吧)

而就我所知的编译器(其实我只知道两个),bool 默认都是一个字节。所以从内存的角度来说,它完全可以表示 254 个非“零”值。那么,一个值为 1 的 bool 变量和一个值为 100 的 bool 变量,他们相等么?上面都说了,bool 类型是数!字!类!型!所以,bool 变量间的比较,其行为和其他数字类型的应该保持一致,也就是进行纯粹的数值比较(true、false 这两个关键字应该作为 1 和 0 的别名,而不是一种新的语义)。

微软似乎也发现了这个秘密(我有告诉过他们么?),所以在 VS2012 里,调试器会把非 0 且非 1 的 bool 变量的具体值显示出来,而不再是简单的判断一个 bool 值是否是非零了。

泪奔吧,骚年!


游戏引擎设计 之 内存管理框架

内存管理对于大型游戏来说是至关重要的一环,这里我们所说的“内存”指的是动态分配的内存。一个好的内存管理框架,能显著提升程序执行效率,也能大大提高内存问题的调试效率。所以,我们设计内存管理框架时必须能够满足以下两大需求:

  1. 自定义内存分配器
  2. malloc/free(包括间接由new/delete等调过来的)等函数本身的开销其实并不大,但由于其功能太过基础了,没有任何策略可言,从而导致反复分配释放带来的开销、大量内存碎片以及多线程内存分配引起的效率问题。所以自定义一个高效的内存分配器就显得必不可少了,对于一般情况,我们可以使用 nedmallocjemalloc 等第三方多线程内存分配器,也可以根据具体需求自己实现一套(例如 Nebula3 中的内存池以及 Heap 对象机制),甚至是基于栈的动态内存分配。而具体的内存分配/释放策略是另一个话题了,以后新开一篇讨论。

  3. 内存统计及内存泄漏跟踪
  4. 虽然 CRT 有 dump 内存泄漏的功能,但最大的问题就在于——仅限于 DEBUG 环境下,而游戏由于其实时计算的复杂性,在后期很多情况下开 DEBUG 模式已经无法满足正常调试的需求了,所以 Release with debug info 模式其实才是我们更常用的,这样我们就必须自己实现内存统计及泄漏跟踪的功能,甚至提供对动态内存泄漏(运行时大量已无用的内存未释放,直到游戏退出时才一并释放,最常见的就是由智能指针引起的动态内存泄漏)的检查。

CONTINUE READING »


代码之初,性本丑?(下)

代码之丑(六)——分家的声明和使用

变量声明的就近原则,除了该文中提到的几点好处之外,还有一点就是能够提高发现声明了却未使用的变量的概率。别小看这个问题,以为编译器会给优化掉,我就碰到过一坨很烂的代码,这个函数里做了大量的计算,比如先经过复杂计算得到 a,然后 a 经过计算得到 b,b 再经过计算得到 c,c 再计算后得到 d,在一团乱麻的代码里,谁也没发现,其实 d 根本没被用到,函数最后直接拿 e 返回了……

代码之丑(七)——你的语言

这篇有点各抒己见的意思,不过我觉得有一点是明确的,学一门语言,主要是学习它的思维方式,而不仅仅学会语法就算完了。至于具体怎么用,这是每个 team 自己定编码规范的事了。

CONTINUE READING »


代码之初,性本丑?(中)

代码之丑(三)——switch陷阱

可能是因为篇幅关系,这篇文章里的例子不够有代表性,而且还有越改越没有可读性和可维护性之嫌。曾经我在公司的一个比较有历史的程序里看到过一段伟岸的 switch/case 代码,每个 case 下的代码各不相同且长短不一也就算了,可能你无法想象的是,它里面竟然有几千个 case,总共 10000+ 行的代码!你没看错,一个函数一万多行!这就是从一开始放任 switch 陷阱,经过漫长地演化得到的后果。

其实我并不认为该把 switch/case 当作唯恐避之不及的恶魔,它只是一把双刃剑,就像没必要一味地反对使用 if 一样。在正确的环境下正确使用它的话,只会提高可读性,但如果滥用它(就像 C++ 很多其他特性那样),那的确是一场噩梦。

对于本身结构很简单的逻辑,使用 switch/case 能很好地保持其可读性(显然比一堆的 if/else 好,更不用说多态了),而且也正因为其逻辑简单,条件不多,也不会对系统性能产生明显影响(代码并非越短越快就越好,它首先是给人看的,顺便附带了能让机器执行的功能)。而对于我上面提到的超大的逻辑结构,倒是可以用多态或者状态机来解决,定位问题显然会方便不少(假如这个逻辑架构本身没法改变的话)。

CONTINUE READING »


代码之初,性本丑?(上)

我是一个很在意代码质量的人,因为我相信软件开发,细节决定成败(追求细节和拥有宏观视野本身并不矛盾,而且这是另一个话题了)。虽然我并不赞成将团队甚至公司中的每个人的编码风格都硬性规范到某种统一格式,那样不仅会扼杀程序员们的创造力,而且从成本收益来看太不划算了。但是,这并不意味着代码就可以随心所欲地写,所谓“规范”不该是“法律”上的,但一定是“道德”上的:你现在写的代码,要对半年后的你负责,更要对团队里其他同事们负责,甚至可能还有你的客户。而这种责任,不仅仅是代码的功能完整度,更包括可读健壮效率

这几个月有幸参与了我们游戏提升稳定性和优化性能的工作,review了很多代码,这期间时长被各种“富有创造性的”代码雷得外焦里嫩的。碰巧正好看到了《代码之丑》系列文章,感触颇深,有些观点也有些自己的看法,记录下来,与大家探讨。

CONTINUE READING »


#include

译自 zeuxcg 的《#include 》(墙外),有删改。请勿转载。
若不听劝告非要转载,请注明出处,谢谢。

我们没法摆脱 C++,至少短期内不太可能。我真希望 C++ 里没有那么多恶心的特性,但目前也没有真正能取代 C++ 的玩意。现代编程语言往往采用大宗编译(bulk compilation)和/或智能链接器,但 C++ 仍然受困于头文件分离的设计思想(当然从另一方面说,C++ 的这种设计也使得其构建过程可以是增量的、高度并行的),使得鱼与熊掌不可兼得。 这种分离头文件并保持代码清晰的策略很显而易见,但我却很纳闷为啥还有那么多人没搞明白到底该怎么使用头文件,希望这篇文章能够进一步理清这团乱麻。本文也适用于 C,但有幸使用其他语言的同学们请绕行。

包含头文件的问题在于预编译器很笨:你的代码告诉它要包含某个文件,那么它就会“递归地”包含整个文件内容;如果你不想包含那个文件却又想用该文件中定义的一个符号呢,那你只有面对编译错误的份了;但你如果把所有头文件都包含进来,哼哼,编译的时候你就去数星星吧。

CONTINUE READING »


微软 STL lower_bound() 在 DEBUG 下的诡异编译错误

众所周知,在 STL 中,对于有序的 vector 容器,使用 binary_search、lower_bound/upper_bound 等搜索算法要比直接 find 高效得多。但是由于各个 STL 实现版本没有统一的标准,在 DEBUG 环境下各自的校验机制千差万别,这就导致可能出现一些让人郁闷的情况,比如这次的主角,微软的 STL。

CONTINUE READING »


避免在 C++ 类结构中出现私有虚成员函数

最近在重构 C++ 代码时突然想起,如果一个基类的虚成员函数被设为 private,有没有意义?又是否合理?

当然,有其一定的意义,那就是不希望子类在其他地方调用父类的这个函数,包括在子类的实现中;如果需要这个功能,应该使用其 public 的接口去使用该功能。而子类可以提供自己的实现,以提供多态。但是,如果子类觉得需要,还可以把这个 private 的虚成员函数重定义成 protected(虽然这会让人迷惑),从而使子类的子类们调用它。

有同学可能会问,如果是一个 private 的纯虚成员函数(语法上当然合理),那语义合理么?嗯,我觉得这的确是个问题——也许是为了告诉写子类的其他同学,这个虚函数,我不希望你们在除了基类已有的接口里调用之外还来用——仅仅是一个道德约束?!

嗯,C++ 就是灵活得过头了,什么都让程序员自己去把控,可是别忘了,“太多的选择比没有选择糟糕得多”。所以,我决定,为了自己也为了别人不犯迷糊,避免使用 private 的虚函数,private 的成员函数仅包含当前类自己使用的函数。

BTW,类的成员变量正好相反,能设为 private 的尽量不要 protected 更不要public,否则后期维护,嗯嗯,就太痛苦了。


乱用 STL 是地狱

根据《Effective STL》条款21中的例子,建立一个比较类型为 less_equal 的 set 容器:

    set< int, less_equal<int> > s;

然后连续插入两个10:

    s.insert(10); // 10a
    s.insert(10); // 10b

会得到什么?

在debug下,可能会给出一个assert报比较符号不合法,第二次插入失败,但在release下,这个动作很可能是未定义的,而通常的结果是,set中存在了两个键值同为10的项,也就是说,set被悄声无息地变成了multiset!太可怕了!

所以,为正确的容器挑选正确的比较函数,很重要。用好 STL 其实并不容易,用错了不仅执行效率狂低,而且还可能出现这些难以想象的意外……


Previous Posts »