更多 C++ 習語/表示式模板
- 在 C++ 中建立一個領域特定嵌入語言 (DSEL)
- 支援 C++ 表示式的延遲求值(例如,數學表示式),這些表示式可以在程式定義後的時間點執行。
- 將表示式(而不是表示式的結果)作為引數傳遞給函式。
領域特定語言 (DSL) 是一種開發程式的方式,其中要解決的問題是用更接近問題域的符號來表達,而不是過程語言提供的通常符號(迴圈、條件語句等)。領域特定嵌入語言 (DSEL) 是 DSL 的一種特殊情況,其中符號被嵌入到宿主語言(例如 C++)中。基於 C++ 的 DSL 的兩個突出例子是Boost Spirit 解析器框架 和 Blitz++ 科學計算庫。Spirit 提供了一種將 EBNF 語法直接寫入 C++ 程式的符號,而 Blitz++ 允許一種對矩陣執行數學運算的符號。顯然,C++ 本身沒有提供這種符號。使用這種符號的主要好處是,程式直觀地捕捉了程式設計師的意圖,使程式更易讀。它極大地降低了開發和維護成本。
那麼,這些庫(Spirit 和 Blitz++)是如何實現這種抽象級別上的飛躍的呢?答案是——你猜對了——表示式模板。
表示式模板背後的關鍵思想是表示式的延遲求值。C++ 本身不支援表示式的延遲求值。例如,在下面的程式碼片段中,加法表示式 (x+x+x) 在呼叫函式 foo 之前執行。
int x;
foo(x + x + x); // The addition expression does not exist beyond this line.
函式 foo 實際上並不知道它接收的引數是如何計算的。加法表示式在第一次也是唯一一次求值後就不再存在了。C++ 的這種預設行為對於絕大多數現實世界程式來說是必要且足夠的。但是,一些程式需要稍後再次求值表示式。例如,將陣列中的每個整數都乘以 3。
int expression (int x)
{
return x + x + x; // Note the same expression.
}
// .... Lot of other code here
const int N = 5;
double A[N] = { 1, 2, 3, 4, 5};
std::transform(A, A+N, A, std::ptr_fun(expression)); // Triples every integer.
這是在 C/C++ 中支援數學表示式延遲求值的傳統方法。表示式被包裝在一個函式中,該函式被作為引數傳遞。這種技術存在函式呼叫和建立臨時變數的開銷,而且通常,表示式在原始碼中的位置與呼叫位置相差甚遠,這會對可讀性和可維護性產生負面影響。表示式模板透過內聯表示式來解決這個問題,從而消除了對函式指標的需要,並將表示式和呼叫位置整合在一起。
表示式模板使用遞迴型別組合 習語。遞迴型別組合使用包含其他相同模板例項作為成員變數的類模板的例項。相同模板的多次重複例項化產生了型別的抽象語法樹 (AST)。遞迴型別組合已被用來建立線性型別列表以及以下示例中使用的二元表示式樹。
#include <iostream>
#include <vector>
struct Var {
double operator () (double v) { return v; }
};
struct Constant {
double c;
Constant (double d) : c (d) {}
double operator () (double) { return c; }
};
template < class L, class H, class OP >
struct DBinaryExpression {
L l_;
H h_;
DBinaryExpression (L l, H h) : l_ (l), h_ (h) {}
double operator () (double d) { return OP::apply (l_ (d), h_(d)); }
};
struct Add {
static double apply (double l, double h) { return l + h; }
};
template < class E >
struct DExpression {
E expr_;
DExpression (E e) : expr_ (e) {}
double operator() (double d) { return expr_(d); }
};
template < class Itr, class Func >
void evaluate (Itr begin, Itr end, Func func)
{
for (Itr i = begin; i != end; ++i)
std::cout << func (*i) << std::endl;
}
int main (void)
{
typedef DExpression <Var> Variable;
typedef DExpression <Constant> Literal;
typedef DBinaryExpression <Variable , Literal, Add> VarLitAdder;
typedef DExpression <VarLitAdder> MyAdder;
Variable x ((Var()));
Literal l (Constant (50.00));
VarLitAdder vl_adder(x, l);
MyAdder expr (vl_adder);
std::vector <double> a;
a.push_back (10);
a.push_back (20);
// It is (50.00 + x) but does not look like it.
evaluate (a.begin(), a.end(), expr);
return 0;
}
這裡,與組合模式的類比非常有用。模板 DExpression 可以被認為是組合模式中的抽象基類。它捕獲介面中的共性。在表示式模板中,公共介面是過載的函式呼叫運算子。DBinaryExpression 是一個真實的組合體,也是一個介面卡,它將 Add 的介面適配為 DExpression 的介面。常量和 Var 是兩種不同型別的葉節點。它們也堅持 DExpression 的介面。DExpression 將 DBinaryExpression、Constant 和 Var 的複雜性隱藏在一個統一的介面後面,使它們能夠協同工作。任何二元運算子都可以代替 Add,例如 Divide、Multiply 等。
上面的示例沒有顯示遞迴型別是如何在編譯時生成的。此外,expr 看起來並不像數學表示式,但它確實是。下面的程式碼展示瞭如何使用以下過載的 + 運算子的重複例項化來遞迴地組合型別。
template< class A, class B >
DExpression<DBinaryExpression<DExpression<A>, DExpression<B>, Add> >
operator + (DExpression<A> a, DExpression<B> b)
{
typedef DBinaryExpression <DExpression<A>, DExpression<B>, Add> ExprT;
return DExpression<ExprT>(ExprT(a,b));
}
上面的過載運算子 + 做了兩件事——它添加了語法糖並啟用了遞迴型別組合,受編譯器限制的約束。因此,它可以用來替換對 evaluate 的呼叫,如下所示
evaluate (a.begin(), a.end(), x + l + x);
/// It is (2*x + 50.00), which does look like a mathematical expression.
- Blitz++ 庫
- Boost Spirit 解析器框架
- Boost 基本線性代數
- LEESA:使用 C++ 中的嵌入式查詢和遍歷語言進行本機 XML 程式設計
- std::valarray 由GNU libstdc++、LLVM libc++ 等實現。
- 表示式模板 - Klaus Kreft & Angelika Langer
- 表示式模板 - Todd Veldhuizen
- 使用模板實現更快的向量數學 - Tomas Arce