概要
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;
}
上の例では add()
関数が簡単なので constexpr
によるありがたみが薄く、
テンプレート変数を使っても同じことができそうに見えるが、
constexpr
を使ったほう簡潔で、通常の関数と同じ記法で書くことができる分うれしい。
関数が複雑になった場合(例えば fact()
pow()
log2()
など) にはありがたみが増す。
使用例
いい使用例を思いついたので書く。
仕様
浮動小数点数のビット表現を表現するクラスを作りたい。
そのクラスは float
と double
のビット表現について学ぶためのクラスで、
以下の機能を持たせたい。
float
と double
に対応するクラスがある。
float
と double
の値を受け取るコンストラクタがある。
- 指数部、仮数部の長さを返す関数がある。
- 格納されている値をビット列として返す関数がある。
- 格納されている値の指数部をビット列として返す関数がある。
- 格納されている値の仮数部をビット列として返す関数がある。
- 上の3つの関数の返り値として
std::bitset<N>
を使う。
この機能を持たせるには float
と double
に対応するクラスで
違う長さの 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();
}
テンプレートを使ってクラスを定義し、それを 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;
}
範囲forについて学んだので実装した。
普通の for
STLコンテナはイテレーターを持つので、要素を順番に取る処理がfor文で書ける。
#include <iostream>
#include <set>
#include <string>
#include <vector>
int main() {
std::vector<int> vec{1, 2, 3};
// begin()とend()でイテレーターを取得し、for文の条件として使う
// itrを定義するときの型はautoと書くのが普通
for (std::vector<int>::iterator itr = vec.begin(); itr != vec.end(); ++itr) {
// イテレーターはコンテナの成分のポインタっぽく使える
std::cout << *itr << std::endl;
}
std::cout << std::endl;
// vectorだけでなく他のコンテナでもほとんど同じように書ける
std::set<std::string> set{"a", "b", "c"};
for (std::set<std::string>::iterator itr = set.begin(); itr != set.end(); ++itr) {
std::cout << *itr << std::endl;
}
}
範囲 for
C++11では同じ意味のfor文を次のように書ける。
#include <iostream>
#include <set>
#include <string>
#include <vector>
int main() {
std::vector<int> vec{1, 2, 3};
// 範囲 for
for (int i : vec) {
std::cout << i << std::endl;
}
std::cout << std::endl;
std::set<std::string> set{"a", "b", "c"};
// strを参照にしてstd::stringのコピーが起こらないようにする
// さらにconstをつけてループ内でsetの内容が変わらないことを保証する
for (const std::string &str : set) {
std::cout << str << std::endl;
}
}
pythonのrange
pythonには range()
関数があり、以下のように使える。
for i in range(3, 10, 2):
print(i)
これをC++で実装する。
範囲 for の展開
リファレンスによると、
for ( range_declaration : range_expression )
loop_statement
は以下のように展開される。
{
auto && __range = range_expression;
for (auto __begin = begin(__range), __end = end(__range); __begin != __end; ++__begin) {
range_declaration = *__begin;
loop_statement
}
}
範囲 for に対応したクラス
展開式を見ると、クラスを範囲 for に対応させるには適当な型のついた
- begin()
- end()
- operator!=
- operator++
- operator*
があればいいことが分かる。
C++で実装
とりあえず最低限のものを実装する。
#include <iostream>
#include <vector>
#include <cassert>
#include <cmath>
template <typename T>
class Range {
private:
T m_value; // 現在値
const T m_end; // 終了値
const T m_stride; // ループ一回ごとに進む値
public:
// コンストラクタ
Range(T begin, T end, T stride)
: m_value(begin), m_end(end), m_stride(stride) {}
// ゲッタ
const T &value() const { return m_value; }
// begin()の戻り値がforループの中で編集される
Range<T> begin() const { return *this; }
// end()の返り値との比較によってループから抜けるかどうかが判定される
// begin()とend()は同じ型を持つ必要がある
const Range<T> end() const { return Range(m_end, m_end, m_end); }
// operator!=(rhs) はforループから抜けてよいかどうかを返す
// Rangeオブジェクトが異なるかどうかではない
bool operator!=(const Range<T> &rhs) const {
return m_stride > 0 ? m_value < rhs.value() : m_value > rhs.value();
}
// m_valueを進める
void operator++() { m_value += m_stride; }
// 現在の値を返す
const T &operator*() const { return m_value; }
};
// 実際に使う
int main() {
int sum = 0;
for (int i : Range<int>{0, 20, 3}) {
sum += i; // i = 0,3,6,9,12,15,18
}
assert(sum == 63);
// Rangeの使い回し
float sumd = 0;
const Range<double> r{0.1, 5.3, 1.1};
for (double d : r) {
for (double d : r) {
sumd += d;
}
sumd -= d * 5;
}
assert(fabs(sumd) < 1.0e-5);
std::cout << "OK";
return 0;
}
とりあえずはこれで使える。
改善
この実装にはいくつか問題がある。
- 同じRangeオブジェクトのend()は常に同じ値しか返さないので、const参照を返したほうがいい。
- Rangeオブジェクトを作るたびに型を明示的に書かなければいけないのは面倒くさい。
これを直す。
#include <iostream>
#include <vector>
#include <cassert>
#include <cmath>
template <typename T>
class Range {
private:
// Rangeとは別にクラスを作って、begin(), end()はこのクラスのオブジェクトを返すことにする
class Iterator {
// 変数/メソッドは同じ
private:
T m_value;
const T m_end;
const T m_stride;
public:
Iterator(T begin, T end, T stride)
: m_value(begin), m_end(end), m_stride(stride) {}
const T &value() { return m_value; }
void operator++() { m_value += m_stride; }
T operator*() const { return m_value; }
bool operator!=(const Iterator &end) const {
return m_stride > 0 ? m_value < end.m_end : m_value > end.m_end;
}
};
private:
// Rangeが begin(), end() で返すのは
// 常に同じIteratorオブジェクトなのでどちらもconstで問題ない
const Iterator m_begin, m_end;
public:
Range(T begin, T end, T stride)
: m_begin{begin, end, stride}, m_end{end, end, end} {}
// begin() の戻り値は常に同じだが、for文の中で変更されるので値で帰す
Iterator begin() const { return m_begin; }
// end() の返り値は常に同じでfor文中で変更されることもないのでconst参照で返す
const Iterator &end() const { return m_end; }
};
// 関数テンプレートを使って型の明示を不要にする
template<typename T>
Range<T> range(T begin, T end, T stride) {
return {begin, end, stride};
}
// 実際に使う (一つ前のプログラムと同じ)
int main() {
int sum = 0;
for (int i : range(0, 20, 3)) {
sum += i; // i = 0,3,6,9,12,15,18
}
assert(sum == 63);
// Rangeの使い回し
float sumd = 0;
auto r = range(0.1, 5.3, 1.1);
for (double d : r) {
for (double d : r) {
sumd += d;
}
sumd -= d * 5;
}
assert(fabs(sumd) < 1.0e-5);
std::cout << "OK";
return 0;
}
だいぶpythonっぽくできる。 range()
の引数を省略できるようにするのは簡単なので省略。
C++17
C++17 では範囲forの展開方法が変わり、begin()とend()の返り値の型が一致しなくても良くなっている。具体的には、C++11では
{
auto && __range = range_expression ;
for (auto __begin = begin_expr, __end = end_expr;
__begin != __end; ++__begin) {
range_declaration = *__begin;
loop_statement
}
}
だったものが
{
auto && __range = range_expression ;
auto __begin = begin_expr ;
auto __end = end_expr ;
for ( ; __begin != __end; ++__begin) {
range_declaration = *__begin;
loop_statement
}
}
に変わっている(リファレンス)。
そのため、python風のrangeも簡潔に書ける。
#include <iostream>
#include <vector>
#include <cassert>
#include <cmath>
// python風range (C++17)
template <typename T>
class Range {
private:
T m_value;
const T m_end;
const T m_stride;
public:
Range(T begin, T end, T stride)
: m_value(begin), m_end(end), m_stride(stride) {}
const T &value() const { return m_value; }
Range<T> begin() const { return *this; }
// end()の返り値の型が変わる
T end() const { return m_end; }
// operator!=の引数の型が変わる
bool operator!=(const T &value) const {
return m_stride > 0 ? m_value < value : m_value > value;
}
void operator++() { m_value += m_stride; }
const T &operator*() const { return m_value; }
};
template<typename T>
Range<T> range(T begin, T end, T stride) {
return {begin, end, stride};
}
// 実際に使う
int main() {
int sum = 0;
for (int i : range(0, 20, 3)) {
sum += i;
}
assert(sum == 63);
// Rangeの使い回し
float sumd = 0;
auto r = range(0.1, 5.3, 1.1);
for (double d : r) {
for (double d : r) {
sumd += d;
}
sumd -= d * 5;
}
assert(fabs(sumd) < 1.0e-5);
std::cout << "OK";
return 0;
}