20 元編程

值元編程(Value Metaprogramming)

  • 常量表達(dá)式最初只能用 enum 來聲明
template<int N>
struct Fac {
  enum { value = N * Fac<N - 1>::value };
};

template<>
struct Fac<0> {
  enum { value = 1 };
};

static_assert(Fac<5>::value == 120);
  • C++98 允許在類內(nèi)部初始化 static int 常量
template<int N>
struct Fac {
  static const int value = N * Fac<N - 1>::value;
};

template<>
struct Fac<0> {
  static const int value = 1;
};

static_assert(Fac<5>::value == 120);
  • Modern C++ 中可以使用 constexpr虑粥,且不再局限于int
template<int N>
struct Fac {
  static constexpr auto value = N * Fac<N - 1>::value;
};

template<>
struct Fac<0> {
  static constexpr auto value = 1;
};

static_assert(Fac<5>::value == 120);
  • 但類內(nèi)初始化 static 成員存在一個(gè)問題,如果把結(jié)果按引用傳遞給函數(shù)
void f(const int&) {}
f(Fac<5>::value);
  • 由于函數(shù)參數(shù)是引用類型,編譯器必須傳遞 Fac<5>::value 的地址,而調(diào)用 f(Fac<5>::value) 必須類外定義 static 數(shù)據(jù)成員(ODR-use)。enum 沒有地址钙畔,不會(huì)有這個(gè)問題绑莺,因此早期一般偏向使用 enum
  • C++17 引入的 inline 成員變量允許多于一次定義蔓姚,解決了上述函數(shù)調(diào)用的地址問題。C++17中幸海,constexpr static 成員默認(rèn) inline
template<int N>
struct Fac {
  static inline constexpr auto value = N * Fac<N - 1>::value;
};

template<>
struct Fac<0> {
  static inline constexpr auto value = 1;
};

void f(const int&) {}
f(Fac<5>::value); // OK
  • C++14 的 constexpr 函數(shù)簡化了元編程的寫法祟身,用二分查找計(jì)算平方根的元編程實(shí)現(xiàn)是
template <int N, int L = 1, int R = N>
struct Sqrt {
  static constexpr auto M = L + (R - L) / 2;
  static constexpr auto T = N / M;
  static constexpr auto value =
      (T < M) ? Sqrt<N, L, M>::value : Sqrt<N, M + 1, R>::value;
};

template <int N, int M>
struct Sqrt<N, M, M> {  // 終止條件為 L == R
  static constexpr auto value = M - 1;
};

static_assert(Sqrt<10000>::value == 100);
  • C++14 的 constexpr 函數(shù)對(duì)相同功能的實(shí)現(xiàn)是
constexpr int Sqrt(T x) {
  if (x <= 1) return x;
  int l = 1;
  int r = x;
  while (l < r) {
    int m = l + (r - l) / 2;
    int t = x / m;
    if (m == t) {
      return m;
    } else if (m > t) {
      r = m;
    } else {
      l = m + 1;
    }
  }
  return l - 1;
}

static_assert(Sqrt(10000) == 100);

遞歸實(shí)例化的開銷

  • 下面是 Sqrt<10000>::value 的計(jì)算過程
Sqrt<10000, 1, 10000>::value => [M, T] = [5000,  2]
Sqrt<10000, 1,  5000>::value => [M, T] = [2500,  4]
Sqrt<10000, 1,  2500>::value => [M, T] = [1250,  8]
Sqrt<10000, 1,  1250>::value => [M, T] = [625,  16]
Sqrt<10000, 1,   625>::value => [M, T] = [313,  31]
Sqrt<10000, 1,   338>::value => [M, T] = [157,  63]
Sqrt<10000, 1,   157>::value => [M, T] = [79,  126]
Sqrt<10000, 80,  157>::value => [M, T] = [118,  84]
Sqrt<10000, 80,  118>::value => [M, T] = [99,  101]
Sqrt<10000, 100, 118>::value => [M, T] = [109,  91]
Sqrt<10000, 100, 109>::value => [M, T] = [104,  96]
Sqrt<10000, 100, 104>::value => [M, T] = [102,  98]
Sqrt<10000, 100, 102>::value => [M, T] = [101,  99]
Sqrt<10000, 100, 101>::value => [M, T] = [100, 100]
Sqrt<10000, 101, 101>::value => 100
  • 但編譯器計(jì)算下列表達(dá)式時(shí)奥务,將實(shí)例化所有分支
Sqrt<10000, 1, 10000>::value => [M, T] = [5000,  2]
(2 < 5000) ? Sqrt<10000, 1, 5000>::value : Sqrt<10000,5001,10000>::value
  • 整個(gè)計(jì)算過程產(chǎn)生了大量實(shí)例化,數(shù)量幾乎是 N 的兩倍袜硫。使用 IfThenElse 可以避免此問題氯葬,實(shí)例化數(shù)量級(jí)將降低為 log2(N)
template <bool COND, typename TrueType, typename FalseType>
struct IfThenElseT {
  using Type = TrueType;
};

template <typename TrueType, typename FalseType>
struct IfThenElseT<false, TrueType, FalseType> {
  using Type = FalseType;
};

template <bool COND, typename TrueType, typename FalseType>
using IfThenElse = typename IfThenElseT<COND, TrueType, FalseType>::Type;

template <int N, int L = 1, int R = N>
struct Sqrt {
  static constexpr auto M = L + (R - L) / 2;
  static constexpr auto T = N / M;
  static constexpr auto value =
      IfThenElse<(T < M), Sqrt<N, L, M>, Sqrt<N, M + 1, R>>::value;
};

template <int N, int M>
struct Sqrt<N, M, M> {
  static constexpr auto value = M - 1;
};

static_assert(Sqrt<10000>::value == 100);
  • 實(shí)際上直接使用 constexpr 函數(shù)是最簡單高效的做法

計(jì)算完整性(Computational Completeness)

  • 上述例子說明一個(gè)模板元編程一般包含以下部分
    • 狀態(tài)變量:即模板參數(shù)
    • 迭代構(gòu)造:通過遞歸
    • 路徑選擇:通過條件表達(dá)式或特化
    • 整型算法
  • 如果對(duì)遞歸實(shí)例化體和狀態(tài)變量的數(shù)量沒有限制,元編程就可以在編譯期對(duì)任何可計(jì)算對(duì)象進(jìn)行高效計(jì)算婉陷。但標(biāo)準(zhǔn)建議最多進(jìn)行 1024 層遞歸實(shí)例化帚称,因此實(shí)際開發(fā)中要有節(jié)制地使用模板元編程。某些情況下模板元編程是一個(gè)重要工具秽澳,尤其是實(shí)現(xiàn)一些對(duì)性能要求嚴(yán)格的算法

遞歸實(shí)例化與遞歸模板實(shí)參

template <typename T, typename U>
struct Doublify {};

template <int N>
struct Trouble {
  using LongType = Doublify<typename Trouble<N - 1>::LongType,
                            typename Trouble<N - 1>::LongType>;
};

template <>
struct Trouble<0> {
  using LongType = double;
};

Trouble<10>::LongType ouch;
  • 對(duì)于表達(dá)式 Trouble<N>::LongType 的類型描述闯睹,隨著遞歸層次的增加,代碼將越來越長担神,復(fù)雜度與 2^N 成正比
// Trouble<0>::LongType
double
// Trouble<1>::LongType
Doublify<double, double>
// Trouble<2>::LongType
Doublify<Doublify<double, double>, Doublify<double, double>>
  • 使用遞歸模板實(shí)參會(huì)強(qiáng)制編譯器生成更多的實(shí)例化體楼吃,編譯器要為每個(gè)類型保存一個(gè) mangled name,早期編譯器中 mangled name 的長度一般等于 template-id 的長度杏瞻,于是 Trouble<10>::LongType 可能產(chǎn)生一個(gè)長度大于 10000 個(gè)字符的 managled name所刀,雖然現(xiàn)在的編譯器使用了智能壓縮技術(shù),可以將 Trouble<10>::LongType 壓縮到幾百個(gè)字符捞挥,但最好避免在模板實(shí)參中遞歸實(shí)例化

類型元編程(Type Metaprogramming)

  • 以前討論過的 traits 只能進(jìn)行簡單的類型計(jì)算,通過元編程可以進(jìn)行更復(fù)雜的類型計(jì)算
template <typename T>
struct sp_element {
  using type = T;
};

template <typename T>
struct sp_element<T[]> {
  using type = typename sp_element<T>::type;
};

template <typename T, std::size_t N>
struct sp_element<T[N]> {
  using type = typename sp_element<T>::type;
};

template<typename T>
using sp_element_t = typename sp_element<T>::type;

static_assert(std::is_same_v<sp_element_t<int[]>, int>);
static_assert(std::is_same_v<sp_element_t<int[5][10]>, int>);
static_assert(std::is_same_v<sp_element_t<int[][10]>, int>);
static_assert(std::is_same_v<sp_element_t<int(*)[5]>, int(*)[5]>);
template <class T, T v>
struct integral_constant {
  static constexpr T value = v;
  using value_type = T;
  using type = integral_constant;  // using injected-class-name
  constexpr operator value_type() const noexcept { return value; }
  constexpr value_type operator()() const noexcept { return value; }
};
  • 可以直接繼承 std::integral_constant 來使用其中提供的元函數(shù)砌函,這種方式稱為元函數(shù)轉(zhuǎn)發(fā)
template <class T, T v>
struct A {
  static constexpr T value = v;
};

template <int N, int... Ns>
struct Max;

template <int N>
struct Max<N> : A<int, N> {};

template <int N1, int N2, int... Ns>
struct Max<N1, N2, Ns...> : A <int,
  N1 < N2 ? Max<N2, Ns...>::value : Max<N1, Ns...>::value> {};

template <int... Ns>
constexpr auto max_v = Max<Ns...>::value;

static_assert(max_v<3, 2, 1, 5, 4> == 5);

混合元編程(Hybrid Metaprogramming)

  • 在編譯期以編程的方式匯編小片帶運(yùn)行期效率的代碼稱為混合元編程。比如計(jì)算兩個(gè)數(shù)組的點(diǎn)積
template <typename T, std::size_t N>
auto dotProduct(const std::array<T, N>& a, const std::array<T, N>& b) {
  T res{};
  for (std::size_t i = 0; i < N; ++i) {
    res += a[i] * b[i];
  }
  return res;
}
  • 在一些機(jī)器上溜族,for 循環(huán)的匯編將產(chǎn)生分支指令讹俊,可能比直接寫出循環(huán)造成更大的開銷
res += x[0]*y[0];
res += x[1]*y[1];
res += x[2]*y[2];
res += x[3]*y[3];
  • 不過現(xiàn)代編譯器將優(yōu)化循環(huán)為目標(biāo)平臺(tái)最高效形式。使用元編程可以展開循環(huán)煌抒,雖然現(xiàn)在已經(jīng)沒有這個(gè)必要仍劈,但出于討論還是給出實(shí)現(xiàn)
template <typename T, std::size_t N>
struct DotProductT {
  static inline T value(const T* a, const T* b) {
    return *a * *b + DotProductT<T, N - 1>::value(a + 1, b + 1);
  }
};

template <typename T>
struct DotProductT<T, 0> {
  static inline T value(const T*, const T*) { return T{}; }
};

template <typename T, std::size_t N>
auto dotProduct(const std::array<T, N>& x, const std::array<T, N>& y) {
  return DotProductT<T, N>::value(&*begin(x), &*begin(y));
}
  • std::array 只是定長數(shù)組,實(shí)際上混合元編程中最強(qiáng)大的容器是 std::tuple寡壮,它可以包含任意數(shù)量任意類型的元素
std::tuple<int, std::string, bool> t{42, "Answer", true};

對(duì) Unit Type 的混合元編程

  • 一些庫用來計(jì)算不同 unit type 值結(jié)果贩疙,也體現(xiàn)了混合元編程的能力
  • 以時(shí)間為例,主要的 unit 是秒况既,一毫秒就可以用 1/1000 來表示这溅,一分鐘則用 60/1 表示,為此先定義一個(gè)用于表示 unit 比值的類型 Ratio
#include <iostream>

template <unsigned N, unsigned D = 1>
struct Ratio {
  static constexpr unsigned num = N;  // 分子
  static constexpr unsigned den = D;  // 分母
  using Type = Ratio<num, den>;
};

template <typename R1, typename R2>
struct RatioAddImpl {
 private:
  static constexpr unsigned den = R1::den * R2::den;
  static constexpr unsigned num = R1::num * R2::den + R2::num * R1::den;

 public:
  using Type = Ratio<num, den>;
};

template <typename R1, typename R2>
using RatioAdd = typename RatioAddImpl<R1, R2>::Type;

int main() {
  using R1 = Ratio<1, 1000>;
  using R2 = Ratio<2, 3>;
  using RS = RatioAdd<R1, R2>;                     // Ratio<2003,3000>
  std::cout << RS::num << '/' << RS::den << '\n';  // 2003/3000
  using RA = RatioAdd<Ratio<2, 3>, Ratio<5, 7>>;   // Ratio<29,21>
  std::cout << RA::num << '/' << RA::den << '\n';  // 29/21
}
  • 基于此模板可以定義一個(gè)表示時(shí)間的類模板
#include <iostream>

template <unsigned N, unsigned D = 1>
struct Ratio {
  static constexpr unsigned num = N;  // 分子
  static constexpr unsigned den = D;  // 分母
  using Type = Ratio<num, den>;
};

template <typename R1, typename R2>
struct RatioAddImpl {
 private:
  static constexpr unsigned den = R1::den * R2::den;
  static constexpr unsigned num = R1::num * R2::den + R2::num * R1::den;

 public:
  using Type = Ratio<num, den>;
};

template <typename R1, typename R2>
using RatioAdd = typename RatioAddImpl<R1, R2>::Type;

template <typename T, typename U = Ratio<1>>
class Duration {
 public:
  using ValueType = T;
  using UnitType = typename U::Type;  // 時(shí)間的基本單位

 private:
  ValueType val;

 public:
  constexpr Duration(ValueType v = 0) : val(v) {}
  constexpr ValueType value() const { return val; }
};

template <typename T1, typename U1, typename T2, typename U2>
constexpr auto operator+(const Duration<T1, U1>& a, const Duration<T2, U2>& b) {
  using VT = Ratio<1, RatioAdd<U1, U2>::den>;  // Ratio<1, U1::den * U2::den>
  auto res =
      (a.value() * U1::num / U1::den + b.value() * U2::num / U2::den) * VT::den;
  return Duration<decltype(res), VT>{res};
}

int main() {
  auto a = Duration<int, Ratio<1, 1000>>(10);   // 10 毫秒
  auto b = Duration<double, Ratio<1, 3>>(7.5);  // 2.5 秒
  auto c = a + b;  // c = 10 * 3 + 7.5 * 1000 = 7530 * 1/3000 秒
  using Type = decltype(c)::UnitType;
  std::cout << c.value() << "*" << Type::num << '/' << Type::den;
}
  • 此外棒仍,operator+ 是constexpr悲靴,如果值在編譯期已知,就能直接在編譯期計(jì)算莫其,std::chrono 使用的就是這個(gè)方法癞尚,并做了一些改進(jìn)耸三,比如使用預(yù)定義 unit(如 std::chrono::milliseconds)來支持時(shí)間字面量和溢出處理
#include <iostream>
#include <chrono>

constexpr std::chrono::seconds operator ""s(unsigned long long s) {
  return std::chrono::seconds(s);
}

constexpr std::chrono::minutes operator ""min(unsigned long long m) {
  return std::chrono::minutes(m);
}

constexpr std::chrono::hours operator ""h(unsigned long long h) {
  return std::chrono::hours(h);
}

int main() {
  using namespace std::chrono_literals;
  std::cout << (1h + 1min + 30s).count(); // 3690
}

反射

  • 模板元編程的方案選擇必須從三個(gè)角度考慮:
    • Computation(計(jì)算)
    • Reflection(反射):以編程方式檢查程序特征的能力
    • Generation(生成):為程序生成附加代碼的能力
  • 前面提到了遞歸實(shí)例化和 constexpr 函數(shù)兩種方案。為了進(jìn)行反射浇揩,type traits 中可以找到部分解決方案仪壮,但遠(yuǎn)不能覆蓋反射所需的全部內(nèi)容。比如給定一個(gè)類類型临燃,許多應(yīng)用程序都希望以編程方式探索該類的成員【Σ担現(xiàn)有的 traits 基于模板實(shí)例化,因此 C++ 可以提供附加的語言能力或內(nèi)在庫組件膜廊,在編譯期生成包含反射信息的類模板實(shí)例乏沸。這是匹配基于遞歸模板實(shí)例化的方案,但類模板實(shí)例會(huì)消耗大量編譯器空間爪瓜,直到編譯結(jié)束時(shí)才能釋放蹬跃。另一種與 constexpr 計(jì)算匹配的方案是引入一種新的標(biāo)準(zhǔn)類型來表示反射信息
  • 在 C++ 中創(chuàng)建一個(gè)靈活的、通用的和程序員友好的代碼生成機(jī)制仍是各方正在研究的挑戰(zhàn)铆铆,但模板實(shí)例化也勉強(qiáng)算是一種代碼生成機(jī)制蝶缀,此外,編譯器擴(kuò)展內(nèi)聯(lián)函數(shù)的調(diào)用已經(jīng)足夠可靠薄货,該機(jī)制也可以用作代碼生成的工具翁都。結(jié)合更強(qiáng)大的反射能力,現(xiàn)有技術(shù)已能實(shí)現(xiàn)卓越的元編程效果
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末谅猾,一起剝皮案震驚了整個(gè)濱河市柄慰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌税娜,老刑警劉巖坐搔,帶你破解...
    沈念sama閱讀 212,816評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異敬矩,居然都是意外死亡概行,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門弧岳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來凳忙,“玉大人,你說我怎么就攤上這事缩筛∠裕” “怎么了?”我有些...
    開封第一講書人閱讀 158,300評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵瞎抛,是天一觀的道長艺演。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么胎撤? 我笑而不...
    開封第一講書人閱讀 56,780評(píng)論 1 285
  • 正文 為了忘掉前任晓殊,我火速辦了婚禮,結(jié)果婚禮上伤提,老公的妹妹穿的比我還像新娘巫俺。我一直安慰自己,他們只是感情好肿男,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,890評(píng)論 6 385
  • 文/花漫 我一把揭開白布介汹。 她就那樣靜靜地躺著,像睡著了一般舶沛。 火紅的嫁衣襯著肌膚如雪嘹承。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,084評(píng)論 1 291
  • 那天如庭,我揣著相機(jī)與錄音叹卷,去河邊找鬼。 笑死坪它,一個(gè)胖子當(dāng)著我的面吹牛骤竹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播往毡,決...
    沈念sama閱讀 39,151評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼蒙揣,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了开瞭?” 一聲冷哼從身側(cè)響起鸣奔,我...
    開封第一講書人閱讀 37,912評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎惩阶,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體扣汪,經(jīng)...
    沈念sama閱讀 44,355評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡断楷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,666評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了崭别。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片冬筒。...
    茶點(diǎn)故事閱讀 38,809評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖茅主,靈堂內(nèi)的尸體忽然破棺而出舞痰,到底是詐尸還是另有隱情,我是刑警寧澤诀姚,帶...
    沈念sama閱讀 34,504評(píng)論 4 334
  • 正文 年R本政府宣布响牛,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏呀打。R本人自食惡果不足惜矢赁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,150評(píng)論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望贬丛。 院中可真熱鬧撩银,春花似錦、人聲如沸豺憔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽恭应。三九已至抄邀,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間暮屡,已是汗流浹背撤摸。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評(píng)論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留褒纲,地道東北人准夷。 一個(gè)月前我還...
    沈念sama閱讀 46,628評(píng)論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像莺掠,于是被迫代替她去往敵國和親衫嵌。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,724評(píng)論 2 351

推薦閱讀更多精彩內(nèi)容

  • C++有許多支持編譯期編程的特性C++98前彻秆,模板提供了編譯期計(jì)算的能力楔绞,包括使用循環(huán)和執(zhí)行路徑選擇使用偏特化,可...
    奇點(diǎn)創(chuàng)客閱讀 506評(píng)論 0 0
  • 本文按照 cppreference[https://en.cppreference.com/w/] 列出的特性列表...
    401閱讀 20,918評(píng)論 2 18
  • 1. C++14 參考文檔[https://en.cppreference.com/w/cpp/14] 不同于重量...
    Platanuses閱讀 792評(píng)論 0 1
  • 模板是在編譯時(shí)實(shí)例化的唇兑。C++ 的某些功能可以與實(shí)例化過程結(jié)合酒朵,成為一種原始的遞歸“編程語言”。C++ 有多種功能...
    找不到工作閱讀 687評(píng)論 0 0
  • 01 一個(gè)實(shí)例:累加一個(gè)序列 1.1 Fixed Traits 上述代碼的問題是扎附,對(duì)于 char 類型希望計(jì)算對(duì)應(yīng)...
    奇點(diǎn)創(chuàng)客閱讀 289評(píng)論 0 0