概念 (C++)

「概念」(concept) 已於 2009 年 7 月份的 C++ 委員會會議中被投票否決，已經從 C++0X 標準中正式被移除。 本文紀錄的是最後一篇有出現「概念」的工作檔案。

在針對 C++ 進行修訂的 C++0x 中，概念 (concept) 和與其相關的一組公設 (axiom) 被提出作為 C++ 模板系統的擴充。它們被設計用來增進編譯器發現問題程式碼所產生的錯誤訊息，並讓程式設計師能在他們所編寫的樣板中定義樣板引數所具備的屬性。這些屬性讓程式碼能指引編譯器做某些最佳化 (除了增進可讀性之外)，同時也可能透過形式驗證工具來檢驗實作與規格是否相符以增進可靠性。

2009 年 7 月，因為概念被認為還未準備好進入 C++0x，C++0x 委員會決定從標準草案中將其移除。目前有些非正式的計劃以某種形式將概念再次納入標準，但仍未有正式的決定。一個針對概念的初步實作是。

動機

模板類別和函式有必要的對他們使用的型別加上限制。例如 STL 容器要求它所包含的型別必須是可賦值的。不像動態多型所展現的類別繼承階級，接受 Foo & 型別的函式可以接受 Foo 的任何子類別；只要支援所有模板內使用的操作，任何類別都可以被提供作為模版引數。對函式來說，引數的要求是明確的 (必需是 Foo 的子類別)；但是對模版來說，物件必須符合的介面是不明確的。「概念」(concept) 提供了一種機制，要求模板引數必須符合特定條件。

引入 concept 的主要目的，是為了改善編譯器發現問題程式碼所產生的錯誤訊息。若程式設計師嘗試在模板中使用不符合其介面需求的型別，編譯器應當回報錯誤。問題在於與模板使用相關的錯誤訊息極難解讀，尤其不利於新手。主要有兩個原因：首先，錯誤訊息往往將模板引數以原名全數列出，造成訊息長度暴增。某些編譯器甚至會對簡單的錯誤產生數千位元組的錯誤訊息。其次，錯誤訊息通常不會立即指出真正發生問題之處。例如，當程式設計師試圖將不帶有複製建構子的型別置入 vector 中，第一個錯誤幾乎總是指向 vector 內部使用複製建構之處。程式設計師必須有足夠的經驗和技巧才能夠瞭解真正的錯誤，是由於使用的型別不滿足 vector 類的要求 (需要複製建構子)。

為瞭解決上述的問題，C++0x 加入了 concept 這種語言特性。Concept 是一種具名的構造，用來描述型別的需求或是條件限制。在 OOP 中，類似的做法是利用基底類別的定義，當作衍生類別的最小需求 ("is-a" 的繼承方式，衍生類別都帶有基底類別的介面)。而 concept 的定義不限於作為模板引數的限制條件，也可以適用於模板定義 (如最後的 concept Stack)。

模板使用 concept 的一種方法是以 concept 名稱取代模板型別指示字 class 或 typename。在下面的例子中，若傳入模板函式 min 的型別不滿足 concept LessThanComparable 的要求，編譯時將會產生錯誤，告知使用者具現化 (instantiate) 模板的型別不符合 concept LessThanComparable。

template

const T& min (const T &x, const T &y)

{

return y

相較於上例的簡式用法，更為泛用的 concept 使用形式如下：

template requires LessThanComparable

const T& min (const T &x, const T &y)

{

return y

泛用形中，使用關鍵字 requires 作為型別需求表列的開始。需求表列由 concept 所構成，可以利用 "非"(!) 與 "且"(&&) 的符號，將數個 concept 結合，如同邏輯運算式。若使用者想避免某個特定的 concept 被模板套用，可以用這樣的語法：requires !LessThanComparable。在模板特化或偏特化中，可以指定型別使用特定的模板實作；而否定的 concept 語法，可以顯式地在模板或 concept 中指明被排除的型別條件為何。另外，若需要在需求表列中表達 "且"(logical-and) 的語意，使用 "&&" 將多個 concept 連結起來即可。例如若模板中的型別需要設值 (assignment) 以及複製建構 (copy-construct)，可以使用 requires Assignable&&CopyConstructible。

定義概念

定義 concept 的方式如下：

auto concept LessThanComparable

{

bool operator

此處為 concept LessThanComparable 宣告，說明若型別 T 有一個雙引數的函式：operator ，且函式傳回值為 bool，則型別 T 滿足 concept LessThanComparable。函式 operator 可以是全域或是成員函式。

C++0x 為了避免 concept 的誤用，除非使用者顯式指明，編譯器不會主動認定型別符合 concept (隱式套用 concept)。為了避免繁瑣的指明，此處關鍵字 auto 代表只要型別帶有 concept 中指定的操作，它即是符合該 concept 的一個型別。若沒有加上 auto，則必須使用 concept_map 來指明型別符合特定的 concept。

concept 也可以包含多種型別。例如以下的 concept Convertible，表示型別 T 可轉換為 U。

auto concept Convertible

{operator U (const T&);

}

在模板中使用涉及多型別的 concept，必須使用泛用形式：

template requires Convertible

U convert (const T& t)

{return t;}

Concept 可以是其他 concept 的構件。在下例中，InputIterator 的第一個引數 Iter 必須符合 concept Regular：

concept InputIterator

{

requires Regular;

Value operator*(const Iter&);

Iter& operator++(Iter&);

Iter operator++(Iter&, int);

}

另一方面，concept 之間也能帶有衍生關係。如同類的繼承，滿足衍生 concept 的型別也必須滿足基底 concept，語法上也和類繼承相同：

concept ForwardIterator : InputIterator

{// 在此加上 ForwardIterator 的其它要求}

Concept 中也可宣告關聯型別 (associated type)，以 typename 宣告。模板使用 concept 時，模板引數必須要提供相關型別的定義。

concept InputIterator

{

typename value_type;

typename reference;

typename pointer;

typename difference_type;

requires Regular;

requires Convertible;

reference operator*(const Iter&); // 解參考

Iter& operator++(Iter&); // 前置遞增

Iter operator++(Iter&, int); // 後置遞增

//...

}

對映概念

Concept map 可以將型別 "對映" 到特定的 concept，告知編譯器使用的型別是 "如何" 符合 concept。

concept_map InputIterator

{

typedef char value_type ;

typedef char& reference ;

typedef char* pointer ;

typedef std::ptrdifft differencetype ;

};

這個 concept_map 定義 char* 符合 concept InputIterator，並且一一宣告所需的關聯型別。

concept_map 可以宣告成模板，下面的例子宣告所有的指標型別都符合 concept InputIterator。

template concept_map InputIterator

{

typedef T value_type ;

typedef T& reference ;

typedef T* pointer ;

typedef std::ptrdifft differencetype ;

};

concept_map 可以作為一個迷你型別，在其中置入函式的定義與其它用來定義類的相關構件。

concept Stack

{

typename value_type;

void push (X&, const value_type&);

void pop (X&);

value_type top (const X&);

bool empty (const X&);

};

template concept_map Stack >

{

typedef T value_type;

void push (std::vector& v, const T& x) {v.push_back (x); }

void pop (std::vector& v) {v.pop_back ; }

T top (const std::vector& v) {return v.back ; }

bool empty (const std::vector& v) {return v.empty ; }

};

在這裡，concept Stack 定義了需要的函式以及關聯型別，而 conceptmap 定義如何以 std::vector 實現底層的操作，每個 concept Stack 裡的函式都可以轉接到 std::vector 的函式呼叫。因此，conceptmap 能在不改變原型別 (類別) 的定義下，

完成介面轉換 (interface adaptation)。

最後值得一提的是，一些模板的要求可以使用編譯期斷言 (static assertion)。它們可以驗證一些模板的要求，不過實際上是針對不同的問題。

公設

C++0x 提供了公設 (axiom) 用來表達概念的語意屬性。舉例來說，我們可以用公設 Associativity 來定義概念 Semigroup:

concept Semigroup : CopyConstructible

{T operator (Op, T, T);

axiom Associativity (Op op, T x, T y, T z)

{op (x, op (y, z)) == op (op (x, y), z);

}

}

編譯器可以利用公設所表達的語意做些原本不被允許的最佳化，因為這些最佳化可能會在程式可見的行為上有副作用 (其除了少數的例外，其中之一是回返值最佳化 (RVO))。在上述的例子中，編譯器可能會重新安排 operator 呼叫的次序。前提是 Op 和 T 與概念 Semigroup 有對映關係。

公設也能在軟體驗證，軟體測試以及其它程式分析和轉換上有所幫助。