LoginSignup
7
3

More than 1 year has passed since last update.

constexprおじさん

Posted at

constおじさんはconstexprおじさんに進化した!

「constおじさん」という記事を書いて5年経過したらしい。

constおじさんの私はconstexprを知りconstexprを積極的に使うようになった。

constexprとは

constant expression(定数式)の略らしい。
どう発音するか毎回迷う。コンストエクスプル?と心の中で言っていたが、マイナーらしい。 →constexprの読み方 - Qiita

constexprはC++11で導入された。コンパイル時に値が決定する定数のこと。
「定数定義には#defineよりもconstを使おう」というのはよく言われていたが、定数定義はconstexprで置き換えることができる。
constは「実行時にしか値が決まらないがその後は変更されない変数」に使うのがよさそう。

constexprの良いところ

下のコードを見てほしい。
100円に税金10%をつける簡単なコード。

main.cpp
#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は展開されるだけなので括弧()を付けておかないと危険な場合がある。
constexprconstは変数定義なので、気にする必要がない。

スコープを指定できる

defineを書くとそのソースコード内すべてで有効になってしまうため、スコープが指定できない。
constexprconstはnamespaceの中に入れたり、ローカル変数にすることができる。

型がつけられる

constexprconstdefineと違い型がつけられる(付けなければならない)。
したがって、型が間違っているとコンパイル時に検出できる。
例えば上記例のCONSTEXPR_YEN_TAXintとしてしまうと警告が出る。

(Qiita上で)シンタックスハイライトが効く

上のコードを見て一目瞭然。
※VisualStudioはdefineの方を色付けしてくる。
image.png

Visual Studioで値が見える

定義にマウスオーバーすると、defineは展開された結果が見えるだけ。
image.png

constは型が表示されるだけ。
image.png

constexprは計算結果が見える。
image.png

static_assertが使える

defineconstexprはコンパイル時に値が決まるので、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ではコンパイル時に計算されないため、配列の個数には使えない。

main.cpp
#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以降)

main.cpp
#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にマウスオーバーすると値が表示される。
image.png

constexpr関数にしても、constexprでない通常の呼び方は可能。この場合、計算は実行時になるため、constexprの変数に代入したり配列の個数に設定することはできない。

main.cpp
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が使える。

main.cpp
#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が使いたくなりませんか?

7
3
0

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
7
3