今日はひたすらアセンブラを追いかけるだけのお話です。ごめんね。
C++では定数の定義を、defineでなく、static constな変数で行うことが推奨されてるらしいです。
// これはC++では(・A・)イクナイ!!
#define VAL (1)
//こっち推奨
const int VAL = 1;
基本的には、これで問題ないのですが、リンク先によると、この定数がクラス変数の場合、定義と実体の二つに分けないといけないそうです。
//ヘッダファイル側(宣言)
class Test
{
private:
protected:
public:
static const int VAL=01234;
};
//ソースファイル側(実体)
//gccでは宣言(ヘッダ)に値を書いても良いけど、VC++等の古いコンパイラだと実体に書かないといけない。
//そのためにenumハックが存在する(上記のリンク先参照)
const int Test:VAL;
ほえー、なるほど。
が、しかし。実際には、gccのときは、実体を書かなくても割と大丈夫みたいです。Ubuntu11.04のx86-64のgccで実験してみました(Win32のMingwでも、機械語は違いますが同じ結果でした)。
以下のソースはgithubからもDLできるよ☆彡 あ、ZIPももちろんあるよ!!
■共通のヘッダファイルはこちら。
(Test.h)
#ifndef TEST_H
#define TEST_H
class Test
{
private:
protected:
public:
static const int VAL1=1234;
static const int VAL2=4321;
};
#endif
特に意味はないです。このTestというクラスの定数を使って、いろいろテストしてみましょう。
以下test*-*とありますが、これをmakeのビルドターゲットにすると自動でコンパイルとかしてくれます。
make test*-*
■(test1-1)実体を定義しないまま、std::coutに入れてみる。
(Test1-1.cc)
#include "./Test.h"
#include <iostream>
int main(int argc, char** argv)
{
std::cout << "VAL: " << std::hex << Test::VAL1 << std::endl;
return 0;
}
ヘッダ上のTest::VAL1やTest::VAL2の実体を一切定義していないことに注意していください。
これをコンパイルして実行すると…
% g++ -o Test1-1.out Test1-1.cc
% ./Test1-1.out
VAL: 1234
どうなってるの!?と思って、アセンブラを出力させてみると…
% g++ -S -masm=intel Test1.cc ←こうすると見慣れたINTEL形式で出力してくれます
% cat Test1.s
main:
.LFB963:
.cfi_startproc
push rbp
.cfi_def_cfa_offset 16
mov rbp, rsp
.cfi_offset 6, -16
.cfi_def_cfa_register 6
sub rsp, 16
mov DWORD PTR [rbp-4], edi
mov QWORD PTR [rbp-16], rsi
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:_ZSt4cout
call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
;---------------------------
mov esi, 1234
;---------------------------
mov rdi, rax
call _ZNSolsEi
mov esi, OFFSET FLAT:_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
mov rdi, rax
call _ZNSolsEPFRSoS_E
mov eax, 0
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
線で囲ったところを見てもらえばわかるとおり、インライン展開されています。ちなみに、1234という数値はこの場所以外一切定義されません。
■(test1-2)じゃ、実体を定義したらどうなるの?
定義を書き加えて
(Test1-2.cc)
//ヘッダファイルとメイン関数の間に追加。
const int Test::VAL1;
としてアセンブラを出力してみると…。
;実体も定義される
_ZN4Test4VAL1E:
.long 1234
;(略)
main:
.LFB963:
.cfi_startproc
push rbp
.cfi_def_cfa_offset 16
mov rbp, rsp
.cfi_offset 6, -16
.cfi_def_cfa_register 6
sub rsp, 16
mov DWORD PTR [rbp-4], edi
mov QWORD PTR [rbp-16], rsi
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:_ZSt4cout
call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
;---------------------------
mov esi, 1234
;---------------------------
mov rdi, rax
call _ZNSolsEi
mov esi, OFFSET FLAT:_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
mov rdi, rax
call _ZNSolsEPFRSoS_E
mov eax, 0
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
Test::VALの値が埋め込みになるのは同じでしたが、それとは独立した実体も定義されました。
一切最適化オプションをつけなくても、それなりに最適化してくれるみたいです。
■(test1-3)ポインタを通す等して、実体が必要な状況をつくってみる
ポインタを通す場合、アセンブラに直接引数を書くことはできません。メモリ上に一旦置かなければなりません。
(Test1-3.cc)
#include "./Test.h"
#include <iostream>
void outVal(const int* val)
{
std::cout << "VAL: " << *val << std::endl;
}
int main(int argc, char** argv)
{
outVal(&Test::VAL1);
return 0;
}
今度からは面倒なのでビルドターゲットでコンパイルします。
% make test1-3
g++ -S -masm=intel Test1-3.cc -o Test1-3.s
g++ -o Test1-3.out Test1-3.cc
/tmp/ccIrdzjI.o: In function `main':
Test1-3.cc:(.text+0x50): undefined reference to `Test::VAL1'
collect2: ld returned 1 exit status
make: *** [test1-3] エラー 1
案の定実行は失敗ですね。アセンブラコードを見てみると?
;実体は定義されないけど...
;(略)
main:
.LFB964:
.cfi_startproc
push rbp
.cfi_def_cfa_offset 16
mov rbp, rsp
.cfi_offset 6, -16
.cfi_def_cfa_register 6
sub rsp, 16
mov DWORD PTR [rbp-4], edi
mov QWORD PTR [rbp-16], rsi
;---------------------------
mov edi, OFFSET FLAT:_ZN4Test4VAL1E ;使おうとする
;---------------------------
call _Z6outValPKi
mov eax, 0
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
予想通りですね。
■(test1-4)でも、最適化を掛けると…。
でもしかし。同じソースのまま、-O3とかを付けて最適化を掛けると、実行可能です。
make test1-4
g++ -S -masm=intel Test1-3.cc -o Test1-4.s -O3 ;ソースは上と同じTest1-3.cc
g++ -o Test1-4.out Test1-3.cc -O3
./Test1-4.out
VAL: 1234
main:
.LFB1004:
.cfi_startproc
push rbp
.cfi_def_cfa_offset 16
mov edx, 5
mov esi, OFFSET FLAT:.LC0
;std::coutへの出力を行う別の関数(outVal)があったのに、インライン展開されてる。
;(展開されてない、outValの実体も別箇所で存在します。)
mov edi, OFFSET FLAT:_ZSt4cout
push rbx
.cfi_def_cfa_offset 24
sub rsp, 8
.cfi_def_cfa_offset 32
.cfi_offset 3, -24
.cfi_offset 6, -16
call _ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l
;----------------------------------
mov esi, 1234 ;ハードコーディング。
;----------------------------------
mov edi, OFFSET FLAT:_ZSt4cout
call _ZNSolsEi
mov rbx, rax
mov rax, QWORD PTR [rax]
mov rax, QWORD PTR [rax-24]
mov rbp, QWORD PTR [rbx+240+rax]
test rbp, rbp
je .L11
cmp BYTE PTR [rbp+56], 0
je .L9
movzx eax, BYTE PTR [rbp+67]
■じゃポインタにしないなら良いんだね良かった良かった
ポインタとして使えないのはdefineでも一緒ですし、だったら同じ条件ですねやったー。
…と言いたいところだが、(大佐風)微妙にこったことをすると必ず実体の定義が要らなくなるとは限りません。
■(test2-1)三項演算子の結果に使う
三項演算子を使って、フラグによって定数を振り分けたい!ありそうなシチュエーションです。
(Test2.cc)
#include "./Test.h"
#include <iostream>
int main(int argc, char** argv)
{
int val = argc > 1 ? Test::VAL1 : Test::VAL2;
std::cout << "VAL: " << val << std::endl;
return 0;
}
これを最適化なしでコンパイルすると…
% make test2-1
g++ -S -masm=intel Test2.cc -o Test2-1.s
g++ -o Test2-1.out Test2.cc
/tmp/cc87c0Ta.o: In function `main':
Test2.cc:(.text+0x17): undefined reference to `Test::VAL1'
Test2.cc:(.text+0x1f): undefined reference to `Test::VAL2'
collect2: ld returned 1 exit status
make: *** [test2-1] エラー 1
なんと。駄目でした。アセンブラを読んでみると、
;もちろん実体は定義されてない。
main:
.LFB963:
.cfi_startproc
push rbp
.cfi_def_cfa_offset 16
mov rbp, rsp
.cfi_offset 6, -16
.cfi_def_cfa_register 6
sub rsp, 32
mov DWORD PTR [rbp-20], edi
mov QWORD PTR [rbp-32], rsi
;---------------------------------- ここから三項演算子
cmp DWORD PTR [rbp-20], 1
jle .L2
mov eax, DWORD PTR _ZN4Test4VAL1E[rip] ;実体は無いのに
jmp .L3
.L2:
mov eax, DWORD PTR _ZN4Test4VAL2E[rip] ;使おうとする。
;----------------------------------
.L3:
mov DWORD PTR [rbp-4], eax
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:_ZSt4cout
call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
mov edx, DWORD PTR [rbp-4]
mov esi, edx
mov rdi, rax
call _ZNSolsEi
mov esi, OFFSET FLAT:_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
mov rdi, rax
call _ZNSolsEPFRSoS_E
mov eax, 0
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
■(test2-2)最適化してみる。
でも最適化をつけると…
make test2-2
g++ -S -masm=intel Test2.cc -O3 -o Test2-2.s
g++ -o Test2-2.out Test2.cc -O3
./Test2-2.out
VAL: 4321
main:
.LFB1003:
.cfi_startproc
push rbp
.cfi_def_cfa_offset 16
mov eax, 4321
mov edx, 5
mov esi, OFFSET FLAT:.LC0
push rbx
.cfi_def_cfa_offset 24
mov ebx, 1234
.cfi_offset 3, -24
.cfi_offset 6, -16
sub rsp, 8
.cfi_def_cfa_offset 32
cmp edi, 2
mov edi, OFFSET FLAT:_ZSt4cout
;----------------------
cmovl ebx, eax ;フラグを見て、条件にあうならeaxをebxへ。ここが三項演算子。
;----------------------
call _ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l
mov esi, ebx
mov edi, OFFSET FLAT:_ZSt4cout
call _ZNSolsEi
mov rbx, rax
mov rax, QWORD PTR [rax]
mov rax, QWORD PTR [rax-24]
mov rbp, QWORD PTR [rbx+240+rax]
test rbp, rbp
je .L8
cmp BYTE PTR [rbp+56], 0
je .L4
movzx eax, BYTE PTR [rbp+67]
まさかの分岐命令…なし…!?cmovlはこちら参考。
また、何度もstd::coutの関数を呼ぶのでなく、出力関数は一度だけのようです。
■(test2-3)じゃ素直にif文を使う。
じゃあ、if文で同じことをしましょう。
(Test2-3.cc)
#include "./Test.h"
#include <iostream>
int main(int argc, char** argv)
{
int val;
if(argc > 1){
val = Test::VAL1;
}else{
val = Test::VAL2;
}
std::cout << "VAL: " << val << std::endl;
return 0;
}
% make test2-3
g++ -S -masm=intel Test2-3.cc -o Test2-3.s
g++ -o Test2-3.out Test2-3.cc
./Test2-3.out
VAL: 4321
むむ。成功してしまいました。アセンブラは?
main:
.LFB963:
.cfi_startproc
push rbp
.cfi_def_cfa_offset 16
mov rbp, rsp
.cfi_offset 6, -16
.cfi_def_cfa_register 6
sub rsp, 32
mov DWORD PTR [rbp-20], edi
mov QWORD PTR [rbp-32], rsi
cmp DWORD PTR [rbp-20], 1
jle .L2
mov DWORD PTR [rbp-4], 1234
jmp .L3
.L2:
mov DWORD PTR [rbp-4], 4321
.L3:
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:_ZSt4cout
call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
mov edx, DWORD PTR [rbp-4]
mov esi, edx
mov rdi, rax
call _ZNSolsEi
mov esi, OFFSET FLAT:_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
mov rdi, rax
call _ZNSolsEPFRSoS_E
mov eax, 0
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
分岐命令で割り振るまでは三項演算子と同じ。でもif文の場合はインラインで定数を書いてくれるみたいですね。
ちなみに、最適化をかけたときの結果は、三項演算子の時と殆ど同じになりました(test2-4)。
■残りのテストの概要
残りのテストの結果は上記の結果から推測できる面白くない結果だったので概要だけ。
- test3:三項演算子でも実体が書き加えてあればちゃんと動作します。そりゃそうさね。
- test4-1:テンポラリな変数に三項演算子を代入するのでなく、そのままstd::coutに突っ込むところに直接書きました。でも駄目でした。
- test4-2:4-1をコンパイルする際に最適化を掛けると、やっぱり大丈夫でした。
どうやら、三項演算子はif文に展開されたりするわけではなく、別扱いになっていて、最適化フラグが掛からないときは扱いが少し違う…そんな感じでしょうか。gccのソースは読んでないのでわかりませんが…。
エミュレータで実体を定義し忘れてたのになぜか動いていたのですが、三項演算子を使ったところでコンパイルエラーでした。なお、実体を定義する前にVC++でビルドしてたので、VC++も同じようにやってくれるみたいですが、最適化の結果どうなるのか、とかは確かめてないです。
■けつろん!
- ちゃんと実体書こう!
- define替わりに使うならenumハックを検討しよう!
ほんとC++は黒魔術やでぇ…いえ、仕様通り書かない私が悪いんです。でも動いちゃうのもちょっと困るよ…。