C++ 关键字浅谈

  这里有一个游戏:要求写一个符合C++标准的程序,包含至少十个连续而且不同的关键字。连续是指不能被标识符、运算符、标点符号分割。注意这里的“不同”要求,别想用

int main() { return sizeof sizeof sizeof sizeof sizeof sizeof sizeof sizeof (int); }

这个交卷,而且这个可以任意长。动动脑经,应该是可以想出来的。我们从很久很久以前(long long ago)开始吧,

unsigned long long int ago;
const volatile unsigned long long int ago;
extern const volatile unsigned long long int ago;
extern inline const volatile unsigned long long int f();
template extern inline const volatile unsigned long long int operator *(const Type&);
template extern inline const volatile unsigned long long int operator and(const Type& lhs, const Type& rhs); // 如果允许运算符&变成关键字and

我们强调了不同的关键字,所以只好用 long int 而不是 long long int
typeid 也可以级联,与 sizeof 不同的是,必须添加括号。如果参数是表达式而非数据类型,sizeof 可以不用加括号。new 则没有这个限制。

#include <typeinfo>

int main()
{
    const std::type_info& info = typeid(typeid(typeid(typeid(int))));
    return 0;
}

C++规范里还要求使用 typeid 关键字的时候必须 #include <typeinfo>,否则任何使用到的地方都是 ill-formed。new 有时候也需要 #include <new> 头文件。

sizeof new const volatile signed long int();

限定不同的关键字,目前最好的答案是:

int main()
{
    if(false);
        else
    do
        throw sizeof new const volatile signed long int();
    while(false);
    return 0;
}
spoiler alert

 

  回到正题,C++ 诞生于1983年,现在有很多的关键字了,这里有详细的列表。


  C++ 的一些运算符和标点符号需要 ISO 646 的代码集之外的字符:{, }, [, ], #, \, ^, |, ~。要能够使用不存在这些字符编码的字符集(如德国的 DIN 66003),C++定义了两种替代方案:额外的关键字对应这些操作符,使用 ISO 646 兼容的字符构成的特殊的两元组或三元组解释成一个非 ISO 646 字符。

首选 && &= & | ~ ! != || |= ^ ^=
替代方案 and and_eq bit_and bitor compl not not_eq or or_eq xor xor_eq

使用两元组和三元组可能会遇到一些奇怪的问题,而且也影响代码阅读。C++标准打算在C++17版本废除掉三元组符号 ??< ??> ??( ??) ??= ??/ ??' ??! ??- <: 会被替换成 [ 符号,于是 std::vector<::std::string> 会被错误的当成 std::vector[:std::string> 对待。现在的键盘都有这些符号,所以不用用他们的替代符号。毕竟符号比字符串易懂,想想数学运算中的加减乘除符号如果用 add/subtract/multiply/devide 英文替换表示,读起来就不舒服,这也是为什么C++引入操作符重载,矩阵的运算可以直接写 (A+B)/(C*D); 而不是 divide(add(A, B), multiply(C, D));

 

  基本数据类型 void bool char wchar_t short int long unsigned float double,布尔值 true false,以及 C++11 新增的 char16_t char32_t nullptr 关键字。
char、signed char、unsigned char 是不同的类型,这个需要注意一下。GCC 编译的时候,可以用编译选项 -fsigned-char 或 -funsigned-char,它们分别将 char 指定为 signed char 或 unsigned char。很多程序直接写char,希望它是有符号的或者无符号的。程序员喜欢简短,跟 int 关键字一样,不写就默认有符号的类型;但有时候处理二进制流,表示成[0, 256)区间的整数。
  看到这个表格,原来整形有这么多。其实我更喜欢用简短的 uint8_t/int8_t/.../int32_t 等。你会看到很多代码工程都自己定义了一套整形数据,避免 16位整形的程序移植到32位系统、32位整形的程序移植到64位系统 出现问题。而且C++ 对关键字的修饰顺序也没有要求,这让一些程序员甚是纠结。

GNU 的 STL 库也有这种顺序的 long unsigned int
#ifndef __SIZE_TYPE__
#define __SIZE_TYPE__ long unsigned int
#endif

  true/false 的类型是 bool,那 nullptr 的类型是什么?

typedef decltype(nullptr) nullptr_t;

这种类型定义妙不可言,也横空推出新的关键字 decltype。如果两个重载的函数 fun 可以接受不同的指针类型,比如 void fun(int*); void fun(float*); 那么编译 fun(NULL) 会有二义性错误。不妨再定义一个 std::nullptr_t 空指针类型的函数 void fun(std::nullptr_t nullp),这次 fun(NULL) 编译失败但是 fun(nullptr) 可以通过。

  wchar_t 用来表示一个 Unicode 字符集中的编码,在 Windows 上是 UTF-16,类 Unix 系统上是 UTF-32。sizeof(wchar_t) 是 implementation defined,移植性差。一般在Windows上是2个字节,在类 Unix 系统上是4个字节。Windows 接受了宽字符并使之成为标准,可以看到许多Windows API 有两个版本,functionNameA 和 functionNameW 分别对应ANSI版本和宽字符版本。这篇博客列举了使用wchar_t可能犯的错误。

  C规范中并没有写明宽字符 wchar_t 的具体类型,与编译器实现相关,可能8/16/32位,也可能是signed 或者 unsigned。关键在于选用的编码字符集,注意字符集(Charset)与字符编码(Character Encoding)的区别,ASCII/GBK/BIG5/GB18030 是字符集,Unicode 是字符集。当时各个国家都做了自己的编码方案,中国有 GBK/GB18030/BIG5 等,这在国内没有问题,但是网络遍及世界各地,外面的人访问就出现乱码。于是 Unicode 应运而生。Unicode 字符集有 UTF-7/UTF-8/UTF-16/UTF-32 这几种编码。
推荐使用 Unicode 字符,不推荐使用 wchar_t,取而代之使用固定长度的 char16_t/char32_t 类型。

ISO/IEC 10646:2003 Unicode 标准 4.0 里讲到:
"The width of wchar_t is compiler-specific and can be as small as 8 bits. Consequently, programs that need to be portable across any C or C++ compiler should not use wchar_t for storing Unicode text. The wchar_t type is intended for storing compiler-defined wide characters, which may be Unicode characters in some compilers."

 

 

  C 语言关键字 int 是用的最多的,我经常觉得 short/long 的修饰让 int 显得多余。short int 与 short 语义一样,long int 与 int 语义一样。long 与 short 是相对来讲的,于是又出现了 long long int 这一类型。long long 是1995年提议加没加成,C++ 因为 C 没有加所以也不肯加入。很多编译器都自己实现了,10年之后,大家觉得有必要标准化了。(说不定将来某一天,整数的计算范围需要扩充到 128 位,于是要唤出 long long long long int?好囧)个人觉得,程序语言诞生的时候,就应该尽量让关键字语义正交,虽然标准规定了 sizeof(short) <= sizeof(int) <= sizeof(long),但是很多编译器还是将 int 与 long int 等同看待。C 语言可移植好不由争辩;Java 很聪明,直接规定了各自的长度。short/int/long 分别为 2/4/8 个字节。
  有一个头文件 <stdint.h> 专门定了各种整形数 int8_t/int16_t/int32_t/int64_t 等,还有 unsigned 类型。其实 short/long/signed 都是形容词,int 是名词,所以声明整形没有以 int 结尾也是对的,与其写 long long int,不如写 long long,甚至 int64_t。我也想过如果 sizeof(int) == sizeof(long int),在 32 位机器上,要么 long 多余,要么 int 多余。然后我想到了 long double 类型,觉得还是 int 多余吧。

  选取关键字要尽量做到语法正交,尽量能表达所有的意思,小部分可以留给扩展。从 C++11 的改变可以看出,尽量不添加新的关键字,双中括号扩展 [[]] 和右值符号 &&;不得已时添加新的关键字 alignas/alignof/thread_local 等。对差不多不用的关键字回收,auto 重新焕发光芒,template 能做的活,auto 现在也可以简单完成。
  添加新的关键字的时候,需要考虑几乎不会在历史代码中用到的。一些上下文相关的(contex-sensitive)关键字 final,override 的出现。它们不算关键字,但是放到函数末尾可以让编译器查错。可想而知,如果像 final override 这样常见的单词选入关键字,会有多少历史代码需要修改。很显然,final 和 override 是从 Java 语言中学过来的。Python 从 2.x 升到 3.x,各种不兼容,让人对这个语言又爱又恨,虽然有帮助迁移代码的文档和工具。
  因为 final 与 override 是上下文相关的,所以你这么写代码也会编译通过。

class Base
{
public:
    virtual int override(int )
    {
        std::cout << "please override me" << std::endl;
        return 0;
    }
};

class Fun: public Base
{
public:
    virtual int override(int ) override final
    {
        int override = 42;
        return override;
    }
};

int main()
{
    int me = 0x3e;
    Fun hey;
    hey.override(me);

    return 0;
}

 

  在 C++11 之前,C++ 标准交了很多东西给编译器自行处理,比如是否自行产生构造函数,内存对齐,RVO 优化,enum 的类型等等,现在都可以显式要求,避免各搞各的一套。enum 的类型是 implementation defined,很多编译器都是用能容纳的最小的整形范围来表示。所以你会在微软的很多代码里看到很多这样的写法:

typedef enum D3DTEXTUREADDRESS { 
    D3DTADDRESS_WRAP         = 1,
    D3DTADDRESS_MIRROR       = 2,
    D3DTADDRESS_CLAMP        = 3,
    D3DTADDRESS_BORDER       = 4,
    D3DTADDRESS_MIRRORONCE   = 5,
    D3DTADDRESS_FORCE_DWORD  = 0x7fffffff
} D3DTEXTUREADDRESS, *LPD3DTEXTUREADDRESS;

最后一个 enum 常量是  XXX_FORCE_DWORD  = 0x7fffffff,对其取值 sizeof(D3DTEXTUREADDRESS) 长度固定为 4。

  对齐本来是语言设计者想掩盖的细节,不过在C++11编程方式越发复杂的情况下,提供给用户更底层的手段往往是必不可少的。在一些情况下,用户虽然不能保证总是写出平台无关,或者说各平台习惯你能最优的代码,但只需要改造 alignas 之后的对齐值参数就可以保证程序的移植性及性能良好,也不失为一种好的选择。而 C++11 对对齐方式的支持从语法规则到库,基本上考虑了各种情况,可以说是相当完备的。

template <typename T>
class alignas(sizeof(T)<<2) Color
{
    T r, g, b, a;
};

 

  让关键字增加功能。以前 default 和 delete 只有一种用途:default 就是 switch 语句里的默认分支,delete 就是释放内存。现在都多了一种用法 = delete 表示删除函数,= default 表示使用编译期默认产生的。以前 using 只能与 namespace 为伴,而现在,typedef 所干的活,全部可以接手过来,而且在写法上更直观易懂。以前写 typedef long long int int64; 现在你可以写 using int64 = long long int; 怎么样?数据类型也可以用等号“赋值”了。
  C++11 出来后,你不用看编译器的脸色行事了。这里不需要定义构造函数,好让编译器自己生成;那里需要定义构造函数,否则编译器会做 shallow copying。现在你可以显示指示编译器怎么做。

class noncopyable
{
private:
    noncopyable(const noncopyable& );            // not defined
    noncopyable& operator=(const noncopyable&);  // not defined
};

  之前,我们不想让对象之间赋值,会讲赋值构造函数和 operator = 限制为 private,并不提供定义。现在我们有更好的写法:

class noncopyable
{
public:
    noncopyable(const noncopyable& ) = delete;
    noncopyable& operator=(const noncopyable&) = delete;
};

  声明为 private 可以阻止被调用。故意不实现这些函数,意味着如果外部通过成员函数或者 friend 类想访问它们,虽然编译可以通过,但是链接的时候会因未定义的符号而报错。在 C++11 里,情况就不一样了。使用 = delete 表示编译期不会产生这个方法,所以这个可以扼杀在编译期,不用推迟到链接的时候。虽然这里的访问权限对 C++11 不重要,但一般来说,标记 delete 的函数最好声明 public,而不是 private。因为有的编译器在检查成员函数的调用时,只报访问权限错误(错误如下)。很显然,这里 delete 才是重点,或者说错误的优先级高些。当然,并不是只有成员函数才能 delete,非成员函数和模版实例化的函数也是可以的。相比 C++98 而言,算是一项改进。

error: ‘pea::Log::Log(const pea::Log&)’ is private
Log(const Log& log) = delete;

 

  提醒一下,using namespace std; 不要写在 .h 文件里,要写也是写在 .cpp 文件里。出于头文件会被其他头文件包含的可能,污染命名空间,但是 .cpp 文件不会。除非你故意想 #include "XXX.cpp" 这么写。在cocos2d-x 里的 class HelloWorld 里面添加方法 void onKeyReleased(EventKeyboard::KeyCode keyCode, Event* event); 会编译不过,报错如下:

1>classes\HelloWorldScene.h(17): error C2653: 'EventKeyboard' : is not a class or namespace name (..\Classes\AppDelegate.cpp)
1>classes\HelloWorldScene.h(17): error C2061: syntax error : identifier 'KeyCode' (..\Classes\AppDelegate.cpp)
1>classes\HelloWorldScene.h(17): error C2653: 'EventKeyboard' : is not a class or namespace name (..\Classes\HelloWorldScene.cpp)
1>classes\HelloWorldScene.h(17): error C2061: syntax error : identifier 'KeyCode' (..\Classes\HelloWorldScene.cpp)
1>classes\HelloWorldScene.cpp(72): error C2276: '&' : illegal operation on bound member function expression

其实修改很简单,添加 EventKeyboard 和 KeyCode 所在的命名空间就好了。

void onKeyReleased(cocos2d::EventKeyboard::KeyCode keyCode, cocos2d::Event* event);

然而在 HelloWorldScene.cpp 里不用添加,因为开头有这么一句 USING_NS_CC; 也就是 using namespace cocos2d; 引入了 cocos2d 所有的东西。

void HelloWorld::onKeyReleased(EventKeyboard::KeyCode keyCode, Event* event)
{
  //TODO
}

 

  virtual 关键字用来申明虚函数,在虚函数的末尾添加 =0 表示纯虚函数。在函数名和参数可能动态变化的情况下,需要手动检查基类与派生类的函数是否一致,是否派生类的成员函数覆盖了基类的成员函数。这种枯燥的工作应该交给编译器来做的,于是 C++11 引入了上下文相关的关键字 override,在需要覆盖的地方没有覆盖,编译器就会报错。

  在 C++98,空指针用字面值 0 来表示, C++11 标准出来后,最好使用 nullptr,于是有人会问纯虚函数的声明 = 0 是不是应该更正为 = nullptr,尝试了下是不行的。语法上规定必须是 0, 而不能是表达式(42 - 42),甚至 0L,0u 0x0 (clang 编译器以前支持这样的写法)等。有些人选择用宏 #define pure = 0 ,这个从声明上似乎容易识别虚函数,这样写不是很好。C++ 之父 Bjarne Stroustrup 解释了没有添加 abstract/pure 关键字。(Java 里有关键字 abstract)

  Bjarne Stroustrup 在他的书 The Design & Evolution of C++, section 13.2.3 中描述

The curious =0 syntax was chosen over the obvious alternative of introducing a new keyword pure or abstract because at the time I saw no chance of getting a new keyword accepted. Had I suggested pure, Release 2.0 would have shipped without abstract classes. Given a choice between a nicer syntax and abstract classes, I chose abstract classes. Rather than risking delay and incurring the certain fights over pure, I used the tradition C and C++ convention of using 0 to represent "not there." The =0 syntax fits with my view that a function body is the initializer for a function also with the (simplistic, but usually adequate) view of the set of virtual functions being implemented as a vector of function pointers. 

  = 0 似乎让人迷惑,认为纯虚函数就是未实现的函数,以为是将该函数指针置空(nullptr),而实际上,纯虚函数也是可以实现的。不能在声明时实现,而是必须在外部给出定义。C++11 引入了上下文相关的关键字概念,现在开来,引入 pure 作为上下文相关的关键字,将 = 0 替换成 pure 的写法会更直观。

class Base
{
public:
    virtual void fun() = 0;
};

void Base::fun()
{
    std::cout<<"pure virtual implementation\n";
}

class Derived: public Base
{
public:
    virtual void fun() { Base::fun(); }
};

  这里引出另外两个话题:析构函数可以是纯虚函数,但必须给出定义,否则会出现链接错误,因为继承的类在析构的时候会调用基类的析构函数,找不到符号。纯虚函数要求非抽象子类给出实现,所以基类的构造函数最好给 protected 权限。




  C++11 增加的 operator "" 的重载,用户自定义语义(user literal限制在以下几种类型,

( const char * )
( unsigned long long int )
( long double )
( char )
( wchar_t )
( char16_t )
( char32_t )
( const char * , std::size_t )
( const wchar_t * , std::size_t )
( const char16_t * , std::size_t )
( const char32_t * , std::size_t )

  发现没有,比如浮点类型只有 long double,却没有 float 和 double 类型,为什么呢?假设我们有一个角度制和弧度制转换的函数语义。

constexpr long double operator"" _deg ( long double deg )
{
    return deg*M_PI/180;
}

我们要表达 M_PI 弧度时候,可以写成 180_deg ,注意 180 和 _deg 是一个整体,之间不能有空格。(就像 C/C++ 语言内的后缀语义 long int degree = 180L; float pi = 3.14159265f 一样)。如果要区分 float、double、long double 数据类型,我们的定义应该是这样的:

float radian1 = 180f_deg;
double radian2 = 180_deg;
long double radian3 = 180L_deg;
很显然,编译器会把没有空格的合法字符当成一个单词(token)解析,于是 f_deg 变成了一个单词,前面也说了 f 与 _deg 之间不能有空格,所以我们无法满足不同浮点类型的重载,也包括整数类型。为了不截断或窄收,于是采取最大表示范围的类型,整形用 unsigned long long int,浮点类型用 long double。整形是 long long int 而不是 unsigned long long int 是因为有符号和无符号数相加减,会扩充到无符号类型。这里似乎暗示了 unsigned long long int 是最大范围的整形,所以应该不会出现 128 位的整形。

 

  尽量让计算或可能发生的错误推前到编译期而不是运行期,在 C++11 增加了新的关键字 static_cast、constexpr。boost 库里面有模拟 static_cast 的功能。
之前我们用宏定义 FOURCC

#define MAKE_FOURCC(ch0, ch1, ch2, ch3) \
    ((uint32_t)(uint8_t)(ch0) |         \
    ((uint32_t)(uint8_t)(ch1) << 8) |   \
    ((uint32_t)(uint8_t)(ch2) << 16) |  \
    ((uint32_t)(uint8_t)(ch3) << 24))   \

现在我们可以直接用 constexpr 函数来写,而不是类型不安全的宏了。因为是编译器计算出值,所以算出来的整形值可以直接用于 switch case 语句中而不会出错。

constexpr uint32_t makeFourCC(uint8_t ch0, uint8_t ch1, uint8_t ch2, uint8_t ch3)
{
    return static_cast<uint32_t>(ch0 | (ch1<<8) | (ch2<<16) | (ch3<<24));
}

constexpr uint32_t RIFF = makeFourCC('R', 'I', 'F', 'F');
constexpr uint32_t WAVE = makeFourCC('W', 'A', 'V', 'E');
constexpr uint32_t FMT_ = makeFourCC('F', 'M', 'T', ' ');
constexpr uint32_t DATA = makeFourCC('D', 'A', 'T', 'A'); 

 

让派生类不要继承父类的行为,大概有三种做法。
1. virtual void fun1() {}
2. virtual void fun2() { assert(false); }
3. virtual void fun3() = 0;
这些写法我都遇到过,fun1 实现留空,表示派生类可以覆盖其行为;fun3 纯虚函数,表示派生类必须覆盖其行为;而 fun2 如果没有覆盖,在运行时断言失败。
很显然 fun2 的做法不是很好,错误要等到运行时才能发现。

 

  类型转换关键字 const_cast / dynamic_cast / reinterpret_cast / static_cast
  不要用C的带括号的强制转换。不要在指针和整形之间转换,也不要在数据指针和函数指针之间转换。类型之间转换时,首先考虑使用 static_cast。整形到 enum 枚举类型、浮点数到整形用 static_cast。从 void* 类型转换到特定的指针类型,虽然 static_cast 与 reinterpret_cast 都能用,最好还是用 static_cast。在不相关的指针类型之间转换时,用 reinterpret_cast,这个相当于C语言里面的转换。const_cast 不要被名字迷惑了,它是可以 cast 掉 CV-qualifier,而不仅仅是 const,可以是 volatile 修饰的类型。

// Possible implementation of std::addressof in <memory>
template<typename T>
T* addressof(T& arg) noexcept
{
    return reinterpret_cast<T*>(
               &const_cast<char&>(
                  reinterpret_cast<const volatile char&>(arg)));
}

  上面是C++11标准里的 std::addressof 的实现。注意这里的hack技巧,最里层需要有const volatile关键字。reinterpret_cast 转换可以增加 const volatile 修饰符,但不能移除。所以如果 arg 是 const 变量的话,reinterpret_cast 就会编译失败。


  对于函数传入的指针或引用,如果不会修改其内容,尽量用 const 修饰这个参数。C语言中释放内存的函数是 void free(void *ptr);,释放本来是不会修改指针指向的内容的,而是让操作系统回收分配的内存。函数签名似乎应该为 void free(const void *ptr);,这样可以直接释放 const 指针类型而不用 const_cast 转换一次。但是从另一方面讲,申请内存一般就会修改内存,修改就不能直接用 const 修饰,谁申请谁释放,释放内存也就不是 const 指针的责任了。
  基类到派生类的转换,需要用到 dynamic_cast,我们称之为下转(downcast)。指针下转失败为空 nullptr,引用下转失败则抛出 std::bad_cast 异常。在 C++11 的 noexcept 出现之前,C++ 的异常处理机制不是很好使,很多程序员也是尽量不去碰。在含有大量层次继承关系的游戏或对性能有要求的一些工程里,会关闭 C++ 的 RAII (Run-Time Type Information) 机制。做法是 debug 版本开启检查类型安全,release 版本关闭运行时的查找以提升性能。

template <typename To, typename From>
To downcast(From p)
{
#ifdef NDEBUG
    return static_cast<To>(p);
#else
    To to = dynamic_cast<To>(p);
    assert(to != nullptr);
    return to;
#endif
}

 


  C++ 十几年的时间没有动静,造就了 boost 优秀库的出现。又从较自己晚出身的语言身上也学到了很多,比如 Java 类的成员在定义的时候给定默认值,委托构造函数(delegate constructor)、Python 的原生字符串常量,也算方便了程序员对 C++ 字符串的学习和使用。

 

posted @ 2015-06-20 22:50  KAlO2  阅读(2150)  评论(1编辑  收藏  举报