ネタの蛇足的解説
本記事はhttps://twitter.com/staryoshi/status/669763731610431488のたくさんの反響を記念して蛇足的な解説を並べたものです。あまりC++に興味を持っていなかった人もこれを見て興味を持っていただけたら幸いです。もちろん、実際にはこれらの表記で困ることはないのですが、こんなこともできてしまうという一種の例でもあります。
このコラの目的は、同一の識別子を用いて関数呼び出しに見えるが実際の定義の仕方は異なるというものはどのくらいあるのか、というのが発端です。ちなみにアイディは宇月まりな on Twitter: "僕が古文を嫌う理由 https://t.co/XiONjbFDYb"より。全く外見は同じだが中身が違う……C++だ、と思って作りました。
当然、いかに目をこらしても文字に違いはありませんので文脈から読み取るしかありません。
その前に
今回、C++啓蒙のためのネタとして投稿したわけですが、どうもC++の魅力が伝えられなかったみたいで大変残念です。
普通に書いていればマクロ名を小文字で書くこともなければ、ローカル変数に代入した変数名と関数名がかぶって混乱することもありません。また、知らなければラムダも関数オブジェクトもマクロもクラスも使わないで書くことだってできます。使わない機能は使わなければいいのです。
C++は使いたい機能だけ使うことができますし、そうすれば自然にC言語に近くなってきます。ちょっと便利なC言語のように使ってもいいと思いますし、多くの人がそうしているでしょう。
今回のネタを通してC++が複雑奇怪だから触るのはやめよう、という思った方は他の最近の言語でも実は多くの機能を知らないのではないでしょうか。他の言語もだいたい似たようなことやそれ以上のことができますが、それを使わない選択も用意されています。
修得するためには結局は1つ1つ丁寧に定義とその存在意義を把握して実際に使ってみるしかないのです。また、修得しなくても代替方法はいくらでも用意されています。ただ、どうしてそんなものがあるかと言うことを改めて考えることでより表現力が広がり、(コーディングの)効率が上がるのではないかと思われます。
あとよく言われるようにJavaScriptのthisも大概です() perlやPHPもアレだし、Rubyも闇。Haskellは(初見で)訳分からないし、Rustは糞難しいし、どこに行っても闇しかないんです……。
各コマのソースコード例
1コマ目(関数呼び出し)
C++関数とは、ある引数を取ってそれ(とまあいろいろな情報)を元にある結果を返すことのできる処理のまとまりです。引数には0個以上を指定します。つまりない場合もあります。
void name() { } // 関数定義 int main() { name(); // 関数呼び出し }
namespace内でも広域内でもはたまたstatic, constexpr, inlineをつけても基本的には関数です。
void name(); // グローバル宣言 void name() { } // グローバル定義 namespace { void name(); // 名前空間内宣言 void name() { } // 名前空間内定義 }
2コマ目(マクロ呼び出し)
マクロとはC++のソースコード上にある識別子を別の識別子に置き換えてしまう黒魔法です。0個以上の引数を取る置換列により置換できます。
#define name() std::cout<<"empty\n" int main() { name(); // マクロ呼び出し }
3コマ目(メンバ関数呼び出し)
C++にはクラスを定義できます。クラスには関数を持つことができ、通常はクラスを直接操作する関数です。ただしメンバ関数と上記の関数とは扱いが異なります。ただし、staticをつけると上記の関数とほぼ同じ扱いになります。定義は同じく{}をつけるだけです。
class C { public: C() { name(); // クラス内のメンバ関数呼び出し this->name(); // これはthis->を省略していない場合 } void name(); // メンバ関数の宣言 static void name(); // 静的メンバ関数の宣言 }; int main() { C c; }
4コマ目(関数ポインタやラムダ式の関数呼び出し)
関数ポインタは関数のアドレスを別の変数Xに保持し、X経由でその関数を呼び出すものです。
ラムダ式とは、端的にいえば関数を関数内でかけるようにして、さらに他の機能も加えて便利にしたものです。ラムダ式は関数オブジェクトと考えられますが実際にはクロージャ型という異なるものです。というのも関数オブジェクトと定義してしまうとコンパイラが最適化できない場合があるためです。
この2つは実は違う動作がなされるのですが、キャプチャしなければ同じ扱いなので一緒にしてしまいました。
void func() { } // 何らかの関数があって int main() { using func_type = void(*)(); // 関数ポインタの型宣言 func_type name = func; // 関数ポインタの代入 name(); // 変数経由で読み出し (*name)(); // こちらの書き方も可能 }
キャプチャのないラムダ式も代入できます。キャプチャとは、ラムダ式の宣言されたスコープで見える変数を、ラムダ式内で利用することです。何もキャプチャしないときはからのラムダ導入子[]
を指定して[]() { ... }
と書きます。この場合、実質関数ポインタとなります。
int main() { auto name = []() { /* 処理 */ }; name(); // ラムダ式の実行 }
ちなみにキャプチャすると関数ポインタではなくクロージャ型です。以下の例ではラムダ式の外の変数xをキャプチャし、ラムダ式内で変更します。これ以上は本題から逸れるのでやめておきます。興味がありましたらC++で遊んでみると楽しいですよ!
int main() { int x = 100; auto name = [&]() { x += 10; }; // クロージャ型 name(); // 実行 x; // x = 110 }
5コマ目(コンストラクタ呼び出し)
コンストラクタはクラスを初期化する特別な関数です。コンストラクタはメンバ関数とは違ってクラス名と同じにしなければいけません。ちなみにスコープから抜けたりオブジェクトが破棄されたりするときに呼び出されるデストラクタは~をつけて明示的に呼び出せます。今回は~でも入っていたら分かってしまうので除外しましたが。
class name { public: name(); // コンストラクタ ~name(); // デストラクタ };
int main() { name(); // コンストラクタの実行 }
7コマ目(関数呼び出し演算子)
クラスには、そのクラスのインスタンスに直接()をつけて関数呼び出しのように振る舞える関数呼び出し演算子を定義できます。このようなクラスを関数オブジェクトと呼び、この発想を元に簡略記法のラムダ式が提案されました。C++では標準ライブラリやその他のインターフェースとして基本的な知識となります。*1
class C { public: C(); void operator()() { } // 関数呼び出し演算子のオーバーロード }; int main() { C name; name(); // インスタンス名に()をつけて呼び出せる return 0; }
8コマ目(暗黙の型変換による呼び出し)
たぶんC++を知っている人でも漫画内の短い文では推測が困難、あるいは説明不足故に想像がつかなかった人がいるかも知れません。C++では型は暗黙に(=知らないうちに)変換されることがあります。今回はこれを利用しました。
void func() { } // ある関数 class C { public: using func_type = void(*)(); // 変換演算子の定義 operator func_type() { return func; // 関数ポインタを返す } }; int main() { C name; name(); // nameは暗黙の変換によってvoid(*)()にされ、呼び出される return 0; }
この暗黙の変換はやっかいな結果を生むこともあります。そこでこれを抑制するには、explicitをつけます。これを指定することにより、型変換が明示的あるいはコンストラクタに入れられるときのみに行われます。
explicit operator func_type() { return func; // 関数ポインタを返す }
このexplicitをつけた変換演算子が呼び出されるのは、コンストラクタの直接初期化(=のない初期化)かあるいは明示的にキャストした場合に限られます。
次の例ではクラスUがクラスCに変換するexplicit変換演算子を定義しています。クラスCへ暗黙の変換がされるのは1つだけです。すなわち、コンストラクタで直接初期化する場合です。
class C { public: C() {} C(C const& it) { } C& operator=(C const& it) { return *this; } }; class U { public: explicit operator C() { return C(); } }; int main() { U u; C c(u); // ○ Cを直接初期化しているため暗黙変換される c = u; // × UからCへ暗黙変換されないため代入できない C c2 = u; // × コピー初期化では上の直接初期化のようにはいかない }
ちなみによく使われるif文等の中に入れるexplicit bool()
はOKです。これはif分の中の条件は暗黙裏に変換されることが約束されているためです。
蛇足ですが有用な使い方を示しておしまいにしておきます。
class Even { public: Even(int n) : n(n) {} explicit operator bool() { return n % 2 == 0; } private: int n; }; int main() { if (Even(10)) { printf("偶数\n"); } else { printf("奇数\n"); } }