本文由这里翻译,讲述一些不常见的C++特性。
中括号的真正含义
访问数组元素ptr[3]
的真正含义是*(ptr+3)
简写。这和*(3+ptr)
是等价的。所以ptr[3]
也可以写作3[ptr]
。
语法分析的优先级
C++定义变量时候的语法分析可能会导致反直觉的分析结果:
// foo是一个:
// 1) std::string类型的变量,初始化值为std::string()?
// 2) 还是一个返回std::string的函数,其带有一个返回值为std::string的函数指针参数?
std::string foo(std::string());
// bar是一个:
// 1) int类型的变量,初始化值为int(x)?
// 2) 还是一个带有一个参数的函数,其参数为int 类型的x?
int bar(int(x));
在这两个例子中,C++标准都会支持第二种解释方法,即使你觉得第一种才是符合直觉的。要想得到第一种解释的结果,你需要加括号:
// Parentheses resolve the ambiguity
std::string foo((std::string()));
int bar((int(x)));
重新定义关键词
尽管理论上重新定义关键词是会导致报错的,不过实际中很多编译器允许这么做。你可以重新定义true, false, else来搞乱你的程序。另外你可以通过对public,private的重新定义来绕过C++的面向对象保护机制。
#define class struct
#define private public
#define protected public
#include "library.h"
#undef class
#undef private
#undef protected
这个小技巧可能会导致一些意想不到的问题,例如内存对象的重新排序。有的编译器会把private的成员统一放在public的成员后面。另外Microsoft的编译器会给成员起一些带public
或者private
的别名,这可能会导致与已经编译的二进制库不兼容。
在已经分配的对象上new
你可以用如下的方法在已经分配的对象上面new
,其需要满足的先决条件是大小和对齐均与原对象一致。它包括的操作是重新设置vtable和运行构造函数:
#include <iostream>
using namespace std;
struct Test {
int data;
Test() { cout << "Test::Test()" << endl; }
~Test() { cout << "Test::~Test()" << endl; }
};
int main() {
// 使用malloc
Test *ptr = (Test *)malloc(sizeof(Test));
// 原地new
new (ptr) Test;
// 需要自己调用析构函数
ptr->~Test();
// 需要自己free内存
free(ptr);
return 0;
}
变量定义语句的返回值
C++提供了一种语法,可以在为变量赋值的同时根据其值来做条件判断,例如:
struct Event { virtual ~Event() {} };
struct MouseEvent : Event { int x, y; };
struct KeyboardEvent : Event { int key; };
void log(Event *event) {
if (MouseEvent *mouse = dynamic_cast<MouseEvent *>(event))
std::cout << "MouseEvent " << mouse->x << " " << mouse->y << std::endl;
else if (KeyboardEvent *keyboard = dynamic_cast<KeyboardEvent *>(event))
std::cout << "KeyboardEvent " << keyboard->key << std::endl;
else
std::cout << "Event" << std::endl;
}
成员函数的引用限定符(Ref-qualifier)
C++11允许类的成员函数根据调用它的对象是左值还是右值来进行重载,声明引用限定符(ref-qualifier)的位置和const以及volatile限定符(cv-qualifier)相同,实际调用的函数取决于调用者的this
指针是左值还是右值。
struct Foo {
void foo() & { std::cout << "lvalue" << std::endl; }
void foo() && { std::cout << "rvalue" << std::endl; }
};
int main() {
Foo foo;
foo.foo(); // 打印 "lvalue"
Foo().foo(); // 打印 "rvalue"
return 0;
}
图灵完全的元编程
模板是C++用来编译期间元编程的工具,其中元编程的意思是说用来生成程序的程序。模板系统本来是设计用来进行简单的类型替换的,不过它实际上已经具备了图灵完全性,通过模板特化来进行计算:
// 递归定义模板
template <int N>
struct factorial {
enum { value = N * factorial<N - 1>::value };
};
// 初始情况下的模板特化
template <>
struct factorial<0> {
enum { value = 1 };
};
enum { result = factorial<5>::value }; // 5 * 4 * 3 * 2 * 1 == 120
我们可以认为C++模板是一种函数式编程语言,因为它使用递归而不是迭代,也不包括任何可变状态(mutable state)。这样你就可以用typedef
定义一个包含类型的变量,用enum
定义一个包含整数的变量。类型内部可以包含数据结构:
// 编译期整数列表
template <int D, typename N>
struct node {
enum { data = D };
typedef N next;
};
struct end {};
// 编译期加法函数
template <typename L>
struct sum {
enum { value = L::data + sum<typename L::next>::value };
};
template <>
struct sum<end> {
enum { value = 0 };
};
// 嵌入类型的数据结构
typedef node<1, node<2, node<3, end> > > list123;
enum { total = sum<list123>::value }; // 1 + 2 + 3 == 6
这些例子整体上来说没什么用,仅仅用来说明元编程的可能性。模板元编程代码很难阅读,编译很慢,而且难于调试。
在实例上调用静态函数
C++允许直接在类的实例上调用静态函数,这样你可以不用改变函数调用方式,直接把一个实例函数变成一个静态函数。
struct Foo {
static void foo() {}
};
//两者等价
Foo::foo();
Foo().foo();
重载++和--运算符
C++设计使用运算符号本身来代表自定义运算符时的函数名,大多数情况下这是有效的,例如-运算符可以通过它是一元还是二元运算符来判断它是负号还是减号,这样重载时它通过函数参数个数不一样来区分。不过自加和自减运算符是不能这么做的。C++使用一个丑陋的hack来解决这个问题,后缀++和--运算符必须带一个没用的int参数来让编译器知道重载的是一个后缀++或--运算符。
struct Number {
Number &operator ++ (); // 重载前缀++运算符
Number operator ++ (int); // 重载后缀++运算符
};
运算符重载和运算次序
重载,(逗号),||,以及&&运算符是非常危险的,因为它破坏了正常的求值顺序。正常情况下逗号运算符会保证左边的表达式一定在右边的表达式之前被求值,而||和&&有所谓的“短路特性”,只在必要的时候对右侧求值。然而,对这些运算符的重载只是普通的函数调用,对参数的求值顺序是不确定的。
重载这些运算符只是一种混淆C++语法的方法,一个例子是通过C++实现Python式的不需要括号的print语句:
#include <iostream>
namespace __hidden__ {
struct print {
bool space;
print() : space(false) {}
~print() { std::cout << std::endl; }
template <typename T>
print &operator , (const T &t) {
if (space) std::cout << ' ';
else space = true;
std::cout << t;
return *this;
}
};
}
#define print __hidden__::print(),
int main() {
int a = 1, b = 2;
print "this is a test";
print "the sum of", a, "and", b, "is", a + b;
return 0;
}
函数作为模板参数
众所周知可以使用具体数字作为参数特化模板,不过具体函数也可以作为参数特化模板,这会让编译器内联调用此函数来特化模板。下面的例子memoize
函数以一个函数为模板参数,只有当值不在map中时候才会调用f求值,否则直接返回cache中存在的值。
#include <map>
template <int (*f)(int)>
int memoize(int x) {
static std::map<int, int> cache;
std::map<int, int>::iterator y = cache.find(x);
if (y != cache.end()) return y->second;
return cache[x] = f(x);
}
int fib(int n) {
if (n < 2) return n;
return memoize<fib>(n - 1) + memoize<fib>(n - 2);
}
模板作为模板参数
可以使用模板来作为模板的参数,它可以在实例化模板时传递进模板类型作为参数,所以有如下代码:
template <typename T>
struct Cache { ... };
template <typename T>
struct NetworkStore { ... };
template <typename T>
struct MemoryStore { ... };
template <typename Store, typename T>
struct CachedStore {
Store store;
Cache<T> cache;
};
CachedStore<NetworkStore<int>, int> a;
CachedStore<MemoryStore<int>, int> b;
CacheStore
可以存储一个数据Store以及该Store的一个缓存。然而,在定义CachedStore
类型变量的时候我们必须把实际存储的类型int写两遍,一次用来实例化Store
,一次用来实例化Cache
,类型系统不能保证两者的类型一致。我们真正想要的是只定义实际存储的类型一次。向模板传递另一个模板作为参数可以解决这个问题,注意需要加class
关键字来告诉编译器这个模板参数自己也需要参数:
template <template <typename> class Store, typename T>
struct CachedStore2 {
Store<T> store;
Cache<T> cache;
};
CachedStore2<NetworkStore, int> e;
CachedStore2<MemoryStore, int> f;
构造函数上的try语句
如果构造函数的初始化列表出现了异常,可以使用构造函数上的try语句来处理。可以使用如下语法:
int f() { throw 0; }
// Here there is no way to catch the error thrown by f()
struct A {
int a;
A::A() : a(f()) {}
};
// f()抛出的异常值
// The value thrown from f() can be caught if a try-catch block is used as
// the function body and the initializer list is moved after the try keyword
struct B {
int b;
B::B() try : b(f()) {
} catch(int e) {
}
};
尽管语法有点奇怪,它不仅可以应用于构造函数,也可以应用于普通函数。