4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

constexprとコンストラクタの話

Last updated at Posted at 2025-01-09

昔からC++をやっててC++11以降に対応している人にはconstexprやそれを付けたコンストラクタは見慣れたものかもしれません。しかし、そうでない人には小ネタになるかもしれないと書いてみました。

1. constexprの前にconstの話

1-1. constを付けない場合

例えば以下のような何の変哲もないコードを考えます。

no_const_primitive.cpp
int var = 1;
int main() {
    return var;
}

知りたいのはこのコードからどんなアセンブラが吐かれるか?です(すみませんが、環境はUbuntu gcc-11.4 x86_64のみです)。コンソールから以下のコマンドを打ってアセンブラコードを吐かせます。

$ g++ -Wall -pedantic -std=c++11 -S -masm=intel -fverbose-asm no_const_primitive.cpp

結果は以下のような形になります

no_const_primitive.s
...
	.data
	.align 4
	.type	var, @object
	.size	var, 4
var:
	.long	1
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	endbr64	
	push	rbp	#
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	mov	rbp, rsp	#,
	.cfi_def_cfa_register 6
# no_const_primitive.cpp:3:     return var;
	mov	eax, DWORD PTR var[rip]	# _2, var
# no_const_primitive.cpp:4: }
	pop	rbp	#
	.cfi_def_cfa 7, 8
	ret	
	.cfi_endproc
...

注目すべきは.dataセクションにvar変数が置かれたことです。静的変数は初期値付きのdataセクションに置かれるか、0初期化されているbssセクションに置かれます。この変数の内容をmain()関数ではreturn varで返すのですが、それをしているのがmov eax, DWORD PTR var[rip]になります。

1-2. constを付けた場合

const_primitive.cpp
const int var = 1;
int main() {
    return var;
}

はい、varの型がintからconst intになりました。これでvarは以降代入操作ができなくなります。このコードを同様にビルドすると…

$ g++ -Wall -pedantic -std=c++11 -S -masm=intel -fverbose-asm const_primitive.cpp

結果は以下のような形になります

const_primitive.s
...
	.text
	.section	.rodata
	.align 4
	.type	_ZL3var, @object
	.size	_ZL3var, 4
_ZL3var:
	.long	1
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	endbr64	
	push	rbp	#
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	mov	rbp, rsp	#,
	.cfi_def_cfa_register 6
# const_primitive.cpp:3:     return var;
	mov	eax, 1	# _1,
# const_primitive.cpp:4: }
	pop	rbp	#
	.cfi_def_cfa 7, 8
	ret	
	.cfi_endproc
...

違いはまずvar.rodataセクションに配置されたことです。.rodataセクションは読み込み専用のデータ領域であることを表します。例えば

const int var = 1;
int main() {
    *const_cast<int*>(&var) = 2;
    return var;
}

強制的にconstを外してアクセスするコードを入れると、実行時にセグメンテーションフォルトで落ちます。

またmain()関数はvarの値ではなく、直値で1を返すようになりました。これはもはやvarが変数ではなく、定数としての扱いであることを意味しています。今はコンパイル時に最適化をかけていないので、varはメモリ領域を確保できていますが、最適化後はきっと消えてしまうでしょう。

2. constexprを付けるとどうなるのか?

constexpr_primitive.cpp
constexpr int var = 1;
int main() {
    return var;
}
$ g++ -Wall -pedantic -std=c++11 -S -masm=intel -fverbose-asm constexpr_primitive.cpp
constexpr_primitive.s
...
	.text
	.section	.rodata
	.align 4
	.type	_ZL3var, @object
	.size	_ZL3var, 4
_ZL3var:
	.long	1
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	endbr64	
	push	rbp	#
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	mov	rbp, rsp	#,
	.cfi_def_cfa_register 6
# constexpr_primitive.cpp:3:     return var;
	mov	eax, 1	# _1,
# constexpr_primitive.cpp:4: }
	pop	rbp	#
	.cfi_def_cfa 7, 8
	ret	
	.cfi_endproc
...

つまりconstと同じです。primitiveな変数では違いがありません。

3. primitiveからobjectへ

3-1. constなし編

no_const_object.cpp
struct _s {
    int var;
    _s(int var):var(var){}
} s{1};
int main() {
    return s.var;
}
$ g++ -Wall -pedantic -std=c++11 -S -masm=intel -fverbose-asm no_const_object.cpp
no_const_object.s
...
; 以下_s::_s()相当
...
_ZN2_sC2Ei:
.LFB1:
	.cfi_startproc
	endbr64	
	push	rbp	#
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	mov	rbp, rsp	#,
	.cfi_def_cfa_register 6
	mov	QWORD PTR -8[rbp], rdi	# this, this
	mov	DWORD PTR -12[rbp], esi	# var, var
# no_const_object.cpp:3:     _s(int var):var(var){}
	mov	rax, QWORD PTR -8[rbp]	# tmp82, this
	mov	edx, DWORD PTR -12[rbp]	# tmp83, var
	mov	DWORD PTR [rax], edx	# this_2(D)->var, tmp83
# no_const_object.cpp:3:     _s(int var):var(var){}
	nop	
	pop	rbp	#
	.cfi_def_cfa 7, 8
	ret	
	.cfi_endproc
...
; sbssセクションに配置される
...
	.bss
	.align 4
	.type	s, @object
	.size	s, 4
s:
	.zero	4
...
; メイン関数
...
	.text
	.globl	main
	.type	main, @function
main:
.LFB3:
	.cfi_startproc
	endbr64	
	push	rbp	#
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	mov	rbp, rsp	#,
	.cfi_def_cfa_register 6
# no_const_object.cpp:6:     return s.var;
	mov	eax, DWORD PTR s[rip]	# _2, s.var
# no_const_object.cpp:7: }
	pop	rbp	#
	.cfi_def_cfa 7, 8
	ret	
	.cfi_endproc
...
; 静的初期化/破棄準備
...
	.size	main, .-main
	.type	_Z41__static_initialization_and_destruction_0ii, @function
_Z41__static_initialization_and_destruction_0ii:
.LFB4:
	.cfi_startproc
...
; この中でsのコンストラクタが呼ばれる
...
# no_const_object.cpp:4: } s{1};
	mov	esi, 1	#,
	lea	rax, s[rip]	# tmp82,
	mov	rdi, rax	#, tmp82
	call	_ZN2_sC1Ei	#
...
    ret	
	.cfi_endproc
...

一般的な静的オブジェクトの初期化とメイン関数の例でした。bssセクションにあるやつをmain()呼び出し前に静的初期化でコンストラクタ呼び出しを連打するいつも通りのやつです。

3-2. const編

const_object.cpp
struct _s {
    int var;
    _s(int var):var(var){}
};
const _s s{1};
int main() {
    return s.var;
}
$ g++ -Wall -pedantic -std=c++11 -S -masm=intel -fverbose-asm const_object.cpp
const_object.s
...
; 以下_s::_s()相当
...
_ZN2_sC2Ei:
.LFB1:
	.cfi_startproc
	endbr64	
	push	rbp	#
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	mov	rbp, rsp	#,
	.cfi_def_cfa_register 6
	mov	QWORD PTR -8[rbp], rdi	# this, this
	mov	DWORD PTR -12[rbp], esi	# var, var
# const_object.cpp:3:     _s(int var):var(var){}
	mov	rax, QWORD PTR -8[rbp]	# tmp82, this
	mov	edx, DWORD PTR -12[rbp]	# tmp83, var
	mov	DWORD PTR [rax], edx	# this_2(D)->var, tmp83
# const_object.cpp:3:     _s(int var):var(var){}
	nop	
	pop	rbp	#
	.cfi_def_cfa 7, 8
	ret	
	.cfi_endproc
...
; s(_ZL1s)は.commなので(リンカが)bssセクションに配置する
...
	.size	_ZN2_sC2Ei, .-_ZN2_sC2Ei
	.weak	_ZN2_sC1Ei
	.set	_ZN2_sC1Ei,_ZN2_sC2Ei
	.local	_ZL1s
	.comm	_ZL1s,4,4
...
; メイン関数
...
	.text
	.globl	main
	.type	main, @function
main:
.LFB3:
	.cfi_startproc
	endbr64	
	push	rbp	#
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	mov	rbp, rsp	#,
	.cfi_def_cfa_register 6
# const_object.cpp:7:     return s.var;
	mov	eax, DWORD PTR _ZL1s[rip]	# _2, s.var
# const_object.cpp:8: }
	pop	rbp	#
	.cfi_def_cfa 7, 8
	ret	
	.cfi_endproc
...
; 静的初期化/破棄準備
...
_Z41__static_initialization_and_destruction_0ii:
.LFB4:
	.cfi_startproc
...
; この中でsのコンストラクタが呼ばれる
...
# const_object.cpp:5: const _s s{1};
	mov	esi, 1	#,
	lea	rax, _ZL1s[rip]	# tmp82,
	mov	rdi, rax	#, tmp82
	call	_ZN2_sC1Ei	#
...
    ret	
	.cfi_endproc
...

細かい部分に若干の違いはありますが、動作内容はconstがないケースと変わりません。つまり、このケースでのconstは純粋にコンパイル時点での型チェックでのみ機能しているようです。まあ初期化のためにコンストラクタ動かさないといけないので結局動作に違いはないんですよね。

3-3. constexpr編

constexpr_object.cpp
struct _s {
    int var;
    _s(int var):var(var){}
};
constexpr _s s{1};
int main() {
    return s.var;
}
$ g++ -Wall -pedantic -std=c++11 -S -masm=intel -fverbose-asm constexpr_object.cpp
constexpr_object.cpp:5:14: error: the type ‘const _s’ of ‘constexpr’ variable ‘s’ is not literal
    5 | constexpr _s s{1};
      |              ^
constexpr_object.cpp:1:8: note: ‘_s’ is not literal because:
    1 | struct _s {
      |        ^~
constexpr_object.cpp:1:8: note:   ‘_s’ is not an aggregate, does not have a trivial default constructor, and has no ‘constexpr’ constructor that is not a copy or move constructor
$

何やら怒られています。コンストラクタを持つstructで、constexprでないコンストラクタで初期化されてるので、定数=リテラル(constexpr)にできないよと言われています。単に初期化後に変更できないだけのconstとの違いですね。

4. constexprなコンストラクタを付けると…

4-1. constなし編

no_const_object_costexpr_ctr.cpp
struct _s {
    int var;
    constexpr _s(int var):var(var){}
} s{1};
int main() {
    return s.var;
}
$ g++ -Wall -pedantic -std=c++11 -S -masm=intel -fverbose-asm no_const_object_costexpr_ctr.cpp
no_const_object_costexpr_ctr.s
...
	.data
	.align 4
	.type	s, @object
	.size	s, 4
s:
# var:
	.long	1
...
	.text
	.globl	main
	.type	main, @function
main:
.LFB3:
	.cfi_startproc
	endbr64	
	push	rbp	#
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	mov	rbp, rsp	#,
	.cfi_def_cfa_register 6
# no_const_object_costexpr_ctr.cpp:6:     return s.var;
	mov	eax, DWORD PTR s[rip]	# _2, s.var
# no_const_object_costexpr_ctr.cpp:7: }
	pop	rbp	#
	.cfi_def_cfa 7, 8
	ret	
	.cfi_endproc
...

コンストラクタの実装コードが生成されなくなりました。コンストラクタを実行した結果のメモリ構造が.dataセクションにsとして配置されているのが分かります。それに伴い静的初期化時のコンストラクタ呼び出しが消えています。

またmain()関数ではsの値をちゃんと返しています。つまりオブジェクトとしては書き換えが可能なのです。

4-2. const編

const_object_costexpr_ctr.cpp
struct _s {
    int var;
    constexpr _s(int var):var(var){}
};
const _s s{1};
int main() {
    return s.var;
}
$ g++ -Wall -pedantic -std=c++11 -S -masm=intel -fverbose-asm const_object_costexpr_ctr.cpp
const_object_costexpr_ctr.s
...
	.text
	.section	.rodata
	.align 4
	.type	_ZL1s, @object
	.size	_ZL1s, 4
_ZL1s:
# var:
	.long	1
...
	.text
	.globl	main
	.type	main, @function
main:
.LFB3:
	.cfi_startproc
	endbr64	
	push	rbp	#
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	mov	rbp, rsp	#,
	.cfi_def_cfa_register 6
# const_object_costexpr_ctr.cpp:7:     return s.var;
	mov	eax, 1	# _1,
# const_object_costexpr_ctr.cpp:8: }
	pop	rbp	#
	.cfi_def_cfa 7, 8
	ret	
	.cfi_endproc
...

primitiveなときと同様に.rodataセクションに配置されるようになり、main()関数ではs.varではなく直値の1が返されています。

4-3. constexpr編

constexp_object_costexpr_ctr.cpp
struct _s {
    int var;
    constexpr _s(int var):var(var){}
};
constexpr _s s{1};
int main() {
    return s.var;
}
$ g++ -Wall -pedantic -std=c++11 -S -masm=intel -fverbose-asm constexpr_object_costexpr_ctr.cpp
constexpr_object_costexpr_ctr.s
...
	.text
	.section	.rodata
	.align 4
	.type	_ZL1s, @object
	.size	_ZL1s, 4
_ZL1s:
# var:
	.long	1
...
	.text
	.globl	main
	.type	main, @function
main:
.LFB3:
	.cfi_startproc
	endbr64	
	push	rbp	#
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	mov	rbp, rsp	#,
	.cfi_def_cfa_register 6
# constexpr_object_costexpr_ctr.cpp:7:     return s.var;
	mov	eax, 1	# _1,
# constexpr_object_costexpr_ctr.cpp:8: }
	pop	rbp	#
	.cfi_def_cfa 7, 8
	ret	
	.cfi_endproc
...

はい、constと同じですね。constexprはconst指定を含んでいるので、内容が同じになら同じコードが生成されるのは道理です。

5. まとめ/うんちく

5-1. 今回見たconstexpr

  • constexprは変数に指定すると型にconstが付く
  • constexprコンストラクタはコンストラクタの実行をコンパイル時にしてしまう
  • constexprは最適化とは関係がない

5-2. constはconstexprにした方がいい?

たまにそういう意見を見るのですが、意味が違うので、個人的にはそうしません。むしろ、変数の型にはconstを指定して、関数にはconstexprを指定する感じなので、基本constのままです。

原理主義者の方がどういう考えでそうしてるのかは、良く知りません。

5-3. constexprの泣き所

実行時エラーが出せないところです。constexprの詳細がコンパクトにまとまってる以下のサイト

の備考に若干記載があります。

5-4. なんで今さら記事にしたか

c++20でunionメンバを持つコンストラクタにconstexpr指定できるようになり、std::stringのコンストラクタもconstextr指定できるようになりました。これに伴い、静的に配置されるオブジェクト内のstd::stringが.dataセクションに移動したため、中身空でもそれなりの初期化値領域が必要になり、C++17→C++20で実行ファイルのサイズが増える、という現象を見たからです。

std::array<std::string, 50000> s;

みたいなものでした。C++17までならbssセクションに確保され、コンストラクタで50000個分初期化するのですが、C++20だと50000個分の初期化済データ(約1.5MB)がdataセクションに追加され(gcc 13以上/clang 17以上)、そのまま実行ファイルのサイズが急増した、というものです。unionに使ってるメンバ切り替え用の変数初期値が0だったら…とちょっと思いました。

4
2
1

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
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?