概要
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()
など) にはありがたみが増す。
使用例
いい使用例を思いついたので書く。
仕様
浮動小数点数のビット表現を表現するクラスを作りたい。
そのクラスは float
と double
のビット表現について学ぶためのクラスで、
以下の機能を持たせたい。
float
とdouble
に対応するクラスがある。float
とdouble
の値を受け取るコンストラクタがある。- 指数部、仮数部の長さを返す関数がある。
- 格納されている値をビット列として返す関数がある。
- 格納されている値の指数部をビット列として返す関数がある。
- 格納されている値の仮数部をビット列として返す関数がある。
- 上の3つの関数の返り値として
std::bitset<N>
を使う。
この機能を持たせるには float
と double
に対応するクラスで
違う長さの std::bitset<N>
をメンバ変数として持つ必要がある。
クラスを2つ作ることも可能だが、長さ以外の部分がほぼ同じになるので無駄になる。
実装
テンプレートと constexpr
を使うといい感じにこの要求を満たすクラスを作れる。
#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();
}
テンプレートを使ってクラスを定義し、それを float
と double
用に特殊化することでコードの重複を防いだ。
やろうと思えば、 float
や double
でない 浮動小数点数の表現にも対応することができる。
以下のように使う。
#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