LoginSignup
9
1

More than 1 year has passed since last update.

C++20指示付き初期化を使って定義順をうっかり間違えることを防ぐマクロは必要だろうか(反語)

Last updated at Posted at 2021-12-24

C++ Advent Calender 2021

この記事はC++ Advent Calendar 2021 13日目の記事です。もうすぐクリスマなんやが???
・・・遅刻してすみません。

<<12日目 | [コルーチン]operator co_await と await_transform || 14日目 | 安全で便利なstd::bit_castを使おう >>

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のアセンブリ

emplace_back
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]  
push_back+designated_init
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]  

image.png

どう考えてもpush_backするほうがでかいレジスタでメモリーコピーしてくれていて効率良さそうです。あれ?emplace_backいらんくね?

gccのアセンブリ

emplace_back
        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&&)
push_back+designated_init
        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"),
});
9
1
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
9
1