constおじさんはconstexprおじさんに進化した!
「constおじさん」という記事を書いて5年経過したらしい。
constおじさんの私はconstexprを知りconstexprを積極的に使うようになった。
constexprとは
constant expression(定数式)の略らしい。
どう発音するか毎回迷う。コンストエクスプル?と心の中で言っていたが、マイナーらしい。 →constexprの読み方 - Qiita
constexprはC++11で導入された。コンパイル時に値が決定する定数のこと。
「定数定義には#define
よりもconst
を使おう」というのはよく言われていたが、定数定義はconstexpr
で置き換えることができる。
const
は「実行時にしか値が決まらないがその後は変更されない変数」に使うのがよさそう。
constexprの良いところ
下のコードを見てほしい。
100円に税金10%をつける簡単なコード。
#include <iostream>
#define DEFINE_YEN (100)
#define DEFINE_TAX (0.1F)
#define DEFINE_YEN_TAX (DEFINE_YEN + DEFINE_YEN * DEFINE_TAX)
const int CONST_YEN = 100;
const float CONST_TAX = 0.1F;
const float CONST_YEN_TAX = CONST_YEN + CONST_YEN * CONST_TAX;
constexpr int CONSTEXPR_YEN = 100;
constexpr float CONSTEXPR_TAX = 0.1F;
constexpr float CONSTEXPR_YEN_TAX = CONSTEXPR_YEN + CONSTEXPR_YEN * CONSTEXPR_TAX;
int main()
{
std::cout << "DEFINE_YEN " << DEFINE_YEN << std::endl;
std::cout << "DEFINE_TAX " << DEFINE_TAX << std::endl;
std::cout << "DEFINE_YEN_TAX " << DEFINE_YEN_TAX << std::endl;
std::cout << "CONST_YEN " << CONST_YEN << std::endl;
std::cout << "CONST_TAX " << CONST_TAX << std::endl;
std::cout << "CONST_YEN_TAX " << CONST_YEN_TAX << std::endl;
std::cout << "CONSTEXPR_YEN " << CONSTEXPR_YEN << std::endl;
std::cout << "CONSTEXPR_TAX " << CONSTEXPR_TAX << std::endl;
std::cout << "CONSTEXPR_YEN_TAX " << CONSTEXPR_YEN_TAX << std::endl;
return 0;
}
括弧を付けなくてもよい
define
は展開されるだけなので括弧()
を付けておかないと危険な場合がある。
constexpr
やconst
は変数定義なので、気にする必要がない。
スコープを指定できる
define
を書くとそのソースコード内すべてで有効になってしまうため、スコープが指定できない。
constexpr
やconst
はnamespaceの中に入れたり、ローカル変数にすることができる。
型がつけられる
constexpr
やconst
はdefine
と違い型がつけられる(付けなければならない)。
したがって、型が間違っているとコンパイル時に検出できる。
例えば上記例のCONSTEXPR_YEN_TAX
をint
としてしまうと警告が出る。
(Qiita上で)シンタックスハイライトが効く
上のコードを見て一目瞭然。
※VisualStudioはdefine
の方を色付けしてくる。
Visual Studioで値が見える
定義にマウスオーバーすると、define
は展開された結果が見えるだけ。
static_assertが使える
define
とconstexpr
はコンパイル時に値が決まるので、static_assertが使え、コンパイル時に不正な値を検出できる。
static_assert(DEFINE_TAX < 0.5, "Tax is too high!");
static_assert(CONSTEXPR_TAX < 0.5, "Tax is too high!");
static_assert(CONST_TAX < 0.5, "Tax is too high!"); // NG
assert(CONST_TAX < 0.5); // OK
const
は実行時にしか値が決まらないので、<cassert>
をincludeしてassert
を使うしかない。エラーは実行時にしか出ない。
constexprな関数が作れる
下記のように、constexpr
を関数定義につけることで計算をコンパイル時に行うことができるので、配列の個数などにも使用できる。
const
ではコンパイル時に計算されないため、配列の個数には使えない。
#include <stdint.h>
#define DEFINE_CALC_SIZE(count) (sizeof(int) * (count))
const size_t const_calc_size(size_t count)
{
return sizeof(int) * count;
}
constexpr size_t constexpr_calc_size(size_t count)
{
return sizeof(int) * count;
}
int main()
{
uint8_t buf1[DEFINE_CALC_SIZE(10)];
uint8_t buf2[const_calc_size(10)]; // NG
uint8_t buf3[constexpr_calc_size(10)]; // OK
return 0;
}
define
でマクロを定義するよりも複雑なことができる。たとえば以下のようにループを書くこともできる。(C++14以降)
#include <stdint.h>
constexpr size_t constexpr_calc_size2(size_t n)
{
size_t size = 0;
for (size_t i = 0; i < n; i++) {
size += i;
}
return size;
}
int main()
{
constexpr size_t SIZE = constexpr_calc_size2(100);
uint8_t buf4[SIZE];
return 0;
}
ちゃんとコンパイル時に計算されるため、SIZEにマウスオーバーすると値が表示される。
constexpr関数にしても、constexprでない通常の呼び方は可能。この場合、計算は実行時になるため、constexprの変数に代入したり配列の個数に設定することはできない。
constexpr size_t constexpr_calc_size(size_t count)
{
return sizeof(int) * count;
}
int main()
{
size_t count = 12;
const size_t bufsize = constexpr_calc_size(count); // OK 実行時に計算される constへの代入はOK
constexpr size_t bufsize = constexpr_calc_size(count); // NG
return 0;
}
#ifの代わりにif constexprが使える(C++17以降)
定数を判定するコンパイルスイッチを#if
で行うことがあるが、定数の判定ならif constexpr
が使える。
#include <iostream>
constexpr int A = 0;
#define B (0)
int main()
{
if constexpr (A == 0) {
std::cout << "A is 0" << std::endl;
} else {
std::cout << "A is not 0" << std::endl;
//ここは文法解釈される
}
#if B == 0
std::cout << "B is 0" << std::endl;
#elif
std::cout << "B is not 0" << std::endl;
ここは文法解釈されないのでコンパイルエラーにならない
#endif
}
コメントに書いたように、#if
で条件から外れた部分は文法解釈もされないので、その部分が文法的に間違っていてもコンパイルエラーにならないが、if constexpr
で条件を外れた部分は文法解釈されるので間違っているとコンパイルエラーになる。
コンパイルスイッチを切り替えたらコンパイルが通らなかったといった事故を防げる。
ちなみに、if constexpr
で条件から外れた部分は文法解釈はされるが実体のコードにはならない模様。安心して使えそう。
int main()
{
00007FF7A41F2410 push rbp
00007FF7A41F2412 push rdi
00007FF7A41F2413 sub rsp,0E8h
00007FF7A41F241A lea rbp,[rsp+20h]
00007FF7A41F241F lea rcx,[__34B43460_Constexpr3@cpp (07FF7A4203066h)]
00007FF7A41F2426 call __CheckForDebuggerJustMyCode (07FF7A41F13E3h)
if constexpr (A == 0) {
std::cout << "A is 0" << std::endl;
00007FF7A41F242B lea rdx,[string "A is 0" (07FF7A41FAC10h)]
00007FF7A41F2432 mov rcx,qword ptr [__imp_std::cout (07FF7A4201198h)]
00007FF7A41F2439 call std::operator<<<std::char_traits<char> > (07FF7A41F108Ch)
00007FF7A41F243E lea rdx,[std::endl<char,std::char_traits<char> > (07FF7A41F103Ch)]
00007FF7A41F2445 mov rcx,rax
00007FF7A41F2448 call qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF7A42011B0h)]
} else {
std::cout << "A is not 0" << std::endl; ★★この部分のアセンブリコードは生成されていない!
//ここは文法解釈される
}
#if B == 0
std::cout << "B is 0" << std::endl;
00007FF7A41F244E lea rdx,[string "B is 0" (07FF7A41FAC18h)]
00007FF7A41F2455 mov rcx,qword ptr [__imp_std::cout (07FF7A4201198h)]
00007FF7A41F245C call std::operator<<<std::char_traits<char> > (07FF7A41F108Ch)
00007FF7A41F2461 lea rdx,[std::endl<char,std::char_traits<char> > (07FF7A41F103Ch)]
00007FF7A41F2468 mov rcx,rax
00007FF7A41F246B call qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF7A42011B0h)]
#elif
std::cout << "B is not 0" << std::endl;
ここは文法解釈されないのでコンパイルエラーにならない
#endif
}
00007FF7A41F2471 xor eax,eax
00007FF7A41F2473 lea rsp,[rbp+0C8h]
00007FF7A41F247A pop rdi
00007FF7A41F247B pop rbp
00007FF7A41F247C ret
最後に
constexprはコンパイル時に計算されるので、実行時には全くコストがかかりません。
あなたもconstexprが使いたくなりませんか?