#事の起こり
C++の2ヶ月勉強しましたが、全く人のコードが読めません!! 原因を自分なりに考えてみた結果、細かな構文を理解できていないことに気づきました。そこで、今回はinline関数とconstメンバ関数といったC++の細かな機能2つについて触れていきたいと思います。
#インラインとは?
まず、インラインについての説明です。結論から言うと、プログラミングにおけるインラインとはインライン展開の事です。インライン展開とは関数の内容をコード上に展開する事です。 例を用いて説明します。例えば、下のようなコードがあった場合、
void print_hello(void){
cout<<"print_hello" <<endl;
}
int main(void){
for(int i = 0;i<20000;i++){
print_hello();//関数呼び出し
}
}
main関数のforループで呼び出されているprint_hello関数の内容をそのまま展開(下記コード)することをインライン展開といいます。
void print_hello(void){
cout<<"print_hello" <<endl;
}
int main(void){
for(int i = 0;i<20000;i++){
cout<<"print_hello" <<endl;//関数の中身を展開
}
}
なぜ、このようなことをするのかと言うと**ひとえにプログラムの高速化のためです。**基本的に関数を呼び出す時は、その関数のおけれているメモリに移動して関数を実行します。**この呼び出しの際に生じるメモリ移動のせいで遅いんだ!! もう、関数に直接書いて呼び出しの手間をなくしてしまえ!!**と思い立って生まれたテクニックがインライン展開です。(上の例だと、print_hello関数を呼び出すために、実行していた20000回のメモリ移動処理がインライン展開することで0回になります)
#インラン展開に関する補足 (読み飛ばしてOK)
誤解のないように言っておくと、インライン展開はC++固有のテクニックではありません。あくまで、プログラミング全般におけるテクニックです。 それに、インライン展開と言うのはかなり昔からあるテクニックなので、現在ではわざわざ自分でインライン展開しなくてもコンパイル時にコンパイラが勝手に行ってくれるます。(もちろん、コンパイラによって、インライン展開を行うか否かの判断基準は異なりますが)
速くなるなら関数に分けずにインライン展開しまくれば良いいじゃんと言う意見もありますが、これもあまり良くありません。なぜなら、関数に分けた方がユニットテストが行いやすくなるからです。
#inline関数
C++でインライン展開したい、でも自分でやるのめんどくさい・・・。コンパイラが自動でやってくれたらいいけど、やってくれる保証もないし・・・
そんな時に、使えるのがinline関数です!! 使い方はとても簡単で、関数定義の前にinline修飾子をつけるだけです。
inline void print_hello(void){//関数定義の手前にinlineをつけるだけ
cout<<"print_hello" <<endl;
}
int main(void){
for(int i = 0;i<20000;i++){
print_hello();//関数呼び出し
}
}
簡単ですね。しかし、実際はinline修飾子をつけても必ずしもインライン展開されるわけはないようです。 例えば、inline展開した結果、プログラムのサイズが大きくなりすぎてしまったり、展開対象を関数ポインタで読んでいる場合などはインライン展開を行わない時もあるようです。(コンパイラによって挙動が変わるため、厳密に制御することは非常に難しいです。)
#inlineメンバ関数
inline修飾子は下記コード(hppであることに注意してください)のように、クラスのメンバ関数にも使えます。ただし、書き方には色々注意点があるので注意してください。
その注意点とは、inlineメンバ関数はクラスを定義したヘッダファイルで実装することです。こうしないと、どこにinline展開していいのかコンパイラが解釈してくれません。(本来だったら、ヘッダファイルにクラス定義をしてメンバ関数の実装は別ファイルで行なった方が適切です。(Allocationエラーを防げる))
class A{
private:
int a;
int b;
public:
void set_a(int num);//aに値をセット
void set_b(int num);//bに値をセット
int sum_a_b(void);//a+bを計算
};
//ヘッダファイルでしかinlineで定義できない
//別ファイルで書くとinline展開の時にエラーになる.
inline void A::set_a(int num){
a = num;
}
inline void A::set_b(int num){
b = num;
}
inline int A::sum_a_b(void){
return a+b;
}
ちなみに、inlineメンバ関数も用いるのと、クラス内でメンバ関数を実装することは本質的に同じことです。
#constメンバ関数
続いて、constメンバ関数についてです。inline関数とはなんの関係もないので、inline関数のことは一旦忘れましょう。
constメンバ関数は、メンバ関数からそのクラスのメンバ変数を変更できなくする為のものです。例えば、先の例のint sum_a_b(void)関数をint sum_a_b(void) constに書き直してみます。そして、int sum_a_b(void) constの実装の時に、メンバ変数aを書き換える処理を追記すると、Cannot assign to non-static data member within const member function 'sum_a_b'のエラーが発生しメンバ変数aの書き換えを防げます。
class A{
private:
int a;
int b;
public:
void set_a(int num);//aに値をセット
void set_b(int num);//bに値をセット
int sum_a_b(void) const;//a+bを計算
};
//ヘッダファイルでしかinlineで定義できない
//別ファイルで書くとinline展開の時にエラーになる.
inline void A::set_a(int num){
a = num;
}
inline void A::set_b(int num){
b = num;
}
inline int A::sum_a_b(void) const{
//a = 20;//メンバ関数のしようとするとエラー
return a+b;
}
int main(void){
A a;
a.set_a(10);
a.set_b(20);
cout << a.sum_a_b() <<endl; //30と表示される
}
constメンバ関数を使うと、そのメンバ関数がメンバ変数を変更しないと言うことが一発でわかりますね。メンバ関数の挙動をあらかじめ知ることができるので,可読性も上がりそうです。 また、上記のコードのようにinline関数との併用可能です。
#constメンバ関数とmutable変数
実はconstメンバ関数から、メンバ変数を変更する方法があります!!!、それがmutable変数です!!!。(もう、カオスすぎますね。C++の良くないところだと思います。)。使用例は下記です。先ほどのコード、クラスAのメンバ変数aをmutableにしています。
class A{
private:
mutable int a;//mutable変数に変更
int b;
public:
void set_a(int num);//aに値をセット
void set_b(int num);//bに値をセット
int sum_a_b(void) const;//a+bを計算
};
//ヘッダファイルでしかinlineで定義できない
//別ファイルで書くとinline展開の時にエラーになる.
inline void A::set_a(int num){
a = num;
}
inline void A::set_b(int num){
b = num;
}
inline int A::sum_a_b(void) const{
a = 20;//mutable変数は変更可能。
return a+b;
}
int main(void){
A a;
a.set_a(10);
a.set_b(20);
cout << a.sum_a_b() <<endl; //40と表示される
}
mutable変数のメリットはなんなのでしょうか。それは、constメンバ関数からどうしても変更したいメンバ変数がある時に変更できる点です!!。一見すると複雑さを助長しているようにも思えますが、ポジティブに考えれば柔軟さが増していると言うことです。しかし、miutable変数を使いまくるともはやconstメンバ関数を定義する意味はなくなるので、多用は禁物です。
#constメンバ関数への理解をさらに深める
実は、constメンバ関数はポインタがさす値や参照の参照先の値は変更することができます。(もう本当にややこしい)
#まとめ
2つの修飾子をやるだけで、こんなに苦労するとは。。。C++は細かい事の積み重ねで読めなくなっていく点が難しいところです。