C++のconstexpr

概要

C++では関数や式に constexpr をつけることで、その値をコンパイル時に計算するようにコンパイラに指示できる。 コンパイル時に計算された constexpr 式の値はリテラルやテンプレート変数と同じように使用できる。例えば、

  • 他のテンプレートに渡す
  • 配列の大きさの宣言に使う
  • 他のconstexprを作る

が行える。

以下は確認例。

#include <array>
#include <iostream>

// この関数は(可能ならば)コンパイル時に計算が行われる
constexpr int add(int x, int y) {
  return x + y;
}

int main() {
  // constexprはテンプレートの引数に使える
  // add()の宣言からconstexprを削除するとコンパイルエラーになる
  std::array<int, add(1, 1)> arr1;
  // 配列も確保できる
  int arr2[add(1, 1)];

  // constexprを使って別のconstexprを定義できる
  constexpr auto x = 0;
  constexpr auto y = add(x, 1);
  std::array<int, y> arr4;

  std::cout << "OK" << std::endl;
  return 0;
}
OK

上の例では add() 関数が簡単なので constexpr によるありがたみが薄く、 テンプレート変数を使っても同じことができそうに見えるが、 constexpr を使ったほう簡潔で、通常の関数と同じ記法で書くことができる分うれしい。 関数が複雑になった場合(例えば fact() pow() log2() など) にはありがたみが増す。

使用例

いい使用例を思いついたので書く。

仕様

浮動小数点数のビット表現を表現するクラスを作りたい。 そのクラスは floatdouble のビット表現について学ぶためのクラスで、 以下の機能を持たせたい。

  • floatdouble に対応するクラスがある。
  • floatdouble の値を受け取るコンストラクタがある。
  • 指数部、仮数部の長さを返す関数がある。
  • 格納されている値をビット列として返す関数がある。
  • 格納されている値の指数部をビット列として返す関数がある。
  • 格納されている値の仮数部をビット列として返す関数がある。
  • 上の3つの関数の返り値として std::bitset<N> を使う。

この機能を持たせるには floatdouble に対応するクラスで 違う長さの std::bitset<N> をメンバ変数として持つ必要がある。 クラスを2つ作ることも可能だが、長さ以外の部分がほぼ同じになるので無駄になる。

実装

テンプレートと constexpr を使うといい感じにこの要求を満たすクラスを作れる。

Code Snippet 1: floating_point.hpp

#include <bitset>

// 浮動小数点数を格納するためのクラス
template <typename T,            // 格納される値の型 (floatかdouble)
          unsigned wExponent,    // 指数部の長さ
          unsigned wSignificand> // 仮数部の長さ
class FloatingPoint {
 public:
  // 浮動小数点数の各部分の長さを返す
  // constexprなのがミソ
  // コンパイル時に他の変数の宣言に使うのでクラスの先頭に書く
  constexpr static unsigned w_exp() { return wExponent; };
  constexpr static unsigned w_sig() { return wSignificand; };
  constexpr static unsigned w_all() { return 1 + wExponent + wSignificand; };

 private:
  T m_val;
  // 浮動小数点数の各部分のビット列を保持する
  // 各部分の長さを取得する関数がconstexprなのでここでテンプレート引数として使える
  std::bitset<w_all()> m_bits_all;
  std::bitset<w_exp()> m_bits_exp;
  std::bitset<w_sig()> m_bits_sig;

  // 指数部、仮数部の第iビットが全体の第何ビットかを返す
  static unsigned index_exp(unsigned i) { return w_sig() + i; }
  static unsigned index_sig(unsigned i) { return i; }

  // m_bits_allが初期化された状態から、m_bits_expとm_bits_sigを初期化する
  void init_bits() {
    for (unsigned i = 0; i < w_exp(); ++i) {
      m_bits_exp[i] = m_bits_all[index_exp(i)];
    }

    for (unsigned i = 0; i < w_sig(); ++i) {
      m_bits_sig[i] = m_bits_all[index_sig(i)];
    }
  }

 public:
  explicit FloatingPoint(T val);
  // ビット列を取り出す
  std::bitset<w_all()> bits_all() const { return m_bits_all; }
  std::bitset<w_exp()> bits_exp() const { return m_bits_exp; }
  std::bitset<w_sig()> bits_sig() const { return m_bits_sig; }
  bool bit_sign() const { return {m_bits_all[w_all() - 1]}; }
};

// IEEE-754に従うように型を定義
using Single = FloatingPoint<float, 8, 23>;
using Double = FloatingPoint<double, 11, 52>;

// コンストラクタだけは特殊に定義する
template <>
Single::FloatingPoint(float val)
    : m_val(val), m_bits_all(*reinterpret_cast<int32_t *>(&val)) {
  init_bits();
}

template <>
Double::FloatingPoint(double val)
    : m_val(val), m_bits_all(*reinterpret_cast<int64_t *>(&val)) {
  init_bits();
}

テンプレートを使ってクラスを定義し、それを floatdouble 用に特殊化することでコードの重複を防いだ。 やろうと思えば、 floatdouble でない 浮動小数点数の表現にも対応することができる。

以下のように使う。

#include "floating_point.hpp"
#include <cassert>
#include <iostream>

int main() {
  // 実数に対応するビット列は適当に求める
  Single f(1234.567);
  assert(f.bit_sign() == false);
  // to_string()をしてから比較をしてもいいが、Single::w_all()などを使うためにあえてそのまま比較する
  assert(f.bits_all() == std::bitset<Single::w_all()>("01000100100110100101001000100101"));
  assert(f.bits_exp() == std::bitset<Single::w_exp()>("10001001"));
  assert(f.bits_sig() == std::bitset<Single::w_sig()>("00110100101001000100101"));

  Double d(-12345678.90123456);
  assert(d.bit_sign() == true);
  assert(d.bits_all() ==
         std::bitset<Double::w_all()>("1100000101100111100011000010100111011100110101101110100111011100"));
  assert(d.bits_exp() ==
         std::bitset<Double::w_exp()>("10000010110"));
  assert(d.bits_sig() ==
         std::bitset<Double::w_sig()>("0111100011000010100111011100110101101110100111011100"));

  std::cout << "OK" << std::endl;
  return 0;
}
OK