(轉)C++語言的15個晦澀特性

原文連接: Evan Wallace   翻譯: 伯樂在線 敏敏
譯文連接: http://blog.jobbole.com/54140/node

這個列表收集了 C++ 語言的一些晦澀(Obscure)特性,是我經年累月研究這門語言的各個方面收集起來的。C++很是龐大,我老是能學到一些新知識。即便你對C++已瞭如指掌,也但願你能從列表中學到一些東西。下面列舉的特性,根據晦澀程度由淺入深進行排序。python

  • 1. 方括號的真正含義
  • 2. 最煩人的解析
  • 3.替代運算標記符
  • 4. 重定義關鍵字
  • 5. Placement new
  • 6.在聲明變量的同時進行分支
  • 7.成員函數的引用修飾符
  • 8.圖靈完備的模板元編程
  • 9.指向成員的指針操做符
  • 10. 靜態實例方法
  • 11.重載++和–
  • 12.操做符重載和檢查順序
  • 13.函數做爲模板參數
  • 14.模板的參數也是模板
  • 15.try塊做爲函數

 

方括號的真正含義

用來訪問數組元素的ptr[3]其實只是*(ptr + 3)的縮寫,與用*(3 + ptr)是等價的,所以反過來與3[ptr]也是等價的,使用3[ptr]是徹底有效的代碼。ios

 

最煩人的解析

「most vexing parse」這個詞是由Scott Meyers提出來的,由於C++語法聲明的二義性會致使有悖常理的行爲:git

// 這個解釋正確?
// 1) 類型std::string的變量會經過std::string()實例化嗎?
// 2) 一個函數聲明,返回一個std::string值並有一個函數指針參數,
// 該函數也返回一個std::string但沒有參數?
std::string foo(std::string());
 
// 仍是這個正確?
// 1)類型int變量會經過int(x)實例化嗎?
// 2)一個函數聲明,返回一個int值並有一個參數,
// 該參數是一個名爲x的int型變量嗎?
int bar(int(x));

兩種情形下C++標準要求的是第二種解釋,即便第一種解釋看起來更直觀。程序員能夠經過包圍括號中變量的初始值來消除歧義:程序員

//加括號消除歧義
std::string foo((std::string()));
int bar((int(x)));

第二種情形讓人產生二義性的緣由是int y = 3;等價於int(y) = 3;編程

譯者注:這一點我以爲有點迷惑,下面是我在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)();
}

能正確輸出,但若是按做者意思添加上括號後再編譯就會報一堆錯誤:「在此做用域還沒有聲明」、「重定義」等,還不清楚做者的意圖。數據結構

 

替代運算標記符

標記符and, and_eq, bitand, bitor, compl, not, not_eq, or, or_eq, xor, xor_eq, <%, %>, <: 和 :>均可以用來代替咱們經常使用的&&, &=, &, |, ~, !, !=, ||, |=, ^, ^=, {, }, [ 和 ]。在鍵盤上缺少必要的符號時你可使用這些運算標記符來代替。編程語言

 

重定義關鍵字

經過預處理器重定義關鍵字從技術上講會引發錯誤,但其實是容許這樣作的。所以你可使用相似#define true false 或 #define else來搞點惡做劇。可是,也有它合法有用的時候,例如,若是你正在使用一個很大的庫並且須要繞過C++訪問保護機制,除了給庫打補丁的方法外,你也能夠在包含該庫頭文件以前關閉訪問保護來解決,但要記得在包含庫頭文件以後必定要打開保護機制!函數

#define class struct
#define private public
#define protected public
 
#include "library.h"
 
#undef class
#undef private
#undef protected

注意這種方式不是每一次都有效,跟你的編譯器有關。當實例變量沒有被訪問控制符修飾時,C++只須要將這些實例變量順序佈局便可,因此編譯器能夠對訪問控制符組從新排序來自由更改內存佈局。例如,容許編譯器移動全部的私有成員放到公有成員的後面。另外一個潛在的問題是名稱重整(name mangling),Microsoft的C++編譯器將訪問控制符合併到它們的name mangling表裏,所以改變訪問控制符意味着將破壞現有編譯代碼的兼容性。

譯者注:在C++中,Name Mangling 是爲了支持重載而加入的一項技術。編譯器將目標源文件中的名字進行調整,這樣在目標文件符號表中和鏈接過程當中使用的名字和編譯目標文件的源程序中的名字不同,從而實現重載。

 

Placement new 

Placement new是new操做符的一個替代語法,做用在已分配的對象上,該對象已有正確的大小和正確的對齊,這包括創建虛函數表和調用構造函數。 

譯者注:placement new就是在用戶指定的內存位置上構建新的對象,這個構建過程不須要額外分配內存,只須要調用對象的構造函數便可。placement new其實是把本來new作的兩步工做分開來:第一步本身分配內存,第二步調用類的構造函數在本身已分配的內存上構建新的對象。placement new的好處:1)在已分配好的內存上進行對象的構建,構建速度快。2)已分配好的內存能夠反覆利用,有效的避免內存碎片問題。

 

#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;
}

 

當在性能關鍵的場合須要自定義分配器時可使用Placement new。例如,一個slab分配器從單個的大內存塊開始,使用placement new在塊裏順序分配對象。這不只避免了內存碎片,也節省了malloc引發的堆遍歷的開銷。 

 

在聲明變量的同時進行分支

C++包含一個語法縮寫,能在聲明變量的同時進行分支。看起來既像單個的變量聲明也能夠有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;
}

 

成員函數的引用修飾符

C++11容許成員函數在對象的值類型上進行重載,this指針會將該對象做爲一個引用修飾符。引用修飾符會放在cv限定詞(譯者注:CV限定詞有三種:const限定符、volatile限定符和const-volatile限定符)相同的位置並依據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;
}

 

圖靈完備的模板元編程

C++模板是爲了實現編譯時元編程,也就是該程序能生成其它的程序。設計模板系統的初衷是進行簡單的類型替換,可是在C++標準化過程當中忽然發現模板實際上功能十分強大,足以執行任意計算,雖然很笨拙很低效,但經過模板特化的確能夠完成一些計算:

// 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++模板能夠被認爲是一種功能型編程語言,由於它們使用遞歸而非迭代並且包含不可變狀態。你可使用typedef建立一個任意類型的變量,使用enum建立一個int型變量,數據結構內嵌在類型自身。 

// 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

固然這些例子沒什麼用,但模板元編程的確能夠作一些有用的事情,好比能夠操做類型列表。可是,使用C++模板的編程語言可用性極低,所以請謹慎和少許使用。模板代碼很難閱讀,編譯速度慢,並且因其冗長和迷惑的錯誤信息而難以調試。

 

指向成員的指針操做符

指向成員的指針操做符可讓你在一個類的任何實例上描述指向某個成員的指針。有兩種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;
}

該特徵實際上十分有用,尤爲在寫庫的時候。例如,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);
}

記住使用成員函數指針與普通函數指針是不一樣的。在成員函數指針和普通函數指針之間casting是無效的。例如,Microsoft編譯器裏的成員函數使用了一個稱爲thiscall的優化調用約定,thiscall將this參數放到ecx寄存器裏,而普通函數的調用約定倒是在棧上解析全部的參數。

並且,成員函數指針可能比普通指針大四倍左右,編譯器須要存儲函數體的地址,到正確父地址(多個繼承)的偏移,虛函數表(虛繼承)中另外一個偏移的索引,甚至在對象自身內部的虛函數表的偏移也須要存儲(爲了前向聲明類型)。

#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編譯器裏全部的成員函數都是相同的大小,這是源於這樣一個聰明的設計:生成「thunk」函數來運用右偏移而不是存儲指針自身內部的偏移。

 

靜態實例方法

C++中能夠經過實例調用靜態方法也能夠經過類直接調用。這可使你不須要更新任何調用點就能夠將實例方法修改成靜態方法。

struct Foo {
  static void foo() {}
};
 
// These are equivalent
Foo::foo();
Foo().foo();

 

重載++和–

C++的設計中自定義操做符的函數名稱就是操做符自己,這在大部分狀況下都工做的很好。例如,一元操做符的-和二元操做符的-(取反和相減)能夠經過參數個數來區分。但這對於一元遞增和遞減操做符卻不奏效,由於它們的特徵彷佛徹底相同。C++語言有一個很笨拙的技巧來解決這個問題:後綴++和–操做符必須有一個空的int參數做爲標記讓編譯器知道要進行後綴操做(是的,只有int類型有效)。

struct Number {
  Number &operator ++ (); // Generate a prefix ++ operator
  Number operator ++ (int); // Generate a postfix ++ operator
};

 

操做符重載和檢查順序

重載,(逗號),||或者&&操做符會引發混亂,由於它打破了正常的檢查規則。一般狀況下,逗號操做符在整個左邊檢查完畢纔開始檢查右邊,|| 和 &&操做符有短路行爲:僅在必要時纔會去檢查右邊。不管如何,操做符的重載版本僅僅是函數調用且函數調用以未指定的順序檢查它們的參數。

重載這些操做符只是一種濫用C++語法的方式。做爲一個實例,下面我給出一個Python形式的無括號版打印語句的C++實現:

#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的模板參數也是一個函數且只有新的參數值才經過函數調用(舊的參數值能夠經過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;

CachedStore的cache存儲的數據類型與store的類型相同。然而咱們在實例化一個CachedStore必須重複寫數據類型(上面的代碼是int型),store自己要寫,CachedStore也要寫,關鍵是咱們這並不能保證二者的數據類型是一致的。咱們真的只想要肯定數據類型一次便可,因此咱們能夠強制其不變,可是沒有類型參數的列表會引發編譯出錯:

// 下面編譯通不過,由於NetworkStore和MemoryStore缺失類型參數
CachedStore<NetworkStore, int> c;
CachedStore<MemoryStore, int> d;

模板的模板參數可讓咱們得到想要的語法。注意你必須使用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塊會在檢查構造函數的初始化列表時捕獲拋出的異常。你不能在初始化列表的周圍加上try-catch塊,由於其只能出如今函數體外。爲了解決這個問題,C++容許try-catch塊也可做爲函數體:

 

int f() { throw 0; }
 
// 這裏沒有辦法捕獲由f()拋出的異常
struct A {
  int a;
  A::A() : a(f()) {}
};
 
// 若是try-catch塊被用做函數體而且初始化列表移至try關鍵字以後的話,
// 那麼由f()拋出的異常就能夠捕獲到
struct B {
  int b;
  B::B() try : b(f()) {
  } catch(int e) {
  }
};

奇怪的是,這種語法不只僅侷限於構造函數,也可用於其餘的全部函數定義。

相關文章
相關標籤/搜索