LoginSignup
15
9

More than 5 years have passed since last update.

C++テンプレートの学習にstatic_assertだけで『テスト駆動開発』多国通貨を写経する

Last updated at Posted at 2018-05-06

はじめに

C++ Template Meta Programmingの学習がしたくてやりました。

C++の勉強を始めて半年近く、そろそろテンプレートに手を付けたいと考えていました。けど、Template Meta Programmingって敷居高いですよね。既存ライブラリになんて中々手が出せないですし。
悶々とどうしようかな、と考えていたところ、別件でKent Beckのテスト駆動開発をパラパラと見返している時、ふと思いつきました。

これ、static_assertだけでやったらテンプレートの練習にならんかな?

結果として、一部不満が残るものの、テンプレートを使ってstatic_assertだけで一通り写経を完了することができました。
十分に小さいステップで1つずつテンプレートテクニックを試すことができ、楽しく学習できると感じました。テンプレートやってみたいけど、なかなか一歩が踏み出せない方々の参考になるかと思い、やったこと、学習できたこと、ハマったところ、を共有します。

なぜ、多国通貨の写経をstatic_assertだけで?

Template Meta Programmingを勉強したいと考えており、動いて楽しく、かつ、ぼちぼちの難易度の課題を探していました(楽しいは正義)。
主要な用途がライブラリ実装なためか、自分で何かを軽く作ってみたいなぁ、に対する良い感じの課題が思いつきませんでした。平凡なプログラマなので、既存ライブラリに手を付けるのは壁が高かったです。
また、(非常に参考にさせて頂いていますが)既存の書籍や記事はテクニック集が多く、軽く遊べるものが欲しい、という目的に沿うものが見つけられませんでした。

おそらく、テンプレートがライブラリ実装で効力を発揮するので、今回やったアプリケーションでの利用は本来の趣旨から外れていると思います。実際に、テンプレートで重要なテクニックであるはずの、decltypeやvariadic templateなどは試せていません。あくまで、テンプレート初心者が最初に遊んでみて足掛かりにする、という趣旨です。

『テスト駆動開発』多国通貨の例

単純すぎず、難しすぎない程度の難易度です(書籍を読んだことのある方はわかると思います)。ポリモーフィズムの扱いもあり、各ステップを非常に小さく記述してくれています。

static_assert

コンパイル時アサート

static_assert宣言は、指定した定数式が真であることを表明するための機能である。
これは、コンパイル時に満たされるべき要件を検証するために使用できる。

つまり、静的に全ての値が確定している必要があります。これはポリモーフィズムも含めて全て静的に解決する必要があることを意味します。

多国通貨の例×static_assert

多国通貨で動的ポリモーフィズムを使っている部分を、全てテンプレートで静的ポリモーフィズムにより、解決します。
書籍通り進めれば、各ステップは十分に小さいため、テンプレートテクニックを1つずつ試しながら、学習できると考えました。

やったこと

テスト駆動開発第1部~多国通貨~(第1章-第16章)のテストケースを全てstatic_assertで書いて、テストをパスするコードを書きました。

開発環境

参考までに。

項目 内容
OS Ubuntu 16.04
ビルドツール cmake 3.11.2 (minimum 3.9)
コンパイラ gcc 5.4.0 (c++14) gcc 8.0.1 (c++17)
テストフレームワーク googletest (何でもよかった)

とりあえずお仕事(で使っているクロスコンパイラ)でも使えるgcc versionとc++14を対象としています。
constexprなmap実現のために、c++17を使用することにしました(2018/5/20)。
コンパイルエラー発生時、何度かWandbox(使ってコンパイラ依存のエラーでないか、を確認しました。

学習できたこと

※完全理解ではないです。

constexpr色々(コンストラクタ、関数など)

お題上、ありとあらゆるところでconstexprを書くので、constexprの勉強になります。
ハマったこと、に詳細を記載しますが、c++11⇒c++14でconstexprの仕様にかなりの改善があり、サンプルコードがコンパイルエラーになったり、苦しみながらも勉強できました。

SFINAE (Substitution Failure Is Not An Error)

第7章~疑念をテストに翻訳する~で、DollarオブジェクトとFrancオブジェクトを比較すると、等価でない、と判定する処理を実装します。ここで、SFINAEを使いました。
他にもっと良いやり方がある気がしますが、今はわかりません。今後の学習課題です。

// T, UがMoneyを基底クラスとしており、同じクラスのオブジェクトである場合、amountを参照して等価性を評価する。
// ==オペレータにDollarとFrancを渡すとこのテンプレートへの置き換えに失敗するが、即時エラーにならず、下のテンプレートを展開しにいってくれる。
template <class T, class U,
            typename std::enable_if<
              std::is_base_of<Money, T>::value &&
              std::is_base_of<Money, U>::value &&
              std::is_same<T, U>::value
            >::type* = nullptr
          >
constexpr bool operator==(const T& rhs, const U& lhs) {
  return rhs.amount() == lhs.amount();
}

// T, UがMoneyを基底クラスとしているが、別クラスのオブジェクトである場合、一律で等価でない、と評価する。
template <class T, class U,
            typename std::enable_if<
              std::is_base_of<Money, T>::value &&
              std::is_base_of<Money, U>::value &&
              !std::is_same<T, U>::value
            >::type* = nullptr
          >
constexpr bool operator==(const T&, const U&) {
  return false;
}

↑は作成途中のもので、この後、グリーン状態でもう少し試行錯誤していました。
グリーン状態でテンプレートテクニックを試行錯誤できる、というは初期の学習において、かなりのメリットと感じました。

CRTP (Curiously Reccursive/Reccuring Template Pattern)

第9章~歩幅の調整~では、抽象メソッドcurrency()が定義されたMoneyを基底クラスとして、派生クラスのDollarFrancを実装します。
ポリモーフィズムを利用するため、テンプレートで静的ポリモーフィズムを実現できるCRTPを使いました。
教科書通りで実装できました。

ソースコード
// 通貨識別用
enum class Currency {
  kUSD, kCHF, kNoCurrency
};

// テンプレートパラメータとして派生クラスの型を持つ。
template <class Derived>
class Money {
 public:
    constexpr Money(int32_t amount) :amount_{amount} {}
    // currency()は保持している通貨を返す
    constexpr Currency currency() const {
      // 派生クラスのcurrency()を呼び出す。
      return static_cast<const Derived&>(*this).currency();
    }
...
};

class Dollar : public Money<Dollar> {
 public:
    constexpr Dollar(int32_t amount) : Money{amount} {}
    constexpr Currency currency() const {
      // DollarクラスはUSDを返す
      return Currency::kUSD;
    }
};
テストコード
TEST_F(MoneyTest, Currency) {
  // Dollarをテンプレートパラメータに指定して、Moneyのインスタンスを作る。currency()はDollarクラスのcurrency()を呼び出す。
  static_assert(Money<Dollar>{1}.currency() == Currency::kUSD, "Dollar must have USD currency.");
  static_assert(Money<Franc>{1}.currency() == Currency::kCHF, "Franc must have CHF currency.");
}

書籍の後半では、Expressionを基底クラスとして、派生クラスのMoneySumを実装します。
こちらは、DollarFrancの実装よりもかなり苦労しました。何回か積んだか?と思うこともありました。

書籍を読んでない方向けの補足

Expressionは抽象メソッドreduce()を定義する基底クラスです。reduce()は式を単純な形に変形(簡約)するという意味があります(『テスト駆動開発pp.83 翻訳者注』)。
つまり、何らかの式(Expressionオブジェクト)に対して、reduce()すると、お金(Money)が得られるようにします。
例えば、5ドル+10フランという式を与えると、銀行(Bank)が欲しい通貨(Currency)の為替レートに基づいて、お金(Money)を返してくれる、という寸法です。

使い方のサンプル
  // 10フラン=5ドルの為替レート
  auto sum = money::dollar(5) + money::franc(10);
  Money result = sum.reduce(bank, Currency::kUSD);
  static_assert(result == money:dollar(10), "5 USD + 10 Franc = 10 USD.");

実際のシーケンスは、次のような呼び出しになります。

Bank::reduce(bank, to)
  ⇒Sum<Expression<Sum>>::reduce(bank, to)  // 5 USD + 5 USD
    ⇒Money<Expression<Money>>::reduce(bank, to) // 5 USD ⇒ 5 USD
    ⇒Money<Expression<Money>>::reduce(bank, to) // 10 Franc ⇒ 5 USD

Expression, Money, Sumの実装

Expressionは派生クラスの型をテンプレートパラメータに持ち、抽象メソッドreduce()を定義しています。教科書通りのCRTPで、特にひねりはありません。
Bankがintのテンプレートパラメータを持っていてダサい理由は、ハマったことで後述します。

Expressionの実装
// Forward declarations.
class Money;
template <int32_t N>
class Bank;

template <class Derived>
class Expression {
 public:
    constexpr Expression() = default;
    template <int32_t N>
    constexpr Money reduce(const Bank<N>& bank, const Currency to) const;
};

// Moneyの定義

template <class Derived>
template <int32_t N>
constexpr Money Expression<Derived>::reduce(const Bank<N>& bank, const Currency to)const {
  return static_cast<const Derived&>(*this).reduce(bank, to);
}

Moneyも教科書通りです。Moneyは自身の金額を返す式(Expression)です。
Bankから、為替レート(自身の通貨⇒変換対象の通貨)を受け取り、新しいMoneyオブジェクトを作成して返しています。

Moneyの実装
class Money : public Expression<Money> {
 public:
    constexpr Money(int32_t amount, Currency currency = Currency::kNoCurrency)
        : amount_{amount}, currency_{currency} {}

    // Implements Expression interface.
    template <int32_t N>
    constexpr Money reduce(const Bank<N>& bank, const Currency to) const;

// 中略

 private:
    int32_t amount_;
    Currency currency_;
};

// Bankの定義

template <int32_t N>
constexpr Money Money::reduce(const Bank<N>& bank, const Currency to) const {
  int rate = bank.rate(currency_, to);
  return Money(amount_ / rate, to);
}

苦労したSumの実装です。Sumはaugend(足される数)とaddend(足す数)を持ち、その合計を返す式(Expression)です。
augendとaddendの型をテンプレート化する、という方法に至れば、すぐでした(が、散々頭を悩ませました。なぜかオーバーロードで何とかしようとしたり…)。
SumはMoney+Moneyを表す場合もあれば、Sum+Moneyを表す場合もあります(Sum+Sumの場合もあるでしょう)。augend/addendがSumの場合、Sumは、augend/addendのreduce()を呼び出します。ほぼ、Compositeパターンですね。

Sumの実装
template <class T, class U>
class Sum : public Expression<Sum<T, U>> {
 public:
    constexpr Sum(const T& augend, const U& addend)
        : augend_{augend}, addend_{addend} {}

    // Implements Expression interface.
    template <int32_t N>
    constexpr Money reduce(const Bank<N>& bank, const Currency to) const {
      auto amount = augend_.reduce(bank, to).amount()
         + addend_.reduce(bank, to).amount();
      return Money{amount, to};
    }

 private:
    T augend_;
    U addend_;
};

ハマったこと

c++11⇒c++14 非静的メンバ関数の、暗黙のconst修飾を削除

Moneyクラスのconstexprコンストラクタを作るところからスタートしました。
で、いきなりハマりました。

cpprefjp constexprのサンプルがコンパイルエラーになったのです。

2018/5/13追記

@yumetodo さんにfix(C++11constexpr): constexpr member function should be const member…で反映頂きました。

cpprefjpのconstexprコンストラクタサンプル
class Integer {
  int value_;
public:
  constexpr Integer(int value)
    : value_(value) {}

  constexpr int get()
  { return value_; }
};

int main()
{
  constexpr Integer x = 3;
  static_assert(x.get() == 3, "x value must be 3");
}

Wandboxコンパイルエラー再現

prog.cc:14:23: error: passing 'const Integer' as 'this' argument discards qualifiers [-fpermissive]
static_assert(x.get() == 3, "x value must be 3");

↑のようにconst修飾に関するエラーが発生します。

実験しているとc++11ではコンパイルエラーにならないことがわかりました。
c++11⇒c++14で、非静的メンバ関数の、暗黙のconst修飾を削除という仕様改善があったことが分かり、メンバ関数にconst修飾することで解決しました。

まだハマり中のもの

constexprなmap

Bankは為替レートのテーブルを持っています。このテーブルは為替レートの追加が可能なように作られています(書籍内では)。
追加可能なmapをconstexprでどのように表現すれば良いのか、が分かっておらず、未だに誤魔化し実装になっています。

無様なBankの実装
// 持っている為替レート数をテンプレートパラメータにしている
template <int32_t N>
class Bank {
  using Hash = std::pair<const Currency, const Currency>;
  using Rate = std::pair<Hash, int>;
 // std::arrayで為替レートテーブルを表現
 private:
    std::array<Rate, N> rates_;

 public:
    constexpr Bank(const std::array<Rate, N>& rates) : rates_{rates} {}

    // 為替レート数を+1したBankを新しく作る
    constexpr Bank<N+1> addRate(const Currency from, const Currency to, int32_t rate) const {
      // 既存のarrayに1つ為替レートを追加する方法が分からず、為替レートは1つに固定されている…。
      std::array<Rate, 1> rates = { Rate{Hash{from, to}, rate} };  // ☆
      return Bank<N+1>{{rates}};
    }
};

☆のところで、↓のような感じのことがしたいです。std::arrayをinitializer listに変換できれば良いと思うのですが…。

  std::array<Rate, N+1> new_rates = { rates_, Rate{Hash{from, to}, rate} };

c++20でvectorをconstexprにしようとしているみたいですが、そこまでは待てないですね…。

c++17 std::arrayによる解決 2018/5/20追記

c++17では、std::array要素の参照がconstexprになりました。(c++14ではconst参照はconstexpr)
この修正により、constexpr内でstd::arrayの要素が読み書きできます。

arrayをconstexpr関数で更新する
template <int N>
constexpr static std::array<int, N> createTable() {
  std::array<int, N> a{};
  for (int i = 0; i < N; ++i) {
    a[i] = i;
  }
  return a;
}

int main() {
  constexpr auto t1 = createTable<1>();
  constexpr auto t2 = createTable<1>();
  static_assert(t1[0] == t2[0], "can build.");
}

c++14の結果(コンパイルエラー)
c++17の結果

これと、コメント欄で頂いたアドバイスを組み合わせて、なんとか形になりました。@yumetodoさん、@ygsiroさん、ありがとうございました。

主な変更点

  • c++17のstd::arrayを使う
  • Rateをstd::pairでなくユーザー定義型にする(std::pairはconstexprな代入不可?)
  • mapをimmutableな固定長配列にする(取り扱い通貨数からmapのエントリ数を固定で算出)
やや無様でなくなったBankの実装
// もにょもにょとヘルパー関数を準備
namespace internal {
// A currency is exchanged to the other (means, n-1) currencies.
constexpr static size_t CalcTradingRateEntry(const uint32_t n) {
  return n*(n-1);
}

constexpr static size_t kNumRateEntry
    = CalcTradingRateEntry(kNumTradingCurrency);

constexpr size_t FindExistingEntry(const std::array<Rate, kNumRateEntry>& rates,
                                   const Rate& target, const size_t filled_index) {
  size_t i = 0;
  for (; i < filled_index; ++i) {
    if (rates[i] == target)
      break;
  }
  return i;
}
}  // namespace internal

// Bankはテンプレート不要になった
class Bank {
 public:
    constexpr Bank(): rates_{}, index_for_new_entry_{0} {}
    constexpr Bank(const std::array<Rate, internal::kNumRateEntry>& rates, const size_t filled)
        : rates_{rates}, index_for_new_entry_{filled} {}

    // Implements Expression interface.
    template <class T>
    constexpr Money reduce(const Expression<T>& source, const Currency to) const {
      return source.reduce(*this, to);
    }

    constexpr Bank addRate(const Currency from, const Currency to, const int32_t rate) const {
      // 既存の変換レートmapをコピーコンストラクトする
      std::array<Rate, internal::kNumRateEntry> new_rates{rates_};
      size_t index_for_new_entry = index_for_new_entry_;
      const Rate target(from, to, rate);

      // 新しいエントリ挿入 or 既存エントリの更新
      size_t index = internal::FindExistingEntry(rates_, target, index_for_new_entry_);
      if (index == index_for_new_entry_) {  // 新しいエントリの挿入
        new_rates[index] = target;
        index_for_new_entry++;
      } else {  // 既存エントリの更新
        new_rates[index].rate = rate;
      }

      return Bank{new_rates, index_for_new_entry};
    }
    constexpr int32_t rate(const Currency from, const Currency to) const {
      if (from == to) return 1;
      return findRate(from, to);
    }

 private:
    constexpr int32_t findRate(const Currency from, const Currency to) const {
      Rate target(from, to, 1);
      for (size_t i = 0; i < index_for_new_entry_; ++i) {
        if (rates_[i] == target)
          return rates_[i].rate;
      }
      return 0;
    }

 private:
    // mapは固定長配列で確保する
    std::array<Rate, internal::kNumRateEntry> rates_;
    // 新しい通貨変換レートが追加されたときに使う配列の添字
    size_t index_for_new_entry_;
};

もう少しうまく作れる気がしますが、一旦満足しました。
今後は、c++14で動くようにしていきたいですね。

Sum宣言の型

5 USD + 10 Franc + 5 USDを表す式(Expression)を作ろうとすると、今は↓のように書かないとダメです。イケてません。
ヘルパー関数を用意して、型推論させれば、できそう、ということしか分かっていません。

TEST_F(MoneyTest, SumPlusMoney) {
  constexpr auto five_bucks = money::dollar(5);
  constexpr auto ten_francs = money::franc(10);
  constexpr auto sum = Sum<Sum<Money, Money>, Money>{
    Sum<Money, Money>{five_bucks, ten_francs} + five_bucks};
}

2018/5/13追記

@yumetodo さんより指摘を頂き、下記でコンパイル可能であることを確認しました。

  constexpr auto five_bucks = money::dollar(5);
  constexpr auto ten_francs = money::franc(10);
  constexpr auto sum = (five_bucks + ten_francs) + five_bucks;
}

ソースコード

github

ソースコード
namespace money {

// Forward declarations.
class Money;
template <class T, class U>
class Sum;
template <int32_t N>
class Bank;

enum class Currency {
  kUSD, kCHF, kNoCurrency
};

template <class Derived>
class Expression {
 public:
    constexpr Expression() = default;
    template <int32_t N>
    constexpr Money reduce(const Bank<N>& bank, const Currency to) const;
};

class Money : public Expression<Money> {
 public:
    constexpr Money(int32_t amount, Currency currency = Currency::kNoCurrency)
        : amount_{amount}, currency_{currency} {}

    // Implements Expression interface.
    template <int32_t N>
    constexpr Money reduce(const Bank<N>& bank, const Currency to) const;

    constexpr friend bool operator==(const Money& lhs, const Money& rhs) {
      return (lhs.amount_ == rhs.amount_) && (lhs.currency_ == rhs.currency_);
    }

    // Accessors.
    constexpr Currency currency() const {
      return currency_;
    }
    constexpr int32_t amount() const{
      return amount_;
    }

 private:
    int32_t amount_;
    Currency currency_;
};

template <int32_t N>
class Bank {
  using Hash = std::pair<const Currency, const Currency>;
  using Rate = std::pair<Hash, int>;

 public:
    constexpr Bank(const std::array<Rate, N>& rates) : rates_{rates} {}

    // Implements Expression interface.
    template <class T>
    constexpr Money reduce(const Expression<T>& source, const Currency to) const {
      return source.reduce(*this, to);
    }

    constexpr Bank<N+1> addRate(const Currency from, const Currency to, int32_t rate) const {
      std::array<Rate, 1> rates = { Rate{Hash{from, to}, rate} };
      return Bank<N+1>{{rates}};
    }
    constexpr int32_t rate(const Currency from, const Currency to) const {
      if (from == to) return 1;
      return findRate(from, to);
    }

 private:
    constexpr int32_t findRate(const Currency from, const Currency to) const {
      for (auto i = 0; i < N; ++i) {
        auto hash = rates_[i].first;
        if (hash.first == from && hash.second == to)
          return rates_[i].second;
      }
      return 0;
    }

 private:
    std::array<Rate, N> rates_;
};

template <class T, class U>
class Sum : public Expression<Sum<T, U>> {
 public:
    constexpr Sum(const T& augend, const U& addend)
        : augend_{augend}, addend_{addend} {}

    // Implements Expression interface.
    template <int32_t N>
    constexpr Money reduce(const Bank<N>& bank, const Currency to) const {
      auto amount = augend_.reduce(bank, to).amount()
         + addend_.reduce(bank, to).amount();
      return Money{amount, to};
    }

    // Accessors.
    constexpr T augend() const { return augend_; }
    constexpr U addend() const { return addend_; }

 private:
    T augend_;
    U addend_;
};

template <class Derived>
template <int32_t N>
constexpr Money Expression<Derived>::reduce(const Bank<N>& bank, const Currency to)const {
  return static_cast<const Derived&>(*this).reduce(bank, to);
}

template <int32_t N>
constexpr Money Money::reduce(const Bank<N>& bank, const Currency to) const {
  int rate = bank.rate(currency_, to);
  return Money(amount_ / rate, to);
}

// Factory methods.
constexpr Money dollar(int32_t amount) {
  return Money{amount, Currency::kUSD};
}

constexpr Money franc(int32_t amount) {
  return Money{amount, Currency::kCHF};
}

// Operators.
template <class T, class U>
constexpr Sum<T, U> operator+(const Expression<T>& lhs, const Expression<U>& rhs) {
  return Sum<T, U>{static_cast<T const&>(lhs), static_cast<U const&>(rhs)};
}

template <class T>
constexpr T operator*(const Expression<T>& lhs, const int32_t multiplier) {
  return T(static_cast<T const&>(lhs).augend() * multiplier,
           static_cast<T const&>(lhs).addend() * multiplier);
}

constexpr Money operator*(const Money& lhs, const int32_t multiplier) {
  return Money{lhs.amount() * multiplier, lhs.currency()};
}

}  // namespace money
テストコード
namespace money_test{
using money::Money;
using money::Currency;
using money::Expression;
using money::Bank;
using money::Sum;

class MoneyTest : public ::testing::Test {
};

TEST_F(MoneyTest, Multiplication) {
  constexpr auto five = money::dollar(5);
  static_assert((five*2) == money::dollar(10), "product must be 10.");
  static_assert((five*3) == money::dollar(15), "product must be 15.");
}

TEST_F(MoneyTest, Equality) {
  static_assert(money::dollar(5) == money::dollar(5), "Two objects must be same.");
  static_assert(!(money::dollar(5) == money::dollar(6)),
    "Two objects must be different.");
  static_assert(!(money::franc(5) == money::dollar(5)),
    "Two objects must be different.");
}

TEST_F(MoneyTest, Currency) {
  static_assert(money::dollar(1).currency() == Currency::kUSD,
    "Dollar must have USD currency.");
  static_assert(money::franc(1).currency() == Currency::kCHF,
    "Franc must have CHF currency.");
}

TEST_F(MoneyTest, SimpleAddition) {
  constexpr Money five = money::dollar(5);
  constexpr auto sum = five + five;
  constexpr Bank<0> bank{{}};
  constexpr Money reduced = bank.reduce(sum, Currency::kUSD);
  static_assert(reduced == money::dollar(10), "sum must be 10 USD.");
}

TEST_F(MoneyTest, PlusReturnsSum) {
  constexpr Money five = money::dollar(5);
  constexpr auto sum = five + five;
  static_assert(sum.augend() == five, "Augend must be five dollar.");
  static_assert(sum.addend() == five, "Addend must be five dollar.");
}

TEST_F(MoneyTest, ReduceSum) {
  constexpr auto sum = money::dollar(3) + money::dollar(4);
  constexpr Bank<0> bank{{}};
  constexpr Money result = bank.reduce(sum, Currency::kUSD);
  static_assert(result == money::dollar(7), "Result must be seven dollar.");
}

TEST_F(MoneyTest, ReduceMoney) {
  constexpr Bank<0> bank{{}};
  constexpr Money result = bank.reduce(money::dollar(1), Currency::kUSD);
  static_assert(result == money::dollar(1), "Result must be one dollar.");
}

TEST_F(MoneyTest, ReduceMoneyDifferentCurrency) {
  constexpr Bank<0> bank{{}};
  constexpr auto new_bank = bank.addRate(Currency::kCHF, Currency::kUSD, 2);
  constexpr Money result = new_bank.reduce(money::franc(2), Currency::kUSD);
  static_assert(result == money::dollar(1), "Two franc must be one dollar.");
}

TEST_F(MoneyTest, IdentityRate) {
  constexpr Bank<0> bank{{}};
  static_assert(bank.rate(Currency::kUSD, Currency::kUSD) == 1,
    "Must be one if two currecy are same.");
}

TEST_F(MoneyTest, MixedAddition) {
  constexpr auto five_bucks = money::dollar(5);
  constexpr auto ten_francs = money::franc(10);
  constexpr Bank<0> empty_bank{{}};
  constexpr auto bank = empty_bank.addRate(Currency::kCHF, Currency::kUSD, 2);
  constexpr Money result = bank.reduce(five_bucks + ten_francs, Currency::kUSD);
  static_assert(result == money::dollar(10), "5 USD + 10 CHF must be 10 USD.");
}

TEST_F(MoneyTest, SumPlusMoney) {
  constexpr auto five_bucks = money::dollar(5);
  constexpr auto ten_francs = money::franc(10);
  constexpr Bank<0> empty_bank{{}};
  constexpr auto bank = empty_bank.addRate(Currency::kCHF, Currency::kUSD, 2);
  constexpr auto sum = Sum<Sum<Money, Money>, Money>{
    Sum<Money, Money>{five_bucks, ten_francs} + five_bucks};
  constexpr Money result = bank.reduce(sum, Currency::kUSD);
  static_assert(result == money::dollar(15), "5 USD + 10 CHF + 5 USD must be 15 USD.");
}

TEST_F(MoneyTest, SumTimes) {
  constexpr auto five_bucks = money::dollar(5);
  constexpr auto ten_francs = money::franc(10);
  constexpr Bank<0> empty_bank{{}};
  constexpr auto bank = empty_bank.addRate(Currency::kCHF, Currency::kUSD, 2);
  constexpr auto sum = Sum<Money, Money>{five_bucks, ten_francs} * 2;
  constexpr Money result = bank.reduce(sum, Currency::kUSD);
  static_assert(result == money::dollar(20), "(5 USD + 10 CHF) *2 must be 20 USD.");
}

}  // namespace money_test

最後に

正確に計測していたわけではないですが、着手してから25時間程度で2018/5/6時点のソースとテストを作れました。
これを学習効果が高いと言えるかどうかは、判断が難しいですが、終始楽しかったです。
テストを頼りに細かくテンプレートテクニックを試せるのも良かったと思います。

C++テンプレートに手を出せないでいる方は、多国通貨を通じて入門してみてはいかがでしょうか。

参考

参考にした書籍、サイト一覧です。非常に参考になりました。ありがとうございました。

テスト駆動開発
C++テンプレートテクニック 第2版
Wandbox
cpprefjp - C++日本語リファレンス
本の虫
C++メタ関数のまとめ

15
9
12

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
9