在此,把C++在不同编译器,即VC++与GCC(或LLVM)上的一些区别记录下来,以备查阅。
(注:以下VC++均是指VC++ 10.)
基本数据类型、const关键字
1. 基本数据类型的大小
Windows | macOS/Linux | |
wchar_t | 16 bits | 32 bits |
long | 32 bits | 64 bits |
影响:对程序在文件的保存和读取上可能会造成问题。如在Windows上保存文件,然后在macOS/Linux上读取,或者在macOS/Linux上保存文件,然后在Windows上读取。我们知道读取文件时,我们一般会使用到文件中的数据地址的偏移量来定位数据,对于读取32 bits和64 bits的整数,显然其造成的地址偏移量是不一样的。
2. const引起的虚函数重载相关的编译问题
我们知道子类可以重载(override)父类的虚函数,但如果我们在虚函数末尾添加一个const,则会产生一个全新的函数,而不是重载原有的父类的虚函数。
然而,如果在子类的虚函数末尾漏写了const,不同的编译器则有不同处理方式。这个区别需要谨慎对待,因为其造成的问题,不易被发现,甚至只能通过调试才能发现。我们知道在运行阶段(如测试、调试)发现问题,比在编译阶段发现问题,成本高出太多。如果在编译阶段发现问题,一般来说编译器会“强迫”程序员当即解决问题。
- VC++:会忽略漏写的const,不报编译错误
- GCC/LLVM:能正确地报出编译错误
C++多态
如果一个类中所有的虚函数为内联函数,那么:
1. 向下转型(dynamic_cast)
- VC++:运行dynamic_cast会得到期望的结果。
- GCC/LLVM:运行dynamic_cast可能不会得到您期望的结果。
原因分析:dynamic_cast依赖于类(class)的虚函数表地址(vtable: virtual function table)。如果把所有的虚函数内联,那么在某些情况下,虚函数表地址会被优化掉,所以造成dynamic_cast无法得出正确的结果。
2. 内联构造函数(Inline constructor)
- VC++:不管构造函数是否内联,在使用该构造函数时不会造成明显的区别
- GCC/LLVM:把构造函数内联在某些情况下会造成链接错误(link error)。我们知道链接错误比较难定位,特别是对于这种少见的链接错误,调查起来是比较耗时的。如果对C++了解不够深刻的话,甚至有无从下手的感觉。解决办法就是把内联的构造函数改成非内联的。
原因分析:构造函数可能会把虚函数表的地址作为一个隐藏的参数,就像"this"指针,也是一个隐藏的参数。在任何.cpp文件(编译单元)中使用一个类(class)的构造函数后,链接器(linker)必须知道这个类(class)的虚函数表的地址,否则链接失败。但是,这个vtable隐藏参数可能已被编译器优化掉了,所以链接器在链接时再也找不到这个参数了。
RTTI
在不同的编译器上,运行std::type_info返回的结果不一致,如下所示:
Class | ClassFoo |
VC++ RTTI Result | "class ClassFoo" |
GCC RTTI Result | "8ClassFoo" |
解决办法是写一个我们自己的type_name函数,把std::type_info的返回结果进行如下修正:
template<class T>
static const char* type_name(T& t)
{
const std::type_info& ti = typeid(t);
if (strncmp(ti.name(), "class ", 6) == 0)
{
return ti.name() + 6;
}
else
{
return ti.name();
}
}
模板(template)的参数
我们知道,使用模板以后,在编译阶段需要对模板的参数进行解析,最终生成所需要的函数:
- VC++:模板是在使用时进行解析的,所以模板实参的头文件可以不包含。
- GCC/LLVM:模板是在声明及定义时进行解析的,所以模板实参的头文件必须包含。
在调用(call)模板(参数)时:
- VC++:不需要在调用(call)的前面加上typename
- GCC/LLVM:必须在调用(call)的前面加上typename
注:我们可以尝试使用编译参数“-fdelayed-template-parsing”来解决以上模板编译解析的问题,但可惜的是它与C++11 runtime library有冲突,所以并不建议使用它。
STL
STL的容器(container)很多都提供erase(iterator)方法,如std::map::erase(iterator)等。但是不同的编译器提供的实现有一个比较有趣的区别:
- VC++:erase方法有返回值,它返回的是下一个有效的迭代器(iterator)
- GCC/LLVM:erase方法没有返回值
这个区别造成的问题是,在循环遍历容器时,调用了erase(iterator)方法之后,如何正确的移动到下一个迭代器,比如这段代码在GCC/LLVM上有编译错误:
for (iterator it = container.begin(); it != container.end(); it++)
{
// do something with it...
if (needToErase)
{
it = container.erase(it);
}
else
{
// do something with it...
}
}
可以用以下方法解决:
for (iterator it = container.begin(); it != container.end(); )
{
// do something with it...
if (needToErase)
{
container.erase(it++);
}
else
{
// do something with it...
it++;
}
}
编译速度
我的体会是在macOS的上使用LLVM编译C++代码,比在Windows上使用VC++的速度快很多。
代码行数(Lines of code) | 大于一百万行 |
Windows(多台电脑集合编译,20~50个CPU可用) | 1小时 |
Mac Server(一台电脑,一个12核CPU,内存12GB) | 20分钟 |
异常处理(Exception handling)
对于操作系统级别的异常处理,不同编译器的支持也有区别:
- VC++:提供__try / __except / __finally,用法跟C++的try / catch / finally类似,但是能捕获一些操作系统级别的异常。比如最常见的对空指针的访问造成的异常,如果不处理则会直接导致程序崩溃。
- GCC/LLVM:需使用UNIX signals
注:对空指针的访问造成的异常不是C++语言级别的异常,所以使用C++的本身的try / catch / finally是捕获不到该异常的。C++的本身的try / catch / finally能捕获的异常是调用C++本身的throw所抛出的异常。
结语
总的来说,给我的感觉是,GCC/LLVM会更严格地按照C++语言的标准来检查编译问题,而VC++会容忍并支持一些标准之外但有用的语法糖。