本文由 伯樂在線 - 敏敏 翻譯自 Evan Wallace。歡迎加入技術(shù)翻譯小組。轉(zhuǎn)載請參見文章末尾處的要求。
這個列表收集了 C++ 語言的一些晦澀(Obscure)特性,是我經(jīng)年累月研究這門語言的各個方面收集起來的。C++非常龐大,我總是能學(xué)到一些新知識。即使你對C++已了如指掌,也希望你能從列表中學(xué)到一些東西。下面列舉的特性,根據(jù)晦澀程度由淺入深進行排序。
- 1. 方括號的真正含義
- 2. 最煩人的解析
- 3.替代運算標(biāo)記符
- 4. 重定義關(guān)鍵字
- 5. Placement new
- 6.在聲明變量的同時進行分支
- 7.成員函數(shù)的引用修飾符
- 8.圖靈完備的模板元編程
- 9.指向成員的指針操作符
- 10. 靜態(tài)實例方法
- 11.重載++和–
- 12.操作符重載和檢查順序
- 13.函數(shù)作為模板參數(shù)
- 14.模板的參數(shù)也是模板
- 15.try塊作為函數(shù)
方括號的真正含義
用來訪問數(shù)組元素的ptr[3]其實只是*(ptr + 3)的縮寫,與用*(3 + ptr)是等價的,因此反過來與3[ptr]也是等價的,使用3[ptr]是完全有效的代碼。
最煩人的解析
“most vexing parse”這個詞是由Scott Meyers提出來的,因為C++語法聲明的二義性會導(dǎo)致有悖常理的行為:
1 2 3 4 5 6 7 8 9 10 11 | // 這個解釋正確?
// 1) 類型std::string的變量會通過std::string()實例化嗎?
// 2) 一個函數(shù)聲明,返回一個std::string值并有一個函數(shù)指針參數(shù),
// 該函數(shù)也返回一個std::string但沒有參數(shù)?
std::string foo(std::string());
// 還是這個正確?
// 1)類型int變量會通過int(x)實例化嗎?
// 2)一個函數(shù)聲明,返回一個int值并有一個參數(shù),
// 該參數(shù)是一個名為x的int型變量嗎?
int bar( int (x));
|
兩種情形下C++標(biāo)準(zhǔn)要求的是第二種解釋,即使第一種解釋看起來更直觀。程序員可以通過包圍括號中變量的初始值來消除歧義:
1 2 3 | //加括號消除歧義
std::string foo((std::string()));
int bar(( int (x)));
|
第二種情形讓人產(chǎn)生二義性的原因是int y = 3;等價于int(y) = 3;
譯者注:這一點我覺得有點迷惑,下面是我在g++下的測試用例:
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 | #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)();
}
|
能正確輸出,但如果按作者意思添加上括號后再編譯就會報一堆錯誤:“在此作用域尚未聲明”、“重定義”等,還不清楚作者的意圖。
替代運算標(biāo)記符
標(biāo)記符and, and_eq, bitand, bitor, compl, not, not_eq, or, or_eq, xor, xor_eq, <%, %>, <: 和 :>都可以用來代替我們常用的&&, &=, &, |, ~, !, !=, ||, |=, ^, ^=, {, }, [ 和 ]。在鍵盤上缺乏必要的符號時你可以使用這些運算標(biāo)記符來代替。
重定義關(guān)鍵字
通過預(yù)處理器重定義關(guān)鍵字從技術(shù)上講會引起錯誤,但實際上是允許這樣做的。因此你可以使用類似#define true false 或 #define else來搞點惡作劇。但是,也有它合法有用的時候,例如,如果你正在使用一個很大的庫而且需要繞過C++訪問保護機制,除了給庫打補丁的方法外,你也可以在包含該庫頭文件之前關(guān)閉訪問保護來解決,但要記得在包含庫頭文件之后一定要打開保護機制!
1 2 3 4 5 6 7 8 9 | #define class struct
#define private public
#define protected public
#include "library.h"
#undef class
#undef private
#undef protected
|
注意這種方式不是每一次都有效,跟你的編譯器有關(guān)。當(dāng)實例變量沒有被訪問控制符修飾時,C++只需要將這些實例變量順序布局即可,所以編譯器可以對訪問控制符組重新排序來自由更改內(nèi)存布局。例如,允許編譯器移動所有的私有成員放到公有成員的后面。另一個潛在的問題是名稱重整(name mangling),Microsoft的C++編譯器將訪問控制符合并到它們的name mangling表里,因此改變訪問控制符意味著將破壞現(xiàn)有編譯代碼的兼容性。
譯者注:在C++中,Name Mangling 是為了支持重載而加入的一項技術(shù)。編譯器將目標(biāo)源文件中的名字進行調(diào)整,這樣在目標(biāo)文件符號表中和連接過程中使用的名字和編譯目標(biāo)文件的源程序中的名字不一樣,從而實現(xiàn)重載。
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實際上是把原本new做的兩步工作分開來:第一步自己分配內(nèi)存,第二步調(diào)用類的構(gòu)造函數(shù)在自己已分配的內(nèi)存上構(gòu)建新的對象。placement new的好處:1)在已分配好的內(nèi)存上進行對象的構(gòu)建,構(gòu)建速度快。2)已分配好的內(nèi)存可以反復(fù)利用,有效的避免內(nèi)存碎片問題。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #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引起的堆遍歷的開銷。
在聲明變量的同時進行分支
C++包含一個語法縮寫,能在聲明變量的同時進行分支??雌饋砑认駟蝹€的變量聲明也可以有if或while這樣的分支條件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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ù)在對象的值類型上進行重載,this指針會將該對象作為一個引用修飾符。引用修飾符會放在cv限定詞(譯者注:CV限定詞有三種:const限定符、volatile限定符和const-volatile限定符)相同的位置并依據(jù)this對象是左值還是右值影響重載解析:
1 2 3 4 5 6 7 8 9 10 11 12 13 | #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;
}
|
圖靈完備的模板元編程
C++模板是為了實現(xiàn)編譯時元編程,也就是該程序能生成其它的程序。設(shè)計模板系統(tǒng)的初衷是進行簡單的類型替換,但是在C++標(biāo)準(zhǔn)化過程中突然發(fā)現(xiàn)模板實際上功能十分強大,足以執(zhí)行任意計算,雖然很笨拙很低效,但通過模板特化的確可以完成一些計算:
1 2 3 4 5 6 7 8 9 10 11 12 13 | // 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++模板可以被認為是一種功能型編程語言,因為它們使用遞歸而非迭代而且包含不可變狀態(tài)。你可以使用typedef創(chuàng)建一個任意類型的變量,使用enum創(chuàng)建一個int型變量,數(shù)據(jù)結(jié)構(gòu)內(nèi)嵌在類型自身。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // 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++模板的編程語言可用性極低,因此請謹慎和少量使用。模板代碼很難閱讀,編譯速度慢,而且因其冗長和迷惑的錯誤信息而難以調(diào)試。
指向成員的指針操作符
指向成員的指針操作符可以讓你在一個類的任何實例上描述指向某個成員的指針。有兩種pointer-to-member操作符,取值操作符*和指針操作符->:
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 | #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;
}
|
該特征實際上十分有用,尤其在寫庫的時候。例如,Boost::Python, 一個用來將C++綁定到Python對象的庫,就使用成員指針操作符,在包裝對象時很容易的指向成員。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #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ù)表的偏移也需要存儲(為了前向聲明類型)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #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è)計:生成“thunk”函數(shù)來運用右偏移而不是存儲指針自身內(nèi)部的偏移。
靜態(tài)實例方法
C++中可以通過實例調(diào)用靜態(tài)方法也可以通過類直接調(diào)用。這可以使你不需要更新任何調(diào)用點就可以將實例方法修改為靜態(tài)方法。
1 2 3 4 5 6 7 | struct Foo {
static void foo() {}
};
// These are equivalent
Foo::foo();
Foo().foo();
|
重載++和–
C++的設(shè)計中自定義操作符的函數(shù)名稱就是操作符本身,這在大部分情況下都工作的很好。例如,一元操作符的-和二元操作符的-(取反和相減)可以通過參數(shù)個數(shù)來區(qū)分。但這對于一元遞增和遞減操作符卻不奏效,因為它們的特征似乎完全相同。C++語言有一個很笨拙的技巧來解決這個問題:后綴++和–操作符必須有一個空的int參數(shù)作為標(biāo)記讓編譯器知道要進行后綴操作(是的,只有int類型有效)。
1 2 3 4 | struct Number {
Number &operator ++ (); // Generate a prefix ++ operator
Number operator ++ ( int ); // Generate a postfix ++ operator
};
|
操作符重載和檢查順序
重載,(逗號),||或者&&操作符會引起混亂,因為它打破了正常的檢查規(guī)則。通常情況下,逗號操作符在整個左邊檢查完畢才開始檢查右邊,|| 和 &&操作符有短路行為:僅在必要時才會去檢查右邊。無論如何,操作符的重載版本僅僅是函數(shù)調(diào)用且函數(shù)調(diào)用以未指定的順序檢查它們的參數(shù)。
重載這些操作符只是一種濫用C++語法的方式。作為一個實例,下面我給出一個Python形式的無括號版打印語句的C++實現(xiàn):
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 | #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;
}
|
函數(shù)作為模板參數(shù)
眾所周知,模板參數(shù)可以是特定的整數(shù)也可以是特定的函數(shù)。這使得編譯器在實例化模板代碼時內(nèi)聯(lián)調(diào)用特定的函數(shù)以獲得更高效的執(zhí)行。下面的例子里,函數(shù)memoize的模板參數(shù)也是一個函數(shù)且只有新的參數(shù)值才通過函數(shù)調(diào)用(舊的參數(shù)值可以通過cache獲得):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #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ù)就能夠傳遞模板類型。看下面的代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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的類型相同。然而我們在實例化一個CachedStore必須重復(fù)寫數(shù)據(jù)類型(上面的代碼是int型),store本身要寫,CachedStore也要寫,關(guān)鍵是我們這并不能保證兩者的數(shù)據(jù)類型是一致的。我們真的只想要確定數(shù)據(jù)類型一次即可,所以我們可以強制其不變,但是沒有類型參數(shù)的列表會引起編譯出錯:
1 2 3 | // 下面編譯通不過,因為NetworkStore和MemoryStore缺失類型參數(shù)
CachedStore<NetworkStore, int > c;
CachedStore<MemoryStore, int > d;
|
模板的模板參數(shù)可以讓我們獲得想要的語法。注意你必須使用class關(guān)鍵字作為模板參數(shù)(他們自身的參數(shù)也是模板)
1 2 3 4 5 6 7 8 | 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塊,因為其只能出現(xiàn)在函數(shù)體外。為了解決這個問題,C++允許try-catch塊也可作為函數(shù)體:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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ù)定義。
原文鏈接: Evan Wallace 翻譯: 伯樂在線 - 敏敏 譯文鏈接: http://blog./54140/ [ 轉(zhuǎn)載必須在正文中標(biāo)注并保留原文鏈接、譯文鏈接和譯者等信息。]
|