C++語言的15個晦澀特性
這個列表收集了 C++ 語言的一些晦澀(Obscure)特性,是我經(jīng)年累月研究這門語言的各個方面收集起來的。C++非常龐大,我總是能學(xué)到一些新知識。即使你對C++已了如指掌,也希望你能從列表中學(xué)到一些東西。下面列舉的特性,根據(jù)晦澀程度由淺入深進(jìn)行排序。
- 1. 方括號的真正含義
- 2. 最煩人的解析
- 3.替代運(yùn)算標(biāo)記符
- 4. 重定義關(guān)鍵字
- 5. Placement new
- 6.在聲明變量的同時進(jìn)行分支
- 7.成員函數(shù)的引用修飾符
- 8.轉(zhuǎn)向完整的模板元編程
- 9.指向成員的指針操作符
- 10. 靜態(tài)實(shí)例方法
- 11.重載++和–
- 12.操作符重載和檢查順序
- 13.函數(shù)作為模板參數(shù)
- 14.模板的參數(shù)也是模板
- 15.try塊作為函數(shù)
方括號的真正含義
用來訪問數(shù)組元素的ptr[3]其實(shí)只是*(ptr + 3)的縮寫,與用*(3 + ptr)是等價的,因此反過來與3[ptr]也是等價的,使用3[ptr]是完全有效的代碼
最煩人的解析
“most vexing parse”這個詞是由Scott Meyers提出來的,因?yàn)镃++語法聲明的二義性會導(dǎo)致有悖常理的行為:
- // 這個解釋正確?
- // 1) 類型std::string的變量會通過std::string()實(shí)例化嗎?
- // 2) 一個函數(shù)聲明,返回一個std::string值并有一個函數(shù)指針參數(shù),
- // 該函數(shù)也返回一個std::string但沒有參數(shù)?
- std::string foo(std::string());
- // 還是這個正確?
- // 1)類型int變量會通過int(x)實(shí)例化嗎?
- // 2)一個函數(shù)聲明,返回一個int值并有一個參數(shù),
- // 該參數(shù)是一個名為x的int型變量嗎?
- int bar(int(x));
兩種情形下C++標(biāo)準(zhǔn)要求的是第二種解釋,即使***種解釋看起來更直觀。程序員可以通過包圍括號中變量的初始值來消除歧義:
- //加括號消除歧義
- std::string foo((std::string()));
- int bar((int(x)));
第二種情形讓人產(chǎn)生二義性的原因是int y = 3;等價于int(y) = 3;
譯者注:這一點(diǎn)我覺得有點(diǎn)迷惑,下面是我在g++下的測試用例:
- #include <iostream>
- #include <string>
- using namespace std;
- int bar(int(x)); // 等價于int bar(int x)
- string foo(string()); // 等價于string foo(string (*)())
- string test() {
- return "test";
- }
- int main()
- {
- cout << bar(2) << endl; // 輸出2
- cout << foo(test); // 輸出test
- return 0;
- }
- int bar(int(x)) {
- return x;
- }
- string foo(string (*fun)()) {
- return (*fun)();
- }
能正確輸出,但如果按作者意思添加上括號后再編譯就會報一堆錯誤:“在此作用域尚未聲明”、“重定義”等,還不清楚作者的意圖。
替代運(yùn)算標(biāo)記符
標(biāo)記符and, and_eq, bitand, bitor, compl, not, not_eq, or, or_eq, xor, xor_eq, <%, %>, <: 和 :>都可以用來代替我們常用的&&, &=, &, |, ~, !, !=, ||, |=, ^, ^=, {, }, [ 和 ]。在鍵盤上缺乏必要的符號時你可以使用這些運(yùn)算標(biāo)記符來代替。
重定義關(guān)鍵字
通過預(yù)處理器重定義關(guān)鍵字從技術(shù)上講會引起錯誤,但實(shí)際上是允許這樣做的。因此你可以使用類似#define true false 或 #define else來搞點(diǎn)惡作劇。但是,也有它合法有用的時候,例如,如果你正在使用一個很大的庫而且需要繞過C++訪問保護(hù)機(jī)制,除了給庫打補(bǔ)丁的方法外,你也可 以在包含該庫頭文件之前關(guān)閉訪問保護(hù)來解決,但要記得在包含庫頭文件之后一定要打開保護(hù)機(jī)制!
- #define class struct
- #define private public
- #define protected public
- #include "library.h"
- #undef class
- #undef private
- #undef protected
注意這種方式不是每一次都有效,跟你的編譯器有關(guān)。當(dāng)實(shí)例變量沒有被訪問控制符修飾時,C++只需要將這些實(shí)例變量順序布局即可,所以編譯器可以對 訪問控制符組重新排序來自由更改內(nèi)存布局。例如,允許編譯器移動所有的私有成員放到公有成員的后面。另一個潛在的問題是名稱重整(name mangling),Microsoft的C++編譯器將訪問控制符合并到它們的name mangling表里,因此改變訪問控制符意味著將破壞現(xiàn)有編譯代碼的兼容性。
譯者注:在C++中,Name Mangling 是為了支持重載而加入的一項(xiàng)技術(shù)。編譯器將目標(biāo)源文件中的名字進(jìn)行調(diào)整,這樣在目標(biāo)文件符號表中和連接過程中使用的名字和編譯目標(biāo)文件的源程序中的名字不一樣,從而實(shí)現(xiàn)重載。
#p#
Placement new
Placement new是new操作符的一個替代語法,作用在已分配的對象上,該對象已有正確的大小和正確的賦值,這包括建立虛函數(shù)表和調(diào)用構(gòu)造函數(shù)。
譯者注:placement new就是在用戶指定的內(nèi)存位置上構(gòu)建新的對象,這個構(gòu)建過程不需要額外分配內(nèi)存,只需要調(diào)用對象的構(gòu)造函數(shù)即可。placement new實(shí)際上是把原本new做的兩步工作分開來:***步自己分配內(nèi)存,第二步調(diào)用類的構(gòu)造函數(shù)在自己已分配的內(nèi)存上構(gòu)建新的對象。placement new的好處:1)在已分配好的內(nèi)存上進(jìn)行對象的構(gòu)建,構(gòu)建速度快。2)已分配好的內(nèi)存可以反復(fù)利用,有效的避免內(nèi)存碎片問題。
- #include <iostream>
- using namespace std;
- struct Test {
- int data;
- Test() { cout << "Test::Test()" << endl; }
- ~Test() { cout << "Test::~Test()" << endl; }
- };
- int main() {
- // Must allocate our own memory
- Test *ptr = (Test *)malloc(sizeof(Test));
- // Use placement new
- new (ptr) Test;
- // Must call the destructor ourselves
- ptr->~Test();
- // Must release the memory ourselves
- free(ptr);
- return 0;
- }
當(dāng)在性能關(guān)鍵的場合需要自定義分配器時可以使用Placement new。例如,一個slab分配器從單個的大內(nèi)存塊開始,使用placement new在塊里順序分配對象。這不僅避免了內(nèi)存碎片,也節(jié)省了malloc引起的堆遍歷的開銷。
在聲明變量的同時進(jìn)行分支
C++包含一個語法縮寫,能在聲明變量的同時進(jìn)行分支??雌饋砑认駟蝹€的變量聲明也可以有if或while這樣的分支條件。
- 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;
- }
成員函數(shù)的引用修飾符
C++11允許成員函數(shù)在對象的值類型上進(jìn)行重載,this指針會將該對象作為一個引用修飾符。引用修飾符會放在cv限定詞(譯者注:CV限定詞有 三種:const限定符、volatile限定符和const-volatile限定符)相同的位置并依據(jù)this對象是左值還是右值影響重載解析:
- #include <iostream>
- struct Foo {
- void foo() & { std::cout << "lvalue" << std::endl; }
- void foo() && { std::cout << "rvalue" << std::endl; }
- };
- int main() {
- Foo foo;
- foo.foo(); // Prints "lvalue"
- Foo().foo(); // Prints "rvalue"
- return 0;
- }
轉(zhuǎn)向完整的模板元編程
C++模板是為了實(shí)現(xiàn)編譯時元編程,也就是該程序能生成其它的程序。設(shè)計(jì)模板系統(tǒng)的初衷是進(jìn)行簡單的類型替換,但是在C++標(biāo)準(zhǔn)化過程中突然發(fā)現(xiàn)模板實(shí)際上功能十分強(qiáng)大,足以執(zhí)行任意計(jì)算,雖然很笨拙很低效,但通過模板特化的確可以完成一些計(jì)算:
- // Recursive template for general case
- template <int N>
- struct factorial {
- enum { value = N * factorial<N - 1>::value };
- };
- // Template specialization for base case
- template <>
- struct factorial<0> {
- enum { value = 1 };
- };
- enum { result = factorial<5>::value }; // 5 * 4 * 3 * 2 * 1 == 120
C++模板可以被認(rèn)為是一種功能型編程語言,因?yàn)樗鼈兪褂眠f歸而非迭代而且包含不可變狀態(tài)。你可以使用typedef創(chuàng)建一個任意類型的變量,使用enum創(chuàng)建一個int型變量,數(shù)據(jù)結(jié)構(gòu)內(nèi)嵌在類型自身。
- // Compile-time list of integers
- template <int D, typename N>
- struct node {
- enum { data = D };
- typedef N next;
- };
- struct end {};
- // Compile-time sum function
- template <typename L>
- struct sum {
- enum { value = L::data + sum<typename L::next>::value };
- };
- template <>
- struct sum<end> {
- enum { value = 0 };
- };
- // Data structures are embedded in types
- typedef node<1, node<2, node<3, end> > > list123;
- enum { total = sum<list123>::value }; // 1 + 2 + 3 == 6
當(dāng)然這些例子沒什么用,但模板元編程的確可以做一些有用的事情,比如可以操作類型列表。但是,使用C++模板的編程語言可用性極低,因此請謹(jǐn)慎和少量使用。模板代碼很難閱讀,編譯速度慢,而且因其冗長和迷惑的錯誤信息而難以調(diào)試。
#p#
指向成員的指針操作符
指向成員的指針操作符可以讓你在一個類的任何實(shí)例上描述指向某個成員的指針。有兩種pointer-to-member操作符,取值操作符*和指針操作符->:
- #include <iostream>
- using namespace std;
- struct Test {
- int num;
- void func() {}
- };
- // Notice the extra "Test::" in the pointer type
- int Test::*ptr_num = &Test::num;
- void (Test::*ptr_func)() = &Test::func;
- int main() {
- Test t;
- Test *pt = new Test;
- // Call the stored member function
- (t.*ptr_func)();
- (pt->*ptr_func)();
- // Set the variable in the stored member slot
- t.*ptr_num = 1;
- pt->*ptr_num = 2;
- delete pt;
- return 0;
- }
該特征實(shí)際上十分有用,尤其在寫庫的時候。例如,Boost::Python, 一個用來將C++綁定到Python對象的庫,就使用成員指針操作符,在包裝對象時很容易的指向成員。
- #include <iostream>
- #include <boost/python.hpp>
- using namespace boost::python;
- struct World {
- std::string msg;
- void greet() { std::cout << msg << std::endl; }
- };
- BOOST_PYTHON_MODULE(hello) {
- class_<World>("World")
- .def_readwrite("msg", &World::msg)
- .def("greet", &World::greet);
- }
記住使用成員函數(shù)指針與普通函數(shù)指針是不同的。在成員函數(shù)指針和普通函數(shù)指針之間casting是無效的。例如,Microsoft編譯器里的成員 函數(shù)使用了一個稱為thiscall的優(yōu)化調(diào)用約定,thiscall將this參數(shù)放到ecx寄存器里,而普通函數(shù)的調(diào)用約定卻是在棧上解析所有的參 數(shù)。
而且,成員函數(shù)指針可能比普通指針大四倍左右,編譯器需要存儲函數(shù)體的地址,到正確父地址(多個繼承)的偏移,虛函數(shù)表(虛繼承)中另一個偏移的索引,甚至在對象自身內(nèi)部的虛函數(shù)表的偏移也需要存儲(為了前向聲明類型)。
- #include <iostream>
- struct A {};
- struct B : virtual A {};
- struct C {};
- struct D : A, C {};
- struct E;
- int main() {
- std::cout << sizeof(void (A::*)()) << std::endl;
- std::cout << sizeof(void (B::*)()) << std::endl;
- std::cout << sizeof(void (D::*)()) << std::endl;
- std::cout << sizeof(void (E::*)()) << std::endl;
- return 0;
- }
- // 32-bit Visual C++ 2008: A = 4, B = 8, D = 12, E = 16
- // 32-bit GCC 4.2.1: A = 8, B = 8, D = 8, E = 8
- // 32-bit Digital Mars C++: A = 4, B = 4, D = 4, E = 4
在Digital Mars編譯器里所有的成員函數(shù)都是相同的大小,這是源于這樣一個聰明的設(shè)計(jì):生成“thunk”函數(shù)來運(yùn)用右偏移而不是存儲指針自身內(nèi)部的偏移。
靜態(tài)實(shí)例方法
C++中可以通過實(shí)例調(diào)用靜態(tài)方法也可以通過類直接調(diào)用。這可以使你不需要更新任何調(diào)用點(diǎn)就可以將實(shí)例方法修改為靜態(tài)方法。
- struct Foo {
- static void foo() {}
- };
- // These are equivalent
- Foo::foo();
- Foo().foo();
重載++和–
C++的設(shè)計(jì)中自定義操作符的函數(shù)名稱就是操作符本身,這在大部分情況下都工作的很好。例如,一元操作符的-和二元操作符的-(取反和相減)可以通 過參數(shù)個數(shù)來區(qū)分。但這對于一元遞增和遞減操作符卻不奏效,因?yàn)樗鼈兊奶卣魉坪跬耆嗤++語言有一個很笨拙的技巧來解決這個問題:后綴++和–操作 符必須有一個空的int參數(shù)作為標(biāo)記讓編譯器知道要進(jìn)行后綴操作(是的,只有int類型有效)。
- struct Number {
- Number &operator ++ (); // Generate a prefix ++ operator
- Number operator ++ (int); // Generate a postfix ++ operator
- };
操作符重載和檢查順序
重載,(逗號),||或者&&操作符會引起混亂,因?yàn)樗蚱屏苏5臋z查規(guī)則。通常情況下,逗號操作符在整個左邊檢查完畢才開始檢 查右邊,|| 和 &&操作符有短路行為:僅在必要時才會去檢查右邊。無論如何,操作符的重載版本僅僅是函數(shù)調(diào)用且函數(shù)調(diào)用以未指定的順序檢查它們的參數(shù)。
重載這些操作符只是一種濫用C++語法的方式。作為一個實(shí)例,下面我給出一個Python形式的無括號版打印語句的C++實(shí)現(xiàn):
- #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;
- }
#p#
函數(shù)作為模板參數(shù)
眾所周知,模板參數(shù)可以是特定的整數(shù)也可以是特定的函數(shù)。這使得編譯器在實(shí)例化模板代碼時內(nèi)聯(lián)調(diào)用特定的函數(shù)以獲得更高效的執(zhí)行。下面的例子里,函數(shù)memoize的模板參數(shù)也是一個函數(shù)且只有新的參數(shù)值才通過函數(shù)調(diào)用(舊的參數(shù)值可以通過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);
- }
模板的參數(shù)也是模板
模板參數(shù)實(shí)際上自身的參數(shù)也可以是模板,這可以讓你在實(shí)例化一個模板時可以不用模板參數(shù)就能夠傳遞模板類型??聪旅娴拇a:
- 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;
CachedStore的cache存儲的數(shù)據(jù)類型與store的類型相同。然而我們在實(shí)例化一個CachedStore必須重復(fù)寫數(shù)據(jù)類型(上面的代碼 是int型),store本身要寫,CachedStore也要寫,關(guān)鍵是我們這并不能保證兩者的數(shù)據(jù)類型是一致的。我們真的只想要確定數(shù)據(jù)類型一次即 可,所以我們可以強(qiáng)制其不變,但是沒有類型參數(shù)的列表會引起編譯出錯:
- // 下面編譯通不過,因?yàn)镹etworkStore和MemoryStore缺失類型參數(shù)
- CachedStore<NetworkStore, int> c;
- CachedStore<MemoryStore, int> d;
模板的模板參數(shù)可以讓我們獲得想要的語法。注意你必須使用class關(guān)鍵字作為模板參數(shù)(他們自身的參數(shù)也是模板)
- template <template <typename> class Store, typename T>
- struct CachedStore2 {
- Store<T> store;
- Cache<T> cache;
- };
- CachedStore2<NetworkStore, int> e;
- CachedStore2<MemoryStore, int> f;
try塊作為函數(shù)
函數(shù)的try塊會在檢查構(gòu)造函數(shù)的初始化列表時捕獲拋出的異常。你不能在初始化列表的周圍加上try-catch塊,因?yàn)槠渲荒艹霈F(xiàn)在函數(shù)體外。為了解決這個問題,C++允許try-catch塊也可作為函數(shù)體:
- int f() { throw 0; }
- // 這里沒有辦法捕獲由f()拋出的異常
- struct A {
- int a;
- A::A() : a(f()) {}
- };
- // 如果try-catch塊被用作函數(shù)體并且初始化列表移至try關(guān)鍵字之后的話,
- // 那么由f()拋出的異常就可以捕獲到
- struct B {
- int b;
- B::B() try : b(f()) {
- } catch(int e) {
- }
- };
奇怪的是,這種語法不僅僅局限于構(gòu)造函數(shù),也可用于其他的所有函數(shù)定義。