メンバ関数にconst修飾って何ぞやとという質問を受けました。メンバ変数を変更できないよって印…なのは分かっているのだそう。ここで、もう少しC++と仲良くなるために、もう少し深く彼らと付き合ってみたいと思います。オマケで、&(&&)修飾子についても考えます。
メンバ関数に付ける修飾子は、static, virtual, inline/constexpr, const/volatile, &/&&, noexcept/throw, override/final があります。このうち、今回取り扱うconst/volatile, &/&&は似た特性のものであるため、一緒に扱いたいと思います。
これら修飾子が実際に修飾するのは、メンバ関数に暗黙的に渡される第0引数とも言うべき、thisポインタです。
このthisポインタさえ解決してしまえば、メンバ関数も通常の関数と同じように扱うことができます。
メンバ関数が修飾されない場合
struct something
{
int n;
void print() { std::cout << n << std::endl; }
};
int main()
{
something s1 = { 123 };
union
{
void (something::*memfun)();
void (*print)( something& );
};
memfun = &something::print;
print(s1);
}
unionで無理やりメンバ関数ポインタを関数ポインタに変換し、その第一引数には*thisの参照を受け取るようにしました。動作の保証は環境によりしかねますが、ideoneでは動いたので載せておきます。
http://ideone.com/yWzEzW
const修飾される場合
上のprintは修飾されない場合でしたが、const修飾したらどうなるでしょうか。
メンバ関数と、メンバ関数ポインタと、通常の関数ポインタに注目してみてください。
struct something
{
int n;
void print() const { std::cout << n << std::endl; } // const
};
int main()
{
something s1 = { 123 };
union
{
void (something::*memfun)() const; // const
void (*print)( const something& ); // const
};
memfun = &something::print;
print(s1);
}
3行目、printをconst修飾しています。
そしてそれに伴ってunionの中身の1行目がのメンバ関数ポインタがconst修飾され、最も注目すべきは次の2行目、thisポインタにconst修飾をつけています。
これがメンバ関数にconst修飾をするということなのです。
const修飾された引数であるthisのメンバ変数を変更することは当然できません。つまり、メンバ関数をconst修飾するとは、メンバ変数を変更しないという意味論の前に、thisポインタが渡される見えない仮引数をconst修飾することである、という意味であることを知っておく必要があります。
volatileは全く同様なので、次は&(&&)修飾子に移らせて頂きたいと思います。
&(&&)修飾される場合
最初に似た特性であると書いたのは、thisポインタに関係する修飾子という意味で、似た特性を持っているからです。
こいつはそもそもvolatileよりも知名度がないと思いますので、まず&(&&)修飾子から説明したいと思います。
struct something
{
something& operator = ( const something& ) & = default;
void memfun() & ;
void memfun() && ;
};
一番右についてる&とか&&がそうです。
これは、呼び出し元のオブジェクトが右辺値か、左辺値かでオーバーロードすることができるもので、一番使うのは上の代入演算子だと思います。これがあれば、右辺値への代入を防ぐことができますからね。
something() = something(); // error!
something().memfun(); // call memfun() &&
something sth;
sth.memfun(); // call memfun() &
このようにコードの安全性を高めることができます。
もうお察しのとおり、こいつらはthisポインタを左辺値で、あるいは右辺値でのみ受け取るように修飾しています。
これを通常の関数ポインタの宣言に置き換えると、以下のようになるでしょう。
void (*memfun)( something& );
void (*memfun)( something&& );
これなら左辺値は左辺値、右辺値は右辺値になります。
ここで、勘の鋭い人は、const修飾されていたら、左辺値修飾しても右辺値から呼び出すことができるのでは? とお考えになることでしょう。
すなわち、void(*memfun)( const something& );
であれば、memfunはsomethingの右辺値を受け取ることができることからわきあがる疑問ですね。
その通りなのです。
const修飾がされていると、左辺値修飾しても右辺値を受け取ることができますし、またconst修飾がされているということは右辺値修飾の意味をなくすことになるので、(&&はそもそもconst修飾されない右辺値参照の為の修飾子です)constと&(&&)演算子が同時に指定されることはないと言えるでしょう。面白い挙動ではありますが、ナンセンスでもありますね。
struct something
{
void memfun() const & {}
void memfun() && {}
};
int main()
{
something().memfun();
}
2つとも定義される場合は正しく解決され、右辺値の方をコメントアウトしても左辺値の方が呼ばれます。
http://ideone.com/EyXaWr
以上で終わりたいと思います。
メンバ関数がthisを暗黙のうちに引数として受け取っていて、それを経由してメンバ変数にアクセスしていることを踏まえれば、例えばラムダでメンバ変数をコピーキャプチャするとthisをコピーキャプチャしてしまい、結果メンバ変数を参照キャプチャしてしまうことも納得いくのではないでしょうか。メンバ関数も所詮はメンバ変数を直接見ることもできないただの関数なのです。
繰り返しにはなりますが、くれぐれも上のようにメンバ関数ポインタを通常のポインタにキャストすることはしないようにしてくださいね。
もし上のような書き方をしたいときは、.*
演算子や->*
演算子を使うようにしましょう。(影の薄い演算子や修飾子の話ばっかりですね……)
( entity.*memfunptr )( arguments... );
のようにしてメンバ関数ポインタとエンティティを別々に書けます。どうしても気持ち悪いのであればラッパに隠してしまいましょう。
auto lambda = [] ( something& _this, auto... args ) ->decltype( auto )
{ // ラッパの中身はさらに気持ち悪い
return (_this.*memfun)( std::forward < decltype( args ) >( args )... );
};
lambda(); // 使う場面ではあんま気持ち悪くない