C++ Advent Calender 2021
この記事はC++ Advent Calendar 2021 13日目の記事です。もうすぐクリスマなんやが???
・・・遅刻してすみません。
4日目の記事も良ければどうぞ
algorithm系関数に状態を持つ関数オブジェクトを生で渡すのはやめてください、しんでしまいます
はじめに
C/C++においては、構造体/クラスのメンバーの並び順は意味を持ちます。aggregate初期化するときも定義順に初期化が必要ですし、今回の話とは関係ないですがクラスのコンストラクタでの初期化子の並びも定義順である必要があります。
さて、次のようなクラスを考えます(コード提供: @AinoMegumi)。ちょっとメンバーの数が多いですよね。
struct Param {
int HP;
int MP;
int Attack;
int Defence;
int MagicAttack;
int MagicDefence;
int MagicCure;
int Speed;
int Cleverness;
int MinExp;
int Level;
};
このクラスの配列を考えます。要素を追加するときどうすればいいでしょうか?まっさきに考えるのがstd::vector::emplace_back
にaggregte初期化してもらうことです。
int GetData(const std::string_view&);
std::vector<Param> Parameters;
Parameters.emplace_back(
GetData("hp"),
GetData("mp"),
GetData("attack"),
GetData("defence"),
GetData("magicattack"),
GetData("magicdefence"),
GetData("magiccure"),
GetData("speed"),
GetData("cleverness"),
GetData("exp"),
GetData("level")
)
問題点
定義順に初期化しなければならないにも関わらず、初期化段階で定義順を意識するのが難しいですよね、例えば下は間違えて初期化しているのですがぱっと見ただけではわからないですよね。というより人間の目で見てレビュー段階で弾くとかやりたくありません。
Parameters.emplace_back(
GetData("hp"),
GetData("mp"),
GetData("attack"),
GetData("defence"),
GetData("magiccure"),
GetData("magicdefence"),
GetData("magicattack"),
GetData("speed"),
GetData("cleverness"),
GetData("exp"),
GetData("level")
)
解決策?
aggregate初期化を投げ捨てる、つまりemplace_back
を投げ捨てることで解決できます。
Parameters.push_back({});
auto& e = Parameters.back();
e.HP = GetData("hp");
e.MP = GetData("mp");
e.Attack = GetData("attack");
e.Defence = GetData("defence");
e.MagicAttack = GetData("magicattack");
e.MagicDefence = GetData("magicdefence");
e.MagicCure = GetData("magiccure");
e.Speed = GetData("speed");
e.Cleverness = GetData("cleverness");
e.MinExp = GetData("exp");
e.Level = GetData("level");
えぇぇ。。。
一旦初期化してから再度値を代入するのは、意味論的に違いますよね・・・?ちゃんと初期化は初期化でやりたいです。
指示付き初期化を試す
C99/C++20からまさにこういう用途のための初期化方法が追加されています。
Parameters.emplace_back({
.HP = GetData("hp"),
.MP = GetData("mp"),
.Attack = GetData("attack"),
.Defence = GetData("defence"),
.MagicAttack = GetData("magicattack"),
.MagicDefence = GetData("magicdefence"),
.MagicCure = GetData("magiccure"),
.Speed = GetData("speed"),
.Cleverness = GetData("cleverness"),
.MinExp = GetData("exp"),
.Level = GetData("level"),
});
順番が入れ替わると
prog.cc: In function 'int main()':
prog.cc:28:25: warning: missing initializer for member 'Param::MagicAttack' [-Wmissing-field-initializers]
28 | Parameters.push_back({
| ~~~~~~~~~~~~~~~~~~~~^~
29 | .HP = GetData("hp"),
| ~~~~~~~~~~~~~~~~~~~~
30 | .MP = GetData("mp"),
| ~~~~~~~~~~~~~~~~~~~~
31 | .Attack = GetData("attack"),
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
32 | .Defence = GetData("defence"),
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
33 | .MagicDefence = GetData("magicdefence"),
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
34 | .MagicAttack = GetData("magicattack"),
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
35 | .MagicCure = GetData("magiccure"),
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
36 | .Speed = GetData("speed"),
| ~~~~~~~~~~~~~~~~~~~~~~~~~~
37 | .Cleverness = GetData("cleverness"),
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
38 | .MinExp = GetData("exp"),
| ~~~~~~~~~~~~~~~~~~~~~~~~~
39 | .Level = GetData("level"),
| ~~~~~~~~~~~~~~~~~~~~~~~~~~
40 | });
| ~~
prog.cc:28:25: warning: missing initializer for member 'Param::MagicCure' [-Wmissing-field-initializers]
prog.cc:28:25: warning: missing initializer for member 'Param::Speed' [-Wmissing-field-initializers]
prog.cc:28:25: warning: missing initializer for member 'Param::Cleverness' [-Wmissing-field-initializers]
prog.cc:28:25: warning: missing initializer for member 'Param::MinExp' [-Wmissing-field-initializers]
prog.cc:28:25: warning: missing initializer for member 'Param::Level' [-Wmissing-field-initializers]
prog.cc:28:25: error: designator order for field 'Param::MagicAttack' does not match declaration order in 'std::vector<Param>::value_type' {aka 'Param'}
コンパイルエラーになります。
emplace_backするほうが速いという幻想に囚われてに得たもの
ところでvectorへの要素追加はpush_backよりemplace_backのほうが早い印象があります。するとデバッグ時とリリース時でマクロで切り替えるみたいなことが必要になりそうですね?
#ifdef _DEBUG
# define VECTOR_DESIGNATED_EMPLACE(v, ...) v.push_back({ __VA_ARGS__ })
# define DESIGNATED_INIT(field, value) . ## field = value
#else
# define VECTOR_DESIGNATED_EMPLACE(v, ...) v.emplace_back( __VA_ARGS__ )
# define DESIGNATED_INIT(field, value) value
#endif
VECTOR_DESIGNATED_EMPLACE(
Parameters,
DESIGNATED_INIT(HP, GetData("hp")),
DESIGNATED_INIT(MP, GetData("mp")),
DESIGNATED_INIT(Attack, GetData("attack")),
DESIGNATED_INIT(Defence, GetData("defence")),
DESIGNATED_INIT(MagicAttack, GetData("magicattack")),
DESIGNATED_INIT(MagicDefence, GetData("magicdefence")),
DESIGNATED_INIT(MagicCure, GetData("magiccure")),
DESIGNATED_INIT(Speed, GetData("speed")),
DESIGNATED_INIT(Cleverness, GetData("cleverness")),
DESIGNATED_INIT(MinExp, GetData("exp")),
DESIGNATED_INIT(Level, GetData("level"))
);
}
#ifdef VECTOR_DESIGNATED_EMPLACE
#undef VECTOR_DESIGNATED_EMPLACE
#endif
#ifdef DESIGNATED_INIT
#undef DESIGNATED_INIT
#endif
ただこういうマクロに染まる前にきちんと計測しましょう。今回は吐かれるアセンブリを見ます。
msvcのアセンブリ
00007FF6548015D6 mov rdx,qword ptr [rbp+108h]
00007FF6548015DD cmp rdx,qword ptr [rbp+110h]
00007FF6548015E4 je main+340h (07FF654801620h)
00007FF6548015E6 mov dword ptr [rdx],eax
00007FF6548015E8 mov eax,dword ptr [rsp+78h]
00007FF6548015EC mov dword ptr [rdx+4],eax
00007FF6548015EF mov eax,dword ptr [rsp+74h]
00007FF6548015F3 mov dword ptr [rdx+8],eax
00007FF6548015F6 mov eax,dword ptr [rsp+70h]
00007FF6548015FA mov dword ptr [rdx+0Ch],eax
00007FF6548015FD mov dword ptr [rdx+10h],r13d
00007FF654801601 mov dword ptr [rdx+14h],r12d
00007FF654801605 mov dword ptr [rdx+18h],r15d
00007FF654801609 mov dword ptr [rdx+1Ch],r14d
00007FF65480160D mov dword ptr [rdx+20h],esi
00007FF654801610 mov dword ptr [rdx+24h],edi
00007FF654801613 mov dword ptr [rdx+28h],ebx
00007FF654801616 add qword ptr [rbp+108h],2Ch
00007FF65480161E jmp main+3AAh (07FF65480168Ah)
00007FF654801620 lea rax,[rsp+7Ch]
00007FF654801625 mov qword ptr [rsp+60h],rax
00007FF65480162A lea rax,[rbp-80h]
00007FF65480162E mov qword ptr [rsp+58h],rax
00007FF654801633 lea rax,[rbp-7Ch]
00007FF654801637 mov qword ptr [rsp+50h],rax
00007FF65480163C lea rax,[rbp-78h]
00007FF654801640 mov qword ptr [rsp+48h],rax
00007FF654801645 lea rax,[rbp-74h]
00007FF654801649 mov qword ptr [rsp+40h],rax
00007FF65480164E lea rax,[rbp-70h]
00007FF654801652 mov qword ptr [rsp+38h],rax
00007FF654801657 lea rax,[rbp-6Ch]
00007FF65480165B mov qword ptr [rsp+30h],rax
00007FF654801660 lea rax,[rsp+70h]
00007FF654801665 mov qword ptr [rsp+28h],rax
00007FF65480166A lea rax,[rsp+74h]
00007FF65480166F mov qword ptr [rsp+20h],rax
00007FF654801674 lea r9,[rsp+78h]
00007FF654801679 lea r8,[rbp-68h]
00007FF65480167D lea rcx,[rbp+100h]
00007FF6DD7B1588 mov rdx,qword ptr [rbp+0B8h]
00007FF6DD7B158F cmp rdx,qword ptr [rbp+0C0h]
00007FF6DD7B1596 je main+2E1h (07FF6DD7B15C1h)
00007FF6DD7B1598 movups xmm0,xmmword ptr [rsp+20h]
00007FF6DD7B159D movups xmmword ptr [rdx],xmm0
00007FF6DD7B15A0 movups xmm1,xmmword ptr [rsp+30h]
00007FF6DD7B15A5 movups xmmword ptr [rdx+10h],xmm1
00007FF6DD7B15A9 movsd xmm0,mmword ptr [rsp+40h]
00007FF6DD7B15AF movsd mmword ptr [rdx+20h],xmm0
00007FF6DD7B15B4 mov dword ptr [rdx+28h],eax
00007FF6DD7B15B7 add qword ptr [rbp+0B8h],2Ch
00007FF6DD7B15BF jmp main+2F3h (07FF6DD7B15D3h)
00007FF6DD7B15C1 lea r8,[rsp+20h]
00007FF6DD7B15C6 lea rcx,[rbp+0B0h]
どう考えてもpush_backするほうがでかいレジスタでメモリーコピーしてくれていて効率良さそうです。あれ?emplace_backいらんくね?
gccのアセンブリ
lea r9, [rbp-164]
lea r8, [rbp-132]
lea rcx, [rbp-100]
lea rdx, [rbp-68]
lea rsi, [rbp-36]
lea rax, [rbp-384]
lea rdi, [rbp-356]
push rdi
lea rdi, [rbp-324]
push rdi
lea rdi, [rbp-292]
push rdi
lea rdi, [rbp-260]
push rdi
lea rdi, [rbp-228]
push rdi
lea rdi, [rbp-196]
push rdi
mov rdi, rax
call Param& std::vector<Param, std::allocator<Param> >::emplace_back<int, int, int, int, int, int, int, int, int, int, int>(int&&, int&&, int&&, int&&, int&&, int&&, int&&, int&&, int&&, int&&, int&&)
mov DWORD PTR [rbp-200], eax
lea rdx, [rbp-240]
lea rax, [rbp-272]
mov rsi, rdx
mov rdi, rax
call std::vector<Param, std::allocator<Param> >::push_back(Param&&)
emplace_backだと直接構築できる利点はあるものの、可変長引数として積むためにコピーが発生する傾向にありますね。そしてこれ、指示付き初期化もはや関係ねーな?
結論
生の指示つき初期化サイコー!
Parameters.push_back({
.HP = GetData("hp"),
.MP = GetData("mp"),
.Attack = GetData("attack"),
.Defence = GetData("defence"),
.MagicAttack = GetData("magicattack"),
.MagicDefence = GetData("magicdefence"),
.MagicCure = GetData("magiccure"),
.Speed = GetData("speed"),
.Cleverness = GetData("cleverness"),
.MinExp = GetData("exp"),
.Level = GetData("level"),
});