概念 (C++)

“概念”(concept)已于2009年7月份的 C++ 委员会会议中被投票否决,已经从 C++0X标准中正式被移除。[1][2] 本文纪录的是最后一篇有出现“概念”的工作文件。[3]

在针对 C++ 进行修订的 C++0x 中,概念 (concept) 和与其相关的一组公设 (axiom) 被提出作为 C++ 模板系统的扩充。它们被设计用来增进编译器发现问题代码所产生的错误讯息,并让程序员能在他们所编写的样板中定义样板参数所具备的属性。这些属性让代码能指引编译器做某些优化(除了增进可读性之外),同时也可能透过形式验证工具来检验实作与规格是否相符以增进可靠性。

2009年7月,因为概念被认为还未准备好进入 C++0x,C++0x 委员会决定从标准草案中将其移除。目前有些非正式的计划以某种形式将概念再次纳入标准,但仍未有正式的决定。一个针对概念的初步实作是ConceptGCC波兰语ConceptGCC

动机

模板类别和函式有必要的对他们使用的型别加上限制。例如STL容器要求它所包含的型别必须是可赋值的。不像动态多型所展现的类别继承阶级,接受Foo&型别的函式可以接受Foo的任何子类别;只要支援所有模板内使用的操作,任何类别都可以被提供作为模版参数。对函式来说,引数的要求是明确的(必需是Foo的子类别);但是对模版来说,物件必须符合的介面是不明确的。“概念”(concept)提供了一种机制,要求模板参数必须符合特定条件。

引入 concept 的主要目的,是为了改善编译器发现问题代码所产生的错误讯息。若程序员尝试在模板中使用不符合其介面需求的型别,编译器应当回报错误。问题在于与模板使用相关的错误讯息极难解读,尤其不利于新手。主要有两个原因:首先,错误讯息往往将模板参数以原名全数列出,造成讯息长度暴增。某些编译器甚至会对简单的错误产生数千字节的错误讯息。其次,错误讯息通常不会立即指出真正发生问题之处。例如,当程序员试图将不带有拷贝建构子的型别置入vector中,第一个错误几乎总是指向vector内部使用拷贝建构之处。程序员必须有足够的经验和技巧才能够了解真正的错误,是由于使用的型别不满足vector类的要求(需要拷贝建构子)。

为了解决上述的问题,C++0x 加入了 concept 这种语言特性。Concept 是一种具名的构造,用来描述型别的需求或是条件限制。在 OOP 中,类似的做法是利用基底类别的定义,当作衍生类别的最小需求("is-a"的继承方式,衍生类别都带有基底类别的介面)。而 concept 的定义不限于作为模板参数的限制条件,也可以适用于模板定义(如最后的 concept Stack)。

模板使用 concept 的一种方法是以 concept 名称取代模板型别指示字classtypename。在下面的例子中,若传入模板函式min的型别不满足 concept LessThanComparable的要求,编译时将会产生错误,告知使用者具现化(instantiate)模板的型别不符合concept LessThanComparable

template<LessThanComparable T>
  const T& min(const T &x, const T &y)
  {
    return y < x ? y : x;
  }

相较于上例的简式用法,更为泛用的concept使用形式如下:

template<typename T> requires LessThanComparable<T>
  const T& min(const T &x, const T &y)
  {
    return y < x ? y : x;
  }

泛用形中,使用关键字 requires 作为型别需求表列的开始。需求表列由 concept 所构成,可以利用"非"(!) 与 "且"(&&)的符号,将数个 concept 结合,如同逻辑运算式。若使用者想避免某个特定的 concept 被模板套用,可以用这样的语法:requires !LessThanComparable<T>。在模板特化或偏特化中,可以指定型别使用特定的模板实作;而否定的 concept 语法,可以显式地在模板或 concept 中指明被排除的型别条件为何。另外,若需要在需求表列中表达"且"(logical-and)的语意,使用"&&"将多个 concept 连结起来即可。例如若模板中的型别需要设值(assignment)以及拷贝建构(copy-construct),可以使用requires Assignable<T>&&CopyConstructible<T>

定义概念

定义 concept 的方式如下:

auto concept LessThanComparable<typename T>
{
  bool operator<(T, T);
}

此处为 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<typename T, typename U>
{
  operator U(const T&);
}

在模板中使用涉及多型别的 concept,必须使用泛用形式:

template<typename U, typename T> requires Convertible<T, U>
  U convert(const T& t)
  {
    return t;
  }

Concept 可以是其他 concept 的构件。在下例中,InputIterator 的第一个参数 Iter 必须符合 concept Regular

concept InputIterator<typename Iter, typename Value>
{
  requires Regular<Iter>;
  Value operator*(const Iter&);
  Iter& operator++(Iter&);
  Iter operator++(Iter&, int);
}

另一方面,concept 之间也能带有衍生关系。如同类的继承,满足衍生 concept 的型别也必须满足基底 concept,语法上也和类继承相同:

concept ForwardIterator<typename Iter, typename Value> : InputIterator<Iter, Value>
{
  // 在此加上 ForwardIterator 的其它要求
}

Concept 中也可宣告关联型别(associated type),以 typename 宣告。模板使用 concept 时,模板引数必须要提供相关型别的定义。

concept InputIterator<typename Iter>
{
  typename value_type;
  typename reference;
  typename pointer;
  typename difference_type;
  requires Regular<Iter>;
  requires Convertible<reference, value_type>;
  reference operator*(const Iter&); // 解參考
  Iter& operator++(Iter&); // 前置遞增
  Iter operator++(Iter&, int); // 後置遞增
  // ...
}

映射概念

Concept map 可以将型别"映射"到特定的 concept,告知编译器使用的型别是"如何"符合 concept。

concept_map InputIterator<char*>
{
  typedef char value_type ;
  typedef char& reference ;
  typedef char* pointer ;
  typedef std::ptrdiff_t difference_type ;
};

这个 concept_map 定义 char* 符合 concept InputIterator,并且一一声明所需的关联型别。

concept_map 可以宣告成模板,下面的例子声明所有的指针型别都符合 concept InputIterator

template<typename T> concept_map InputIterator<T*>
{
  typedef T value_type ;
  typedef T& reference ;
  typedef T* pointer ;
  typedef std::ptrdiff_t difference_type ;
};

concept_map 可以作为一个迷你型别,在其中置入函式的定义与其它用来定义类的相关构件。

concept Stack<typename X>
{
  typename value_type;
  void push(X&, const value_type&);
  void pop(X&);
  value_type top(const X&);
  bool empty(const X&);
};

template<typename T> concept_map Stack<std::vector<T> >
{
  typedef T value_type;
  void push(std::vector<T>& v, const T& x) { v.push_back(x); }
  void pop(std::vector<T>& v) { v.pop_back(); }
  T top(const std::vector<T>& v) { return v.back(); }
  bool empty(const std::vector<T>& v) { return v.empty(); }
};

在这里,concept Stack 定义了需要的函式以及关联型别,而 concept_map 定义如何以 std::vector 实现底层的操作,每个 concept Stack 里的函式都可以转接到 std::vector 的函式调用。因此,concept_map能在不改变原型别(类别)的定义下, 完成介面转换(interface adaptation)。

最后值得一提的是,一些模板的要求可以使用编译期断言(static assertion)。它们可以验证一些模板的要求,不过实际上是针对不同的问题。

公设

C++0x 提供了公设 (axiom) 用来表达概念的语意属性。举例来说,我们可以用公设 Associativity 来定义概念 Semigroup:

concept Semigroup< typename Op, typename T> : CopyConstructible<T>
{
  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() 呼叫的次序。前提是 OpT 与概念 Semigroup 有映射关系。

公设也能在软体验证,软体测试以及其它程序分析和转换上有所帮助。

参考资料

  1. ^ InformIT: The Removal of Concepts From C++0x. [2010-11-21]. (原始内容存档于2016-05-31). 
  2. ^ C++0x中concept的移除[失效链接]
  3. ^ Working Draft, Standard for Programming Language C++ (version of 2009-06-22) (PDF). [2010-11-21]. (原始内容存档 (PDF)于2013-07-20). 

外部链接