2016年4月から「センメツコースター(仮)」というiOS用のゲームを作っています。
2016:08:18 20-37 from shinya on Vimeo.
ジェットコースター+アクションゲームな感じです。
最初は1ヶ月で完成させることを目標にしていましたが、4ヶ月も経ってしまいました。ゲーム開発って怖い…。 まだ完成の目処は立っていませんが、2016年内リリースを目標にしています。
どうか完成しますように🙏
2016年4月から「センメツコースター(仮)」というiOS用のゲームを作っています。
2016:08:18 20-37 from shinya on Vimeo.
ジェットコースター+アクションゲームな感じです。
最初は1ヶ月で完成させることを目標にしていましたが、4ヶ月も経ってしまいました。ゲーム開発って怖い…。 まだ完成の目処は立っていませんが、2016年内リリースを目標にしています。
どうか完成しますように🙏
Xcode7.3へアップデート後、QtCreatorのC++プロジェクトのビルド時のリンク時間がやたら遅いなと思っていましたが解決法が書かれたページを見つけ、試してみたところ治ったので共有したいと思います。
※Xcode自体でのビルドではなく、Xcode付属のClangを使ったときに発生した問題です。私の環境ではXcode自体でのビルドでは問題ありませんでした。
参考(感謝!): Extremely slow linking with clang, ld from XCod... | Apple Developer Forums
QtCreatorのC++プロジェクトのビルド時のリンク時間がXcode7.3へのアップデート前は1〜2秒だったのが、アップデート後25〜30秒になった。これはデバッグビルド時のみ発生し、リリースビルド時はアップデート前と同じように1〜2秒でリンクが完了する。
デバッグビルド時のコンパイラオプションに-O0 -fvisibility-inlines-hidden
を与える。
リンク時間が長くなったのをXcode7.3へのアップデート後と書きましたが、その前にMacOSを10.10から10.11へアップデートしたり、QtCreatorをバージョンアップしたりしていたので、もしかしたらそちらに原因があった可能性もなきにしもあらず…。いや、たぶんXcode7.3へのアップデートが原因だと思いますが。
プログラミングをしていてソースコードの行数が一体どれくらいになっているのか気になることがよくあるのですが、Gitリポジトリを解析して統計情報を生成してくれるGitStatsというツールがあることを知りました。ソースコード行数はもちろん、ファイル数、コミット回数などの推移も見ることができます。
公式サイト GitStats - git history statistics generator
どんな感じで表示されるのかはこちらの記事で詳しく紹介されています。
Gitリポジトリに蓄積された歴史を可視化、グラフ化する·GitStats MOONGIFT
今回はメモもかねて私がMacでGitStatsを利用するためにやったことを記しておきたいと思います。
試した環境はMacOSX 10.11です。
大まかな手順は以下の通り
GitStatsはgnuplotを使うようなのでgnuplotをインストールします。もちろんすでにインストールされている場合は不要です。
gnuplot公式サイトのcontributed executables for OSXというページからMac用実行ファイル(dmg)が入手できるのでそれを利用します。今回はgnuplot-5.0.5-quartz.dmg
というファイルをダウンロードしました。とりあえず最新バージョンを選んでおけばいいと思います。
ダウンロードしたら他の一般的なMacアプリケーションと同じようにdmgファイルをダブルクリックで展開してgnuplotアプリケーションをアプリケーションフォルダなどへコピーします。
このままだとGitStatsはgnuplotの場所がわからないのでパスを通します。具体的にはgnuplotの実行ファイルのシンボリックリンクを/usr/local/bin/
に貼ります。
ターミナルを起動して以下のコマンドを実行します。これはgnuplotアプリケーションをアプリケーションフォルダに配置した場合です。
ln -s /Applications/Gnuplot.app/Contents/Resources/bin/gnuplot /usr/local/bin/
/Applications/Gnuplot.app
からではなく、さらにその中にあるContents/Resources/bin/gnuplot
からシンボリックリンクを貼ることに注意。.app
はバンドルと呼ばれるただのフォルダで実行ファイルはその中にあります。
シンボリックリンクを貼ったら、ターミナルでgnuplot
と入力してgnuplotが起動するか確認してみます。
ターミナルを起動して、適当な場所に移動してから以下のコマンドを実行します。
git clone git://github.com/hoxu/gitstats.git
git clone
で入手したgitstats
ディレクトリに移動して以下のコマンドを実行します。
make install
これでGitStatsのインストールは完了です。
ターミナルを起動して以下のコマンドを実行します。
gitstats 【リポジトリ(.git)の場所】 【生成場所】
コマンドを実行すると指定した生成場所に40個ぐらいのファイルが生成されます。その中にあるindex.html
をブラウザで開けばリポジトリの統計情報を見ることができます。
ところで、GitStatsが生成した統計情報の行数(Lines)項目はバイナリファイルでもカウントされているようです。FilesタブのExtensions項目で拡張子ごとの行数が見れるので、ソースコードの行数を知りたい場合はそこを見たほうがいいかもしれません。
namespace HogeCore {} namespace Hoge = HogeCore;
しかし、複数の名前空間に同じ別名を与えることはできません。
namespace HogeCore {} namespace HogeGraphics {} namespace HogeAudio {} namespace Hoge = HogeCore; namespace Hoge = HogeGraphics; // コンパイルエラーnamespace Hoge = HogeAudio; // コンパイルエラー
ちなみにサンプルコードの意図としては、Hogeというライブラリの内部に複数の名前空間が存在し、ライブラリのユーザーにはすべてHogeという名前(名前空間)でアクセスできるようにさせたい、というものです。
C++では複数の名前空間に同じ別名を与えることはできませんが、別の方法で実質的に同じようなことを行うことができます。その別の方法とは、別名用の名前空間を作り、そのなかでusing namespace
を使うことです。
namespace HogeCore { class A{}; } namespace HogeGraphics { class B{}; } namespace HogeAudio { class C{}; } // 別名用の名前空間を作り…namespace Hoge { // 使用可能にしたい名前空間をusing namespaceするusingnamespace HogeCore; usingnamespace HogeGraphics; usingnamespace HogeAudio; } int main() { Hoge::A a; Hoge::B b; Hoge::C c; return0; }
注意点としては、名前空間を1つにまとめることで名前の衝突が起こる可能性があります。乱用はしないようにしましょう。
参考: c++ - Multiple aliases for a namespace? - Stack Overflow
------------------------------- Lua ------------------------------- /****************************************************************************** * Copyright (C) 1994-2012 Lua.org, PUC-Rio. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ******************************************************************************/ ------------------------------- picojson ------------------------------- Copyright 2009-2010 Cybozu Labs, Inc. Copyright 2011-2014 Kazuho Oku All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ------------------------------- alure ------------------------------- Copyright (c) 2009-2010 Chris Robinson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ------------------------------- libogg ------------------------------- Copyright (c) 2002, Xiph.org Foundation Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - Neither the name of the Xiph.org Foundation nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ------------------------------- libvorbis ------------------------------- Copyright (c) 2002-2008 Xiph.org Foundation Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - Neither the name of the Xiph.org Foundation nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
長い間作っていたゲームがようやく完成しました。
センメツコースターはiOS用2Dアクションゲームです。広告無しの有料アプリ(240円)です。 レールの上をコースター(乗り物)で移動して敵を倒す(センメツ)するのが目的です。
こんな人におすすめです。
5つのワールドにそれぞれ5ステージで合計25ステージあります。クリアするだけなら1〜2時間くらいのボリュームだと思いますが、各ステージでスコアレートで星3つを目指したり、チャレンジモードで遊んだりすればもうちょっと長く遊べます。
正直地味なゲームですが、丁寧に作ったつもりなので楽しんでいただけたらうれしいです。
※ゲーム内の音楽はPANICPUMPKINさんのフリー素材を使わせていただきました。
先日、iOS用ゲームアプリ「センメツコースター」をリリースしました。このゲームの開発にはゲームエンジンは使っておらず、C++とOpenGLとOpenALなどで実装されています。最近はゲームエンジンを使うことが当たり前になっており、ゲームエンジンを使わないゲームの作り方があまり知られていない気がしたので「センメツコースター」を例にして、どうやってC++とOpenGLでゲームを作るのかを(大雑把に)紹介したいと思います。記事中で取り上げたツールやライブラリへのリンクは最後にまとめて掲載してます。
作業はすべてMac上で行いました*1が、基本的にクロスプラットフォームなツールやライブラリしか使っていないのでWindowsでも同じように開発できると思います。また、「センメツコースター」はiOS用アプリですが開発自体はMac上で行っていたので、PCゲーム開発の話として読んで下さい。iOS対応の話はまた別の機会に…。
まず、そもそもゲームを作るとはどういうことなのか。ゲームは何で出来ているのか。大きく分けると以下の要素になります。
ソフトウェアの生成とファイル読み書き以外、すべて素のC++では実現できません。外部のライブラリに頼る必要があります。逆にいうと、これらさえ用意できればあとはC++でコードを書くだけでゲームを作ることができます(理論的には)。
ゲームとは何なのか。実態はソフトウェアです。アプリケーション、実行ファイルとも言います。ソフトウェアを生成する手段はいろいろありますが、「センメツコースター」ではC++コンパイラを使っています。つまりC++コードをコンパイルしてソフトウェアを生成しているわけです。
C++コンパイラはいろいろな種類がありますが、今回はClangを使いました。WindowsだとVC++が一番手軽かもしれません。コンパイラは直接使うことも出来ますが、一般的には統合開発環境(IDE)経由で使います。C++の統合開発環境もいろいろありますが、私はQtCreatorを好んで使っています。QtCreatorはクロスプラットフォームなのでWindowsでも使うことができます。他の統合開発環境はMacだとXcode、WindowsだとVisualStudioが有名です。
次に必要なのは絵の表示です。素のC++では絵の表示を行うことができないので外部ライブラリに頼ることになります。C++を使ったゲーム開発で一般的なのはOpenGLとDirectXです。記事のタイトルにもあるように今回はOpenGLを選びました。OpenGLの魅力は何と言っても動作環境の多さです。MacとWindowsはもちろんiOSやAndroidでも動作します。ただしOpenGLと言っても1枚岩ではなく、複数のバージョンとOpenGLESというコンパクト版があります。OpenGLESは組み込み用のOpenGLなので、Mac,WindowsはOpenGLのみに対応、iOS,AndroidはOpenGLESのみに対応しています。ですが、OpenGLESはOpenGLのコンパクト版なので、OpenGLESを使っておけばそのままOpenGL環境でも動作します(バージョンは合わせる必要があります)。「センメツコースター」ではOpenGLES2.0を選びました。
ところで絵の表示には、絵を表示する場所も用意する必要があります。絵を表示する場所とは、PCゲームの場合はウインドウのことです。また、ウインドウを用意した上でOpenGLを利用できるように関連付けを行う必要もあります。これらをまとめてやってくれるのがGLFWというライブラリです。GLFWを使うことで、簡単にOpenGLの描画先となるウインドウを用意することができます。さらにGLFWは入力の受付やメインループを作成するための機能も持っています。OpenGLでゲームを作るためにうってつけのライブラリです。
効果音やBGMの再生もゲームには必須でしょう。音の再生も素のC++では行うことができないので外部のライブラリが必要になります。「センメツコースター」ではOpenALを選択しました。OpenALは名前からわかるようにOpenGLの音声版のような存在です。OpenALはOpenGLと同じように多くの環境で動作します。OpenGLとは違いコンパクト版(組み込み版)や大きなバージョン違いはありません。
OpenALは音の再生を行うことができるのですが、wavやoggのような音声ファイルを読み込むことはできません。そこで利用するのがALUREというライブラリです。ALUREはOpenALのユーティリティライブラリです。音声ファイルを読み込んでOpenALへ渡してくれます。
ゲームにはユーザーからの入力を受け付ける処理も必要です。これも素のC++ではできません*2。これには「絵の表示」のところでも出たGLFWが利用できます。
ゲームにセーブ機能をつける場合、ファイルの読み書き機能が必要になってきます。セーブ機能をつけなくても、テクスチャファイルなどの読み込みで必要です*3。これはC言語の標準関数、fopenとfwriteで対応可能です。fopenとfwriteはC言語の標準関数なので基本的にはどの環境でも動きます*4。
最後に必要なのは定期的にゲームの更新処理と描画処理を実行することです。これをメインループと呼びます。メインループは正しく一定の周期で回す必要があるのですが、それを行うにはモニタの垂直同期待ちを行うか高精度タイマー(時間計測)が必要です。どちらも素のC++では実現できないことですが、GLFWにはどちらも用意されています。
ここまででゲームを作るために必要最低限のものが揃いました。しかし、このままゲームを作り始めようとしてもいろいろ足りないことに気がつくでしょう。そんな足りないものの中でも特に必要になってくるであろうものを紹介します。これらは自分で実装してもいいし、他の人が作ったライブラリを使ってもいいでしょう。
描画を行うために必要なOpenGLですが、OpenGLのAPIは使いづらい上に低レベルなので直接使うのではなく扱いやすくしたラッパークラス、ラッパー関数経由で使うことになると思います。
OpenALも同様です。
ベクトルは座標データとして使いますし、当たり判定などでも使います。行列はOpenGLで頂点データを移動回転拡縮をさせるのに必要です。当たり判定はアクションゲームでは特に必要になってくる処理です。
これら数学系クラス、関数は自分で実装するよりすでに存在するライブラリを利用した方がいいかもしれません。なぜならその方が正確かつ高速な可能性が高いからです。「センメツコースター」では全部自分で実装しましたが…。
画像ファイルローダーとは画像ファイルを読み込んで解析しOpenGL用テクスチャデータとして変換する機能のことです。「センメツコースター」ではTGAファイルのローダーのみ作りました。TGAは簡単なフォーマットなので自作しましたが、pngなどの複雑なフォーマットの場合はライブラリを利用したほうがいいでしょう。
フォント描画システムは必須ではありませんが用意しておかないと、テキストを描画する箇所でいちいち画像データを用意する必要が出てきます。フォント描画システムの実装は、まともにやろうとするとかなり大変です。一番楽に必要最低限の実装をするとしたら、文字を0-9A-Zの範囲のみで固定文字幅のフォントテクスチャをペイントソフトなどで用意することでしょうか。固定文字幅でないテキスト描画や、日本語にも対応しようとするとかなり実装難易度が上がります。もしかしたらゲームエンジンを使わなかったことを一番後悔する場面がフォント描画システムを作るときかもしれません。テキストを描画したいだけなのになんでこんなに苦労しているんだろうと。
「センメツコースター」では、フォントテクスチャ生成ツールを作成し、そのツールから生成されたテクスチャデータと文字情報テーブル(jsonファイル)からテキストを描画するシステムを実装しました。
ゲームは画面内にいろいろな物が表示されます。そしてよく動きます。これを実現するための古典的な方法がタスクシステムです。別にタスクシステムである必要はないのですが、個々のオブジェクトが毎フレーム更新処理を呼び出される仕組みはゲームと相性がいいのでまず必要になってくると思います。
参考: 【C++ ゲームプログラミング】STLで実装する最小のタスクシステム - Flat Leon Works
「最低限必要なもの」と「現実的に必要になってくるもの」を紹介してきました。ここでは「センメツコースター」でさらに実装、導入したもの紹介します。
C++には文字列クラスとして標準でstd::stringがありますがUTF8に対応しておらず*5、日本語を含む文字列の場合1文字1文字を正確に扱うことができません。これが問題になるのは、例えばフォント描画システムで日本語を描画する場合などです。またstd::stringは機能も少ないといった不満もあったので、独自に文字列クラスを作成しました。
GUIシステムとは、ボタンやウインドウなどのGUIを実装するための仕組みです。具体的には以下のような機能群です。
このようなGUIシステムを用意することでゲーム中のUIの実装が楽になります。またデバッグ機能やツールを作る場合にも利用できます。
Jsonとは汎用データフォーマットです。Jsonを使うことで、構造化されたデータをファイルに書き出し/ファイルから読み出しすることができるようになります。ゲームでは、設定ファイルやセーブデータのフォーマットとして利用できます。Json以外の選択肢としてはxml、ini、バイナリなどがありますが、Jsonが一番扱いやすいと思います。Jsonをバイナリ化し高速化したMessagePackというものもあります。
Jsonを利用するためにはJsonフォーマットの読み取りと書き出し処理が必要ですが、「センメツコースター」ではPicoJsonというライブラリを利用しました。
Luaは軽量スクリプト言語です。単体で使うのではなくプログラムに組み込んで使うことに特化しています。Luaは柔軟なのでC++では大量にコーディングする必要があることを数行で書くことができるようになります。「センメツコースター」ではLuaをゲーム全体の制御と敵生成処理ルーチン、各種イベント、チュートリアルの実装に使っています。特にゲーム全体の制御にLuaを使うことの効果は抜群で、今はLuaを使わずにゲームを作ることは考えられないくらいです(個人の感想です)。
データ作成ツールというのは、例えばドット絵エディタやマップエディタのことです。「センメツコースター」ではドット絵エディタとしてAseprite、ドット絵のアニメーションデータエディタとしてDarkFunctionEditor、マップエディタとしてTiledMapEditorを利用し、それぞれのファイルローダーを実装しました。*6
以上が「センメツコースター」を作るためにやったことです。もちろん実際にはもっとたくさんのことをやったのですが、C++とOpenGLでゲームを作るという意味ではだいたいカバーできていると思います。ただ、さらっと書いてますがいろいろな場面で苦労は多いです。例えばOpenGLで板ポリを出すだけでも相当つまづくポイントが多いです。ですが、この記事で紹介しているようなことを実装できた時点でもう自分だけのゲームエンジンが出来ているようなものです。あとは自分の好きなようにゲームエンジンを強化してくだけです。楽しい!でも、ゲームエンジンばかり作り込んでしまいゲームが完成しないということには注意しましょう。おすすめなのは実際にゲームを作りながら必要になった機能だけゲームエンジンに追加していくことです。
先日「センメツコースター」の開発が完了しました。そこで開発に使ったものを紹介したいと思います。
QtCreator : Mac, Windows
Visual Studio Code : Mac, Windows
C++のコードはIDEでコーディングしていましたが、Luaやその他のテキストはVisual Studio Codeを使っていました。最初はSublime Text2を使っていたのですが、次にAtomを使い始め、最終的にVisual Studio Codeにたどり着きました。
Aseprite(有料) : Mac, Windows
ドット絵作成にはAseprite(v1.1.3)を使いました。Asepriteは有料ですが、使い勝手がよくアニメーションの作成も可能です。Asepriteの良いところはゲーム開発での利用に特化した機能が備わっていることです。具体的には以下のような機能です。
パック化は例えばこんな感じでびっちりと敷き詰めてくれます。
DarkFunctionEditor(Java製) : Mac, Windows
DarkFunctionEditorは、スプライトのアニメーションデータを作成するためのツールです。複数のスプライトを組み合わせてセルを作り、それを並べてアニメーションを作ります。Asepriteにもアニメーション機能はありますが、このツールのようにスプライトを組み合わせてアニメーションを作るということができません。DarkFunctionEditorはタイムラインや各種補間と言った機能がありませんが、2Dのドット絵なゲームなら十分な機能を持っていると思います。ただし、DarkFunctionEditorをゲーム開発に使うにはちょっと手間が必要です。まず、DarkFunctionEditorが読み取るスプライトシートリスト(xml)を作らなくてはいけません。これはAsepriteのスプライト情報の吐き出し機能を使って、自動生成するPythonスクリプトを書きました。次に、DarkFunctionEditorで作成したアニメーションデータ(xml)を読み込んで表示されるプログラムを実装する必要があります。つまりDarkFunctionEditorのローダーを実装する必要があるわけです。
作業フローとしてはこんな感じでやってました
スプライトアニメーションツールは他にも探したのですが、高機能過ぎたり(高機能すぎるとゲーム側でローダーを実装するのが大変になる)、値段が高かったりで、今回のプロジェクトに一番合っていたのがDarkFunctionEditorでした。
Sketch(有料) : Mac
Sketchはベクターグラフィックソフトです。グラフィックソフトですが、「センメツコースター」では主に企画書として使いました。テキストエディタと違ってテキストを自由な位置に配置できる上に色や大きさを変えることもできる、枠で囲ったり画像を張り付けたりもできる、1つのファイル内でいくつもページ分けができるなど企画書やアイデア出しに便利でした。他にもiOSアプリアイコンを作成したり*1、このサイトでも利用している「センメツコースター」のバナー画像もSketchで作成しました。99ドルと少し高いですがその価値はあります。ただし、販売モデルが変わってしまい、買い切りではなく1年単位のサブスクリプションとなってしまいました。買い切りモデル時に買った人はアップデートを行わなければそのまま使えますが、アップデートを行ってしまうと即ライセンス切れとなってしまい新たにライセンスを買わないと使えなくなってしまいます。ご注意を*2。
GarageBand : Mac
「センメツコースター」ではBGMはPANICPUMPKINさんのフリー素材を使用させていただきましたが、ゲーム成功時と失敗時にジングル(短い音楽)だけどうしてもマッチするものが見つからず、ためしにGarageBandで作ってみたらそれっぽいのができたのでそのまま採用しました。
また、GarageBandでファミコン風サウンドを利用するためにMagical 8bit Plugを利用しました。
bitfontmaker2 : Webサービス
最初はゲーム中のUI用のテキストをAsepriteで描いて使用箇所ごとにテクスチャ化していました。ところがだんだん面倒になってきたので、フォントを自分で作れないか調べていたところ、このWebサービスを発見しました。ブラウザ上でぽちぽちとドットを打って自分だけのフォントを作ることができます。ちなみに私が作ったフォントはここからダウンロードできます。収録されているのは英大文字小文字数字といくつかの記号だけです。一応英小文字も描きましたがサイズの都合上、見た目が悪いです。
Source Tree : Mac, Windows
ソフトウェア開発には必須なバージョン管理。ソースコードだけでなくリソース(アセット)含めて全部Gitでバージョン管理してました。クライアントはSource Treeを利用。
Dropbox : Mac, Windows
Time Machine : Mac標準搭載(ただし外付けHDDなどが必要)
Dropboxはバックアップのためのツールではありませんが、実質的にバックアップにもなります。ただし、Dropboxが対象としているフォルダのみです。Time MachineはMac標準搭載のバックアップソフトウェアです。システム全体の復旧だけでなく、ファイルを1つだけ選んで復旧なんてこともできます。
Clipy : Mac
クリップボード履歴を扱えるクリップボード拡張はコーディングでほぼ必須とも言えるものです。昔はClipMenuを使っていたのですが、頻繁にハングするようになってしまったのでClipyに乗り換えました。
動画キャプチャはゲーム開発の進捗動画や、AppStore用の動画を撮るときに必要になってきます。幸いなことにMacデスクトップの動画キャプチャもiPhone実機の動画キャプチャも無料のQuickTimePlayerで行うことができます。ただしデスクトップの動画キャプチャは標準では音声を撮ることができないので注意が必要です。
iMovie : Mac
AppStore用の動画は最大30秒までしか許可されていないので、動画を編集する作業が発生します。今回、動画編集ツールとしてはiMovieを利用しました。iMovieは無料な上になんとAppStore用のテンプレートもあります。iMovieは今回初めて使いましたが、そんなに苦労せずに動画を編集することができました。
Sip(有料) : Mac
Sipはカラーピッカーです。デスクトップ上の色を見たり、RGB値としてコピーできたりします。カラーピッカーはMacに標準で用意されているものがありますが、色をコピーするのにキーボードショートカットが必要だったりフォーマットを指定できないなど不満があります。Sipは簡単に色をコピーできたり、フォーマットを指定することができます。カラーピッカーを使うことで、ペイントソフトで画面デザインを試行錯誤したあとにそのままカラーピッカーで色をコピーしてソースコードにペーストということができるようになります。ソースコード内で直接色を指定する場面が結構あったので重宝しました。
XnConvert : Mac, Windows
XnConvertは強力な画像コンバータです。複数のファイルをまとめて、しかも複数の処理を行うことができます。リサイズ、クリッピング、回転、フォーマット変換、やりたいことだいたい用意されています。補間なしでリサイズを行うこともできるのが2Dゲーム開発的にポイント高いです。
InstrumentsはXcodeに付属するツールでプログラムのパフォーマンス調査ツールです。またメモリリークの調査も行うことができます。ソースコードではなく実際に動いているプログラム(プロセス)にアタッチして使います。Xcodeから起動しなくても単体で起動しますし、Xcodeでビルドしたプログラムでなくてもアタッチすることができます(ただし、デバッグモードでビルドする必要がある…気がします)。「センメツコースター」はPC(Mac)上ではQtCreatorでビルドして動作確認をしていましたが、このQtCreatorでビルドしたプログラムでもアタッチして調査することができました。Instrumentsの仕組みが良くわかっていないので予想でしかないのですが、QtCreatorでのビルドに使っていたコンパイラがXcode付属のClangコンパイラだったからかなと思っています。ちなみにInstrumentsはiOS実機のアプリの調査も行うことができます。
Python : Mac, Windows
AsepriteやDarkFunctionEditorで作成したデータをコンバートするのに、Pythonを利用しました。その他いろいろちょっとしたコマンドラインツールとして利用しています。
rumps : Mac
rumpsはMacのデスクトップ上部のメニューバーにメニューを追加できるようにするPythonのライブラリです。Pythonライブラリなので、メニューの追加もPythonコードで書くことになります。「センメツコースター」の開発では、各Asepriteファイルのコンバートや各ステージの直接起動、ステージエディタの起動のためのランチャーとして使っていました。この仕組みを用意することでたいぶ開発効率が上がったと思います。
Vivaldi : Mac, Windows
プログラミングをしていると、ネットで検索する必要が必ず出てきます。しかも必要な情報はすぐに得られるとは限らないので大量のタブを開くことになります。前はブラウザとしてFirefoxを使っていましたが、タブを閉じる動作があまりにも遅かったのでVivaldiに乗り換えました。Vivaldiの良いところはタブの動作が高速なことはもちろん、標準でタブの縦並びに対応していることです。さきほど言ったようにプログラミングをしていると調べ物で大量のタブを開くのでタブは縦に並べたいのです。VivaldiはChromeと同じ内部エンジンを使っているためChrome拡張が使えたりします*3。最近まで日本語入力バグがありましたが、それも解消され最強のブラウザになりました(個人の感想です)。ただしメモリは大量に喰います。
Trello : Webサービス
Trelloはタスク(Todo)管理のためのツール(Webサービス)です。ブラウザ上で動いているとは思えないくらい使いやすいツールです。タスク(カード)をリストに分けたりタグをつけた、詳細説明をmarkdownで書けたりと機能も申し分ないです。さらに複数のボードを利用できるのもポイントが高いです。あまりタスク数が多いと破綻してしまいそうですが、個人〜少人数で使う分には問題なさそうです。Trelloは「センメツコースター」の開発の終盤から使い始めたのですが、もっと早く知りたかったです。
使用ライブラリに関しては詳しくはこちらの記事で。
ゲームエンジンを使わずにC++とOpenGLでゲームを作った話 - Flat Leon Works
上でフォントを自作したと書きましたがあれは英数字だけなので、ひらがなだけでなく漢字まで含まれているPixelMplusはとても助かりました。音楽はファミコン風の音楽素材を公開してくださっているPANICPUMPKINさんのものを利用しました。やっぱりドット絵にはピコピコなサウンドが良いのです。
今回紹介した中でとくに良かったのがAsepriteです。Asepriteのスプライト情報出力機能とパック機能のおかげでだいぶ楽できたかなと思います。ところで、「センメツコースター」の開発では今回紹介したツールだけでなく、自分で作ったツールもいくつかありました。その紹介はまた今度ということで。
数年前、「Flan」というプログラミング言語を作っていました。このプログラミング言語は長い間C++でプログラミングをしてきて感じた不満をもとに、自分好みの最高のプログラミング言語を作ろうと、そういう考えで作っていました。「Flan」は言語機能的にはだいたい完成していたのですが訳*1あって開発は中断していました。
そして中断から数年経ったわけですが、このまま埋もれさせておくのももったいないなと思い、紹介だけでもすることにしました。公開予定は今のところありません。
百聞は…ということでまずはソースコード例を紹介。機能を詰め込んだサンプルになっているのでちょっとわかりずらいかもしれません。 また、Pythonコードとして無理やり構文カラーを適用してるので一部変な配色になっています。
このサンプルコードが実際に動作するくらいにはプログラミング言語Flanの開発は進んでました。
## ここはコメント#-ここもコメント -##- ここもコメント -### 変数 [int]: a <- 1## 変数定義と初期化 [int]: b <- 2print:( a + b ) ## 3print:( a - b ) ## -1print:( a * b ) ## 2 print:( a / b ) ## 0 b = 5## 代入## for文, if文 [list<string>]: strList strList.Append:( 'aaa' ) ## メンバ関数呼び出し strList.Append:( 'bbb' ) strList.Append:( 'ccc' ) for i in strList if i == 'aaa'print:( 'A' ) elif i == 'bbb'print:( 'B' ) elseprint:( 'X' ) end end ## while文,switch文 [int]: i while i <= 3 switch( i ) case 0: assert:( i == 0 ) case 1: assert:( i == 1 ) case 2: assert:( i == 2 ) case 3: assert:( i == 3 ) end i += 1 end ## 関数deffunc:[string]( [int]a, [int]b, [int]c ) [string]a + b + c ## キャストと暗黙のreturn end assert:( func:(123, 456, 789) == '123456789' ) ## クラスclassBasedefdump:() ## メンバ関数print:( 'Base.dump' ) end end classA : [Base] ## 継承 classAInner## 内部クラス end [float]: m_Value ## メンバ変数def@init:( [float]value = 0.0#-デフォルト引数-# ) ## コンストラクタ m_Value <- value end def@del:() ## デストラクタ [stringliteral]: infoStr <- [stringliteral]@typeinfo:() ## 型情報取得print:( '[' + infoStr + '].@del:()' ) end defdump:() |override| ## オーバーライドprint:( m_Value ) if false print:( $this.m_Value ) ## this end end defs_func:[int]() |static| ## staticメンバ関数 0 end deffunc:[int]([int]a,[int],[int]c) ## 引数名の省略(第二引数に注目)print:( 'func: ' + a + $arg1 + c ) ## 引数名を省略した引数へのアクセス [self].s_func:() ## [self]で自身の型 end def@unittest:() ## 単体テスト end end print:( 'staticメンバ関数呼び出し:' + [A].s_func:() ) [A]: a_ins <- 1.23## インスタンス作成 a_ins.dump:() ## 1.230000 [owner:A]: owner_a <- [A].@new:( 4.56 ) ## newでインスタンス作成 owner_a.dump:() ## 4.560000## テンプレート関数deftemplateFunc<T>:([T]param) assert:( param == 10 ) end templateFunc<int>:( 10 ) ## テンプレートクラスclasstestTemplate<T> [T]: m_ValueT end [testTemplate<int>]: tInt assert:( tInt.m_ValueT == 0 ) ## トレイト trait named_trait [string]: name <- 'no name'defgetName:[string]() name end defsetName:([string]) name = $arg0 end end classCharactor has named_trait end [Charactor]: charactor charactor.setName:( 'aaa' ) ## 関数オブジェクトdefgetFuncObj:[func0_obj<int>]() [int]: outerVariable <- 11returndef:[int]() outerVariable += 1; outerVariable end ## 関数オブジェクトを返す end [auto]: funcObj <- getFuncObj:() ## auto型assert:( funcObj:() == 12 ) ## 関数オブジェクトは外部変数をキャプチャしているのでassert:( funcObj:() == 13 ) ## 呼び出すたびに戻り値が変わる## ファイバーclassHoge [int]: value <- 10 end [Hoge]: hoge [fiber]: fiber <- def:() hoge.value += 1yield## ファイバー中断 hoge.value += 1yield hoge.value += 1 end assert:( hoge.value == 10 ) fiber.resume:() ## ファイバー実行assert:( hoge.value == 11 ) fiber.resume:() assert:( hoge.value == 12 ) fiber.resume:() ## ファイバー実行(そして中断されずに終了)assert:( hoge.value == 13 ) fiber.resume:() ## 終了済みファイバーを実行してもassert:( hoge.value == 13 ) ## ファイバーは終了しているので、もう値は変化しない
ここで紹介する以外にも言語機能はたくさんあるのですが、とりあえず大きめなやつ、または個性的なだけ紹介します。
FlanはFlanソースコードをバイトコードへ変換し、それをFlanVM(Flanの仮想マシン)が実行することで動作します。Luaと同じ仕組みです。ただし将来的にはC++ソースコード生成による実行も考えています。
FlanはLuaとは違い静的型付けです。つまり、全ての変数には型があり、型が一致しないと(変換不可だと)コンパイル時点でエラーが発生します。
FlanはC++から影響を受けたオブジェクト指向なプログラミング言語です。C++に存在する、継承やオーバーライドなどの機能はたいてい言語仕様として含まれています。ただし、多重継承はできません。そのかわりにトレイトがあります。
FlanはC++とは違い、文末にセミコロンは不要です。セミコロンを使って1行に複数の文を記述することも可能です。
FlanはC++とは違い、コメントアウトを入れ子にすることができます。これはLuaの影響を受けています。
## ここはコメント#-ここもコメント -##-入れ子 #--なコメントアウトも --#可能 -##- その場コメント -##- -10 -##!-ここはコメントではない -#
FlanはC++とは違い、前方宣言が不要です。なぜ不要かというと、構文解析(パース)を2パスで行っているからです*2。
Flanではreturnを明示的に記述しなくても、関数内で最後の式文が自動的にreturn文になります。
deffunc:[int]([int]a,[int]b) a+b ## 最後の文が自動でreturnされる end
Flanでは変数の型としてauto型を使うことができます。これはC++11で導入されたauto型と同じようなもので、初期化の式の型から自動で変数の型を決めることができる機能です。
[auto]: a <- 10 [auto]: b <- 'string'
C++では初期化も代入もどちらも=
で行いますが、Flanでは初期化は<-
、代入は=
で行います。初期化と代入は別の操作なのですから、別の演算子にするべきだと思ったのでこういう仕様にしました。
また、C++では代入は式なのでif文の条件式内で使えたりしましたが、Flanでの代入は文なのでそういうことはできません。
[int]: a <- 10## 初期化 a = 20## 代入
C言語ではこんな書き方ができます。
(int)hoge((1+2)*3);
これをFlanで書くとこうなります。
[int]hoge:((1+2)*3)
C言語では、()
の意味がいくつもあります。上の例では、キャストと関数呼び出しと式の優先順序変更がすべて()
で行われています。Flanではキャストは[]
、関数呼び出しは:()
、式の優先順序変更は()
とすべて区別されています。
なお、Flanでは[]
で囲われた中はすべて型を表します。
<=>
変数の中身の交換というのは普遍的な操作なので演算子として存在してもいいのではと思い、交換演算子というものを仕様に入れました。
[int]: a <- 10 [int]: b <- 20 a <=> b assert:( a == 20 ) assert:( b == 10 )
Flanには3種類の参照が存在します。1つは所有。これはC++のunique_ptrのようなもので、所有者が1人であることを保証します。この参照が破棄されるとき、参照先も破棄されます。2つめは共有。これはC++のshared_ptrのようなもので、複数の所有者が存在できることを表します。この参照が破棄されるとき、他に所有者がいない場合は破棄されます。3つめは弱参照です。これはC++のweak_ptrのようなもので、所有ではなく"参照"を表します。参照先が破棄されると弱参照はnullとなります。
ソースコードとしては、所有は[owner:Hoge]
、共有は[ref:Hoge]
、弱参照は[wref:Hoge]
という記述方法になります。
JavaやPythonやLuaなど多くのプログラミング言語ではプリミティブ型以外のすべてが参照なことが多いですが、Flanは値型と参照型が個別に存在します。
[A]: a ## 値型 [ref:A]: ref_a ## 参照型
Flanの参照型への操作は、基本的にデリファレンスしてから行われます。つまりC言語でいうところの常に*ptr
が行われるということです。例えば参照型変数a
と参照型変数b
があったとして、a = b
とした場合、a
にb
への参照がコピーされるのではなく、a
の参照先にb
の参照先が代入されます。C言語で表すと*a = *b
です。
では参照をコピーしたい場合にはどうしたらいいのか。それを行えるようにするのが参照系演算子です。例えば参照をコピーしたい場合はa := b
とします。=
ではなく:=
を使うのです。
参照系演算子は他にも参照を交換するための:<=>
、参照を比較するための:==
などがあります。どれも通常の演算子の前に:
が付いているのが特徴です。
classA [int]: m_Value def@init:( [int]value ) m_Value <- value end end [ref:A]: a0 <- [A].@new:( 0 ) [ref:A]: a1 <- [A].@new:( 1 ) a0 = a1 ## 参照先の代入 a1.m_Value = 2assert:( a0.m_Value == 1 ) assert:( a1.m_Value == 2 ) a0 := a1 ## 参照の代入 a0.m_Value = 3assert:( a0.m_Value == 3 ) assert:( a1.m_Value == 3 )
Flanには通常の関数とは別に、値として扱える関数オブジェクトが存在します。この関数オブジェクトは他の言語では無名関数やクロージャとも呼ばれます。Flanの関数オブジェクトはクロージャでもあるので、その関数が定義された環境を保持(キャプチャ)します。
[int]: value <- 11 [auto]: funcObj <- def:[int]() value += 1; value end ## 関数オブジェクトassert:( funcObj:() == 12 ) assert:( funcObj:() == 13 ) assert:( value == 11 ) ## キャプチャはコピーなのでコピー元には影響がない(値型の場合)
Flanでは関数の引数名を省略することができます。省略された引数名は$argN
のような名前が自動で設定されます。このときN
には引数の位置が入ります。
deffunc:[int]([int],[int],[int]) $arg0 + $arg1 + $arg2 end
Flanでは関数内で$args
を使うことで引数リストを展開することができます。これは受け取った引数をそのまま他の関数に渡す場合などに便利です。
deffunc:[int]([int],[int],[int]) func2:( $args ) ## 引数リストを展開 end deffunc2:[int]([int],[int],[int]) $arg0 + $arg1 + $arg2 end
C++ではstatic/非staticで同じ名前のメンバ関数を作ることができませんでしたが、Flanでは可能です。
classAdeffunc:[stringliteral]() 'func:()' end deffunc:[stringliteral]() |static| 'func:() |static|' end end [A]: a assert:( a.func:() == 'func:()' ) assert:( [A].func:() == 'func:() |static|' )
C++のように関数テンプレートが存在します。
deftemplateFunc<T>:([T]param) assert:( param == 10 ) end templateFunc<int>:( 10 )
これもC++にある機能です。関数への引数から、関数テンプレートのテンプレート引数を推論する機能です。
deftemplateFunc<T,T2=bool,T3>:([T]a,[T3]t3) assert:([T].@typeinfo:() == [int].@typeinfo:() ) assert:([T2].@typeinfo:() == [bool].@typeinfo:() ) assert:([T3].@typeinfo:() == [stringliteral].@typeinfo:() ) end ## 本来はこう書く必要があるところを templateFunc<int,bool,stringliteral>:( 1, '' ) ## このようにテンプレート引数を省略して書くことが可能 templateFunc:( 1, '' )
流用テンプレート関数とは、引数リストを他の関数から流用するテンプレート関数です。オーバーロードされた関数群のラッパー関数を作るのに便利です。
defhoge:[stringliteral]([int]a) 'hoge:[stringliteral]([int])' end defhoge:[stringliteral]([bool]a) 'hoge:[stringliteral]([bool])' end defhoge:[stringliteral]([stringliteral]a) 'hoge:[stringliteral]([stringliteral])' end defhoge_wrapper:[stringliteral](<hoge>) ## <hoge>という記述で流用テンプレートになるprint:( 'Pre Hoge' ) hoge:($args) print:( 'Post Hoge' ) end hoge_wrapper:( 3 ) ## int型を1つ引数にとるhogeが存在するのでOK hoge_wrapper:( 5, 6 ) ## int型を2つ引数にとるhogeは存在しないのでエラーになる
クラスの内部でクラスを定義することができます。また、関数内部で関数やクラスを定義することも出来ます。
classAclassAInner## 内部クラス end end deffunc:() defInnerFunc:() ## 関数内部関数 end classInnerClass## 関数内部クラス end end
C++のようにクラステンプレートが存在します。
classtestTemplate<T> [T]: m_ValueT end [testTemplate<int>]: tInt assert:( tInt.m_ValueT == 0 )
トレイトとはクラスに機能を持たせるための仕組みです。Wikipediaに記事がありますが、言語機能としてトレイトを持つプログラミング言語でも、その意味は微妙に違っているようです。
乱暴に説明すると、実装を持つインターフェイス(Java)です。
trait named_trait ## 名前トレイト(機能) [string]: name <- 'no name'defgetName:[string]() name end defsetName:([string]) name = $arg0 end end classCharactor has named_trait ## キャラクターは名前トレイト(機能)を持つ end [Charactor]: charactor charactor.setName:( 'aaa' ) ## 名前トレイト(機能)のメンバ関数を使える
トレイトもクラスのようにテンプレートが存在します。
ファイバーは中断できる関数オブジェクトのようなものです。Luaにおけるコルーチンとほぼ同じものですが、Fiberの方が文字数が短いのと響きがよいのでFlanではFiberという名称にしました。
classHoge [int]: value <- 10 end [Hoge]: hoge [fiber]: fiber <- def:() hoge.value += 1yield## ファイバー中断 hoge.value += 1yield hoge.value += 1 end assert:( hoge.value == 10 ) fiber.resume:() ## ファイバー実行assert:( hoge.value == 11 ) fiber.resume:() assert:( hoge.value == 12 ) fiber.resume:() ## ファイバー実行(そして中断されずに終了)assert:( hoge.value == 13 ) fiber.resume:() ## 終了済みファイバーを実行してもassert:( hoge.value == 13 ) ## ファイバーは終了しているので、もう値は変化しない
クラスに@unittest
というメンバ関数を定義すると、単体テスト用の関数になります(扱いとしてはstaticメンバ関数)。コンパイル時に単体テストフラグが立っていた場合、実行時にすべての@unittest
関数が呼び出されます。
classA [int]: m_Value <- 10def@unittest:() [A]: a assert:( a.m_Value == 10 ) end end classB [int]: m_Value def@unittest:() [B]: b assert:( b.m_Value == 0 ) end end ## [A].@unittest:() と [B].@unittest:()が自動で呼び出される
実はプログラミング言語と同時にIDE(統合開発環境)も作っていました。このIDEはQtを使って作りました。下の画像を見ればだいたいわかると思いますが、機能としては以下のようなものを実装しました。一部、実行形式がC++コード生成だったときの名残もあります。
VMデバッガーは最初は実装していなかったのですが、プリントデバッグでVMの動作をデバッグするのがとても辛かったので作りました。世のVM開発者の方々はどうやってデバッグをしているのでしょうか…。
最後にプログラミング言語Flanをどうやって実装したのかを紹介したいと思います。思い出しながら書いているので間違っている部分があるかもしれません…。また、「言語モデル」*3などFlan独特の名称を使ったりしてます。
Flanのソースコードから実行までの流れは以下のようになっています。
パーサーはソースコードを受け取り、抽象構文木(AST)を生成します。このパーサーは、パーサージェネレータであるANTLRを使って生成しました。ANTLRはデフォルトではJavaソースコードを生成しますが、C言語コードを生成させることもできます。ちなみに、C言語用のパーサージェネレータは一般的にはlex/yaccが使われるようです。ただし、yaccには抽象構文木の生成機能はありません。
モデルファクトリはパーサーが生成した抽象構文木から、プログラミング言語Flanの言語モデルを構築します。言語モデルとは簡単に言えば、抽象構文木から意味を読み取って新たに構築したデータ構造です。現在の実装ではこの言語モデルはバイトコード生成時だけでなくVM実行時にも必要になります。
バイトコードジェネレータは言語モデルを元に、バイトコードを生成します。
FlanVMはバイトコードを実行するための仮想マシンです。FlanVMにはバイトコードと言語モデルを与える必要があります。このFlanVMによってバイトコードが実行されることでようやくFlanが実行されたことになります。
前の節でFlanがどうやってプログラムを実行しているのかを紹介しました。実行の仕組みだけならこれだけで良いのですが、実際にプログラミング言語を作るとなるとより多くの作業が必要になります。その辺を含めたプログラミング言語を作るためのステップを紹介します。
なお、ここでの説明はFlanの場合のもので、必ずしもこの方法が必要というわけではありません。例えばパーサージェネレータを使わずに自分でパーサーを書くこともできます。
プログラミング言語を作るにはまず文法を決める必要があります。さらに文法を決める前にプログラミング言語にどんな機能を持たせる決めなくてはいけません。ここは楽しい場面ですが、機能を追加すればするほどそれを文法に落とし込むのに苦労することになります。文法は最初にすべてを決めるのではなく少しずつ付け足していくことも可能ですが、新しい文法を導入するとすでに決まっていた文法を修正する必要が出てくる場合があります。実装はあとにしても文法だけは最初から考えておいたほうがいいかもしれません。
文法が決まったらそれを厳密に定義します。文法を厳密に定義することはソースコードの構造を決めることでもあります。パーサージェネレータは文法の厳密な定義を必要とします。いきなり、パーサージェネレータ用の文法定義を書いてもいいし、BNF記法で一旦書いてからそれをパーサージェネレータ用の文法定義に落とし込んでもいいでしょう。ANTLRの文法定義方法はBNF記法に近いのでいきなりANTLR用の文法定義を書き始めてもあまり困ることはないです。
パーサージェネレータ用の文法定義ができたらパーサーを生成してもらいます。ANTLRは抽象構文木を生成してくれるのでよいのですが、yaccでは自分で抽象構文木を構築するコードを書かないといけないかもしれません。
言語モデルとはソースコードのデータ構造です。このデータ構造を構築するためのクラス群が必要になってきます。例えばクラス、関数、変数、式、文などを表すクラスです。変数クラス、式クラス、クラスクラスなどを作っていくのはなかなか楽しいかもしれません。言語モデルクラスには各種エラー処理の実装も必要になります。型が一致しない、変数、関数が見つからないなどです。
言語モデルクラスができたら、それらを使って言語モデルを構築する言語モデルファクトリを実装します。言語モデルファクトリはパーサーが出力した抽象構文木を走査して言語モデルを構築していきます。
言語モデルの構築までできるようになったら、あとはそれを実行する仕組みを作るだけです。実行するための仕組みとしてFlanではVMを利用しました。VMとは仮想マシンのことでソフトウェアで実装されたCPUのようなものです。CPUは機械語を読み取って動作しますが、VMはバイトコードを読み取って動作します。
VMを実装するには以下のようなことをします。
長くなってしまうので詳細は省きます。「バイトコード」「スタックマシン」当たりで検索してみてください…。
VMが出来たので、そのVMが利用するバイトコードを生成するバイトコードジェネレータを作ります。バイトコードジェネレータは言語モデルを走査してバイトコードを生成していきます。
ここまでで「パーサー」、「言語モデルファクトリ」、「バイトコードジェネレータ」が出来ました。これら順番に使うことでソースコードからバイトコードを生成することができます。ただ、このままでは不便なのでこれらの機能をまとめた「コンパイラ」を作りましょう。「コンパイラ」はソースコードを受け取り、「パーサー」、「言語モデルファクトリ」、「バイトコードジェネレータ」を順に使い、バイトコードの生成します。
コンパイラとVMが出来たのであとは、ソースコードを受け取り、コンパイルし、VMにバイトコード(と言語モデル)を渡して実行する仕組みを用意するだけです。
プログラミング言語を作るというのは、本当に楽しくて気付いたら1年くらい経過してました。Flanは現在、開発を中断していますがここまで作ったんだからいつか完成まで持っていきたいと思う…ような思わないような。*4
通常、ブラウザ上から任意のプログラムを起動することはできないようになっています。これはおそらく安全性のためなので、ブラウザ上から任意のプログラムを起動することは不可能だと諦めていたのですが、最近ついにその方法を発見したので紹介したいと思います。
注意
発見した方法というのはURLスキームを使った方法です。URLスキームというのはURLリンクからアプリケーションを起動する仕組みで、iOSでは多くのアプリがURLスキームに対応しています。URLスキームといえばiOSと思っていたのですが、Macにもあるのではと思い調べてみたところ今回の方法が見つかったわけです。
URLスキーム機能だけでは、任意のプログラムの起動はできないのですが、URLスキーム機能を応用することで任意のプログラムを起動できるようになります。具体的には、URLスキームのパラメータをコマンドとして実行するプログラムを用意すればいいのです。これにより、そのプログラム経由で任意のプログラムが起動できるようになります。
手順としては以下のようになります。
URLスキームに対応したプログラムを作成するには、Macのアプリケーションとして作成する必要があります。大変そうですが、AppleScriptを使えば簡単にMac用アプリケーションを作成することができます。
手順
まずプログラム名を決めます。URLスキームとしての名前にもなることを考えて決めましょう。この記事では「URLSchemeExec」という名前にします。URLスキームとしては、「URLSchemeExec://」または「urlschemeexec://」という感じで利用することになります。
スクリプトエディタを起動し「新規書類」を選ぶと、こんな画面が出るはずです。
以下のコードを入力します。"URLSchemeExec://"
の部分は、先程決めたプログラム名に書き換えてください。
-- URLスキーム経由で起動したときに呼ばれる関数onopen location url_scheme -- URLスキームの先頭部分を除去する("URLSchemeExec://"の部分は、先程決めたプログラム名にする)set AppleScript'stextitemdelimitersto {"URLSchemeExec://"} set txt_items totextitemsof url_scheme set AppleScript'stextitemdelimitersto {""} set scheme_txt to txt_items as Unicode text-- URLエンコード(%エンコード)を元に戻すset scheme_txt2 to urlDecode(scheme_txt) -- Terminalを起動するtellapplication"Terminal"activate-- コマンドをTerminal上で実行するdo script scheme_txt2 endtellendopen location on urlDecode(str) set temp to"php -r 'echo rawurldecode("& quote & str & quote &");'"return (do shell script temp) asstringend urlDecode
「ファイル ->書き出す」で書き出してください。このときファイルフォーマットの指定に「アプリケーション」を選んでください。
これで、「URLスキーム経由でコマンド文字列を受け取り、そのコマンドを実行するプログラム」が用意できました。
先程作ったアプリケーションは、URLスキーム呼び出し時の処理は実装されていますが、実際にURLスキーム経由で起動できるようにはなっていません。これをできるようにするには、アプリケーション内のinfo.plist
を書き換える必要があります。info.plist
はアプリケーションバンドル内のURLSchemeExec.app/Contents/Info.plist
にあります。バンドル内に入るには「URLSchemeExec.app」のコンテキストメニューから「パッケージの内容を表示」を選びます。
以下のコードを追加します。「URLSchemeExec」の部分は最初に決めたプログラム名に置き換えてください。
<key>CFBundleURLTypes</key><array><dict><key>CFBundleURLName</key><string>com.apple.ScriptEditor.id.URLSchemeExec</string><key>CFBundleURLSchemes</key><array><string>URLSchemeExec</string></array></dict></array>
URLスキームをOS側に認識させるために、一度アプリケーションをダブルクリックで起動します。ただし起動しても、URLスキーム対応以外のコードを書いていないので何も起こらないはずです。
これでURLスキームから任意のプログラムを実行する準備ができました。
実際にURLスキームを試すには、ブラウザのアドレスバーに入力するのが手っ取り早いです。アドレスバーに以下のように入力してみて、正しく動作するかどうか確認してみましょう。
URLSchemeExec://echo aaa
Safariの場合、空白があるとURLスキームと認識されないようなので以下のようにする必要があります。
URLSchemeExec://echo%20aaa
ターミナルが起動して、「aaa」と出力されたら成功です。
上記のAppleScriptはターミナル経由でプログラムを起動していましたが、ターミナルを出さずにプログラムを起動させたい場合はAppleScriptコードを以下のようにします。別名のアプリケーションとして用意しておくよいかもしれません。
-- URLスキーム経由で起動したときに呼ばれる関数onopen location url_scheme -- URLスキームの先頭部分を除去する("URLSchemeExec://"の部分は、先程決めたプログラム名にする)set AppleScript'stextitemdelimitersto {"URLSchemeExec://"} set txt_items totextitemsof url_scheme set AppleScript'stextitemdelimitersto {""} set scheme_txt to txt_items as Unicode text-- URLエンコード(%エンコード)を元に戻すset scheme_txt2 to urlDecode(scheme_txt) -- プログラムを起動する do shell script scheme_txt2 endopen location on urlDecode(str) set temp to"php -r 'echo rawurldecode("& quote & str & quote &");'"return (do shell script temp) asstringend urlDecode
以上、URLスキームとAppleScriptの組み合わせでブラウザから任意のプログラムを起動できるようになるという話でした。ブラウザから任意のプログラムが起動できて何が嬉しいのかというと、HTMLがより便利なフォーマットになることです。例えば、ローカルHTMLを使ってプログラムランチャーあるいはファイルランチャーのようなものを作ることができるようになります。HTMLを使ってランチャーを作ると、自由なレイアウトでリンクを配置したり、画像を貼ったり、プログラムの説明を記述したりといったことが可能になります。また、ドキュメントを記述する際にファイルやフォルダの場所を記述ことはよくあると思いますが、HTMLを使ってドキュメントを作成すれば、openコマンドを使ってドキュメント上から直接ファイルやフォルダを開けるようになります。
今回紹介した方法はMacでしかできない方法ですが、URLスキーム自体はWindowsにもあるようなのでもしかしたら似たような方法でWindowsでも可能かもしれません。->ASCII.jp:WindowsでURLのプロトコルからアプリを起動する|Windows Info
Windowsにはダブルクリックでスクリプトを実行できるバッチファイルという便利な仕組みがあります。Macにはバッチファイルというものはありませんが、拡張子を.commandにしたシェルスクリプトを用意することでバッチファイルと同じようにダブルクリックでスクリプトを実行させることができます。ところで、WindowsでもMSYS等を導入することでシェルスクリプトを実行することができるようになります。ということは、MacとWindowsで共通のバッチファイルを利用することも可能なのではないでしょうか。ということで、いろいろ模索してみた結果、実際に実現することができたのでその方法を紹介したいと思います。
執筆時の環境
注意
記事のタイトルは「MacとWindowsで共通のバッチファイル(シェルスクリプト)を利用できるようにする」となっていますが、具体的には「MacとWindowsでシェルスクリプトをダブルクリックで起動できるようにする」という形になります。Macで「シェルスクリプトをダブルクリックで起動できるようにする」は比較的簡単なので、今回紹介する方法のメインは、Windowsで「シェルスクリプトをダブルクリックで起動できるようにする」の実現方法です。
Macでシェルスクリプトをダブルクリックで起動できるようにするには、シェルスクリプトを記述したファイルの拡張子を.commandにし、さらにファイルに実行権限を与える必要があります。やり方は以下の記事などが参考になります。
参考リンク
まず、Windowsでシェルスクリプトを実行する方法について考える必要があります。Windowsでシェルスクリプトを実行する方法はいろいろあると思いますが、今回利用するのはMsys2+MinGWです。
Msys2+MinGWの導入方法は以下のページなどを参考にしてください。基本的にはMsys2をインストールし、Msys2上からMinGWをインストールする流れだと思います。
参考リンク
Msys2+MinGWを導入することでWindowsでシェルスクリプトを実行できるようになるわけですが、シェルスクリプトの実行には以下のようなステップを踏む必要があります。
ダブルクリックでシェルスクリプトを実行できるようにするには、これらをまとめて実行できるようにしなければなりません。
Msys2のシェルの起動はMSYS2 MinGW 32-bit.lnk
のようなファイルで行うようになっているのですが、これはMsys2の起動用バッチの引数付きショートカットになっています。これを展開すると以下のようなコマンドになります。
C:\msys64\msys2_shell.cmd -mingw32
引数については-mingw32
の他に-mingw64
や-msys
なども指定可能なようですが、それぞれの違いがよくわからないのでとりあえず、-mingw32
としています。
Msys2の起動用バッチmsys2_shell.cmd
の中ではいろいろな処理のあとに最終的にbash.exeなどのシェルプログラムを起動しています。その際にmsys2_shell.cmd
に渡された引数はそのままシェルプログラムに渡されるようになっているようなので、以下のようにすることでシェルの起動と同時にシェルスクリプトファイルを渡すことができます。
C:\msys64\msys2_shell.cmd -mingw32 シェルスクリプトファイルパス
これでシェルスクリプトの起動が1つのコマンドでできるようになりました。なお、この方法で起動するとスクリプトの実行が終わったらすぐにシェルのウインドウが閉じてしまいます。出力結果などを確認したい場合は、シェルスクリプトの末尾にread
コマンドをおいてユーザーの入力を待つようにします。
シェルスクリプトをダブルクリックで起動できるようにするには、ファイルの関連付けを行います。今回はMacと共通のシェルスクリプトを利用できるようにするという目的なので、Macで利用している拡張子.command
で関連付けを行うことにします。
通常の関連付けの方法、つまりエクスプローラー上から「プログラムから開く->常にこのアプリを使う」という方法ではプログラムに対して引数を与えることができません。先程紹介したシェルの起動と同時にシェルスクリプトの実行を行うコマンドは、msys2_shell.cmd
に対して-mingw32
という引数を与えているので、エクスプローラーを使った関連付け方法は利用できません。
すでに.command
に対して関連付けを行ってしまっている場合は、それを解除してから以下の作業を行ってください。関連付けの解除については以下のサイトが参考になりました。
参考リンク
引数付きで関連付けを行うには、assoc
コマンドとftype
コマンドを使います。具体的には以下のような手順になります。
.command
にファイルタイプShellScriptFile
を割り当てる :assoc .command=ShellScriptFile
ShellScriptFile
の実行プログラムを設定する :ftype ShellScriptFile="C:\msys64\msys2_shell.cmd" -mingw32 "%1" %*
ShellScriptFile
という名前は適当につけたものなので、別のものでもかまいません。
参考リンク
これでWindowsで.command
ファイルに記述したシェルスクリプトをダブルクリックで実行できるようになりました!(初回はどのアプリで開くかのダイアログが出るかもしれません。その場合は先程設定したファイルタイプが選択肢にあるはずなのでそれを選びます)
ずっとC++03縛りを続けてきましたが、そろそろC++11に手を出してもいいだろうと思いC++11の勉強を始めてみました。C++11の情報はcpprefjpさんのページを参考にしています*1。
注意
autoを使用しない場面(予想)
auto hoge = 0;
参考リンク : auto - cpprefjp C++日本語リファレンス
decltype(1+2) hoge; // int hoge; となる
参考リンク : decltype - cpprefjp C++日本語リファレンス
std::for_each
+ ラムダ式を使うのがいいのかもしれないstd::vector<int> vec; for (constauto& value : vec) { }
参考リンク : 範囲for文 - cpprefjp C++日本語リファレンス
{a,b,c...}
というリストによるオブジェクト構築(コンストラクタ呼び出し)ができるようになる{a,b,c...}
という式はstd::initializer_list
型になる。利用には<initializer_list>
ヘッダーをインクルードする必要がある...
記法より扱いやすい可変個引数としても便利かもしれない参考リンク : 初期化子リスト - cpprefjp C++日本語リファレンス
参考リンク : 一様初期化 - cpprefjp C++日本語リファレンス
std::move
を利用した場合。もう1つは代入操作の最適化によりムーブが行われる場合。
参考リンク:
&
で宣言される。C++03時代から存在しており所謂「参照」のこと&&
で宣言される。わかりづらいことに、右辺値参照型の変数は左辺値である。しかし型としては右辺値参照であるstd::move
を使うことで、左辺値から右辺値参照を取り出すことができる。(この右辺値参照を使って代入などが行われた場合、ムーブ(所有権の移動)が発生する)参考リンク:
mutable
指定が必要参考リンク : mutable
参考リンク : noexcept - cpprefjp C++日本語リファレンス
参考リンク : constexpr - cpprefjp C++日本語リファレンス
参考リンク : nullptr - cpprefjp C++日本語リファレンス
参考リンク : インライン名前空間
_
で始まり、2文字目は小文字である必要がある(_
無しと_大文字
はC++標準規約で予約されているため)参考リンク : ユーザー定義リテラル - cpprefjp C++日本語リファレンス
= default
= delete
= default
または= delete
を使った場合、他のメンバ関数が暗黙に定義されなくなる(面倒だな…)参考リンク : 関数のdefault/delete宣言 - cpprefjp C++日本語リファレンス
参考リンク : 移譲コンストラクタ - cpprefjp C++日本語リファレンス
参考リンク : 非静的メンバ変数の初期化 - cpprefjp C++日本語リファレンス
参考リンク : 継承コンストラクタ - cpprefjp C++日本語リファレンス
参考リンク : overrideとfinal - cpprefjp C++日本語リファレンス
explicit
を付けることで暗黙的変換を禁止させることができるexplicit
機能がC++03の時点で存在したことを考えると、同じく型変換機能である型変換演算子にexplicit
機能が付くのは順当な進化だと思う参考リンク : 明示的な型変換演算子のオーバーロード - cpprefjp C++日本語リファレンス
参考リンク : friend宣言できる対象を拡張 - cpprefjp C++日本語リファレンス
&
を、右辺値用には&&
を付加する&
のみでオーバーロードはできず、&
と&&
はセットでオーバーロードさせる必要がある&
の意味がまた増えてしまったな…参考リンク : メンバ関数の左辺値/右辺値修飾 - cpprefjp C++日本語リファレンス
// C++03struct Color { enum e { Red , Green , Blue }; }; // C++11enumclass Color2 { Red , Green , Blue }; int main() { Color::e value = Color::Red; Color2 value2 = Color2::Red; return0; }
参考リンク : スコープを持つ列挙型 - cpprefjp C++日本語リファレンス
参考リンク : 共用体の制限解除 - cpprefjp C++日本語リファレンス
std::vector<std::vector<int>>
のようなテンプレートで>>
が連続する書き方はコンパイルエラーになっていたがC++11でそれが解消された参考リンク : テンプレートの右山カッコ - cpprefjp C++日本語リファレンス
参考リンク
using newType = oldType;
という書き方で型の別名を作れる機能参考リンク
typename
の替わりにtypename...
を使う(class...
でも可)テンプレート引数名...
でパラメータパックを展開できる
テンプレート引数名... 仮引数名
とすることで、パラメータパックを引数とする関数を定義できる(これを「関数パラメータパック」と呼ぶ)...仮引数名
とすることで関数パラメータパックを展開することができるp
を変換関数とすると、p(仮引数名)...
とすることでパラメータパックの拡張が可能t<仮引数名>...
というような、テンプレートのインスタンス化の形でもパラメータパックの拡張は可能sizeof...(テンプレート引数名)
で与えられたテンプレート引数の個数を取得できる参考リンク : 可変引数テンプレート - cpprefjp C++日本語リファレンス
参考リンク : ローカル型と無名型を、テンプレート引数として使用することを許可 - cpprefjp C++日本語リファレンス
thread_local 型名 変数名;
でスレッドローカルストレージとなる参考リンク : スレッドローカルストレージ - cpprefjp C++日本語リファレンス
参考リンク : ブロックスコープを持つstatic変数初期化のスレッドセーフ化 - cpprefjp C++日本語リファレンス
int hoge(void);
をauto hoge(void) -> int;
と記述できるようになった参考リンク : 戻り値の型を後置する関数宣言構文 - cpprefjp C++日本語リファレンス
参考リンク : コンパイル時アサート - cpprefjp C++日本語リファレンス
R"(文字列)"
という記法でraw文字列になるR"xxx(文字列)xxx"
というふうに()
の前後に任意の文字を与えて文字列の開始/終了マークとすることができる
()
や"
を文字列中に使うことができるようになるR"xxx(const char* temp = "(aiueo)";)xxx"
は文字列としてはconst char* temp = "(aiueo)";
になる参考リンク : 生文字列リテラル - cpprefjp C++日本語リファレンス
u8"文字列"
とすることでUTF-8エンコードされた文字列となる\uXXXX
と記述すると、コードポイントXXXXのユニコード文字として解釈される(\U
にすると8桁での記述が可能になる)参考リンク : UTF-8文字列リテラル - cpprefjp C++日本語リファレンス
参考リンク : 属性構文 - cpprefjp C++日本語リファレンス
参考リンク
auto
を使うようにする(ここは意見が別れるかもしれない)std::for_each
+ ラムダ式)noexcept
を付ける(かなり面倒そうだけど)NULL
じゃなくてnullptr
を使うtypedef
ではなくusing
による型エイリアスを利用するstd::array
を使うstd::map
ではなくstd::unordered_map
を使う(std::set
も同様にstd::unordered_set
を使うほうがよい)enum
ではなくenum class
を使う(より安全で、名前空間も汚さないので)override
を付けるfinal
を付けるfinal
を付けるstd::tuple
の使用を検討するenum class
嬉しいstd::unordered_map
嬉しいstd::list<std::list<int> >
って書かなくてすむ*1:情報の充実っぷりが素晴らしいです
C++11になり、後置き戻り値型、auto型、ラムダ式が導入され、戻り値の記述方法が大幅に増えました。またC++14でさらに新しい記述方法が加わったのでとてもややこしいことになっています。そこで戻り値型の記述方法をまとめてみることにしました。(勉強中のため間違っている箇所があるかもしれません)
int Hoge( void ){ return0; } int [](){ return0; } // コンパイルエラー
おなじみの戻り値の記述方法です。ラムダ式では前置きで記述することはできません。
auto Hoge( void ) -> int { return0; } []() -> int { return0; }
C++11から導入された記述方法です。通常関数で後置きを利用するには、前置き部分にプレースホルダーとしてauto
と記述する必要があります。
Hoge( void ){ return0; } // コンパイルエラー [](){ return0; }
戻り値型を記述しない記述方法です。通常関数では利用できず、ラムダ式にのみ許されています。
戻り値型を明示せず、関数本体から推論させることができます。やり方がいくつかあります。
void
になります通常関数で後置き戻り値型記法のために前置きするautoはただのプレースホルダーであることに注意が必要です。
auto Hoge( void ) { return0; } // 前置きautoは戻り値型の型推論を意味するauto Hoge( void ) -> int { return0; } // 前置きautoは後置き記法のためのプレースホルダーを意味するauto Hoge( void ) -> auto { return0; } // 前置きautoは後置き記法のためのプレースホルダーを意味する。後置きautoは戻り値型の型推論を意味する
auto
のかわりにauto&
と記述することで、型推論させた上でその型の参照型とすることができます。
decltype(auto)
の前にdecltype(式)
の説明を行います。decltype(式)
は型を表し、その型は式
から決定されます。ところでauto
も型を表し、その型は式から決定されます。
decltype(10+1) x = 10+1; // decltype(10+1)は式10+1から int型 となるauto y = 10+1; // autoは式10+1から int型 となる
同じように見えますが、式が参照型の場合に違いが出てきます。
int v = 0; int& r = v; decltype(r) x = r; // decltype(r)は式rから int&型 となるauto y = r; // autoは式rから int型 となる
これがdecltype(式)
とauto
の違いです。上記の例では変数定義でしたが、関数の戻り値型としてのdecltype(式)
とauto
でも同じです。
そして、decltype(auto)
は、decltype(式)
の式部分の記述をコンパイラに推論させる記述方法です。本質的にはdecltype(式)
なので、上記のように式が参照型の場合にはdecltype(auto)
も参照型になります。
戻り値型としてdecltype(auto)
を使った場合、decltype(return文の式部分)
となります。
通常関数 | ラムダ式 | |
---|---|---|
前置き戻り値型 | 必須 | 不可 |
後置き戻り値型 | 可能 | 可能 |
戻り値型の省略 | 不可 | 可能 |
前置きauto | 可能(C++11では後置き戻り値型のプレースホルダとしてのみ利用可能) | 不可(そもそも前置き不可) |
後置きauto | 可能(C++14から) | 可能(C++14から) |
decltype(式)
は式が参照型の変数の場合、戻り値型も参照型になるdecltype(式)
の式記述を省略した記法「前置き」「後置き」「auto」「なし」の組み合わせを羅列してみました。
//-----------------// 前置き戻り値型//-----------------int Hoge( void ){ return0; } // 通常関数/* コンパイルエラーint [](){ return 0; }; // ラムダ式*///-----------------// 後置き戻り値型(前置きautoなし)//-----------------/* コンパイルエラーHoge2( void ) -> int { return 0; } // 通常関数*/ []() -> int { return0; }; // ラムダ式//-----------------// 後置き戻り値型(前置きautoあり)//-----------------auto Hoge3( void ) -> int { return0; } // 通常関数/* コンパイルエラーauto []() -> int { return 0; }; // ラムダ式*///-----------------// 前後戻り値型の省略//-----------------/* コンパイルエラーHoge4( void ){ return 0; } // 通常関数*/ [](){ return0; }; // ラムダ式//-----------------// 前置きauto//-----------------auto Hoge5( void ) { return0; } // 通常関数(C++14から可能)/* コンパイルエラーauto [](){ return 0; }; // ラムダ式*///-----------------// 後置きauto(前置きautoなし)//-----------------/* コンパイルエラーHoge6( void ) -> auto { return 0; } // 通常関数*/ []() -> auto { return0; }; // ラムダ式(C++14から可能)//-----------------// 後置きauto(前置きautoあり)//-----------------auto Hoge7( void ) -> auto { return0; } // 通常関数(C++14から可能)/* コンパイルエラーauto []() -> auto { return 0; }; // ラムダ式*/
注意
0b0011
みたいな書き方ができるようになる1<<2
みたいな書き方をしていたけど、これからは0b0010
のように書いた方が分かりやすいかも参考リンク : 2進数リテラル - cpprefjp C++日本語リファレンス
auto
にしておき、return文からコンパイラに推論させることができる参考リンク : 通常関数の戻り値型推論 - cpprefjp C++日本語リファレンス
decltype(auto)
を使うことでdecltype(式)
より簡潔に書くことができるdecltype(auto)
(decltype(式)
)とauto
の違いは、式が参照だった場合、参照型になるかどうか(autoは参照が解除される)参考リンク : decltype(auto) - cpprefjp C++日本語リファレンス
[a](){return a;}
->[a=a](){return a;}
[&a](){return a;}
->[&a=a](){return a;}
[apple=a](){return apple;}
参考リンク : ラムダ式の初期化キャプチャ - cpprefjp C++日本語リファレンス
auto
にすると、ジェネリックラムダになり、任意の型の引数での呼び出しができるようになるoperator()
が関数テンプレートになっていると考えると分かりやすい参考リンク : ジェネリックラムダ - cpprefjp C++日本語リファレンス
参考リンク : 変数テンプレート - cpprefjp C++日本語リファレンス
参考リンク : constexprの制限緩和 - cpprefjp C++日本語リファレンス
参考リンク : [[deprecated]]属性 - cpprefjp C++日本語リファレンス
123'456'789
のように数値に桁区切りを入れられるようになった0b1111'1111
のように2進数記法でも利用可能C++11から初期化子リストが導入されました。初期化子リストは簡単に見えて結構難しい機能です。そこで初期化子リストを大雑把に理解するための記事を書いてみることにしました。理解することを優先しているので言葉の定義などが不正確だったりするかもしれません。というか筆者もC++11を勉強中の身なので大嘘書いている可能性もあります。間違っていたらごめんなさい。
初期化子リスト自体は「std::initializer_list<T>型のオブジェクトを楽に構築するための機能」です。そこから発展したものとして、「リスト初期化」「統一初期化」という機能が存在します。この記事では、「初期化子リスト機能」と「初期化子リスト全般に関連する機能」を区別するために、前者を「初期化子リスト」、後者を「初期化子リスト関連機能」と呼ぶことにします。つまり、今後この記事内では単に「初期化子リスト」と記述した場合は、「std::initializer_list<T>型のオブジェクトを楽に構築するための機能」を指します。
さて、この「初期化子リスト関連機能」が難しいのは3つの機能が1つの機能のように見えてしまっていることです。3つの機能とは「初期化子リスト」「リスト初期化」「統一初期化」のことです。これらの3つの機能は別々の機能ですが関連した機能でもあります。そこで、この記事ではこれらの3つの機能を順番に説明していきます。そして「初期化子リスト関連機能」をさらに難しくしているいくつかの特殊な仕様もあるのでそれも紹介します。
というわけで、この記事は以下のような構成になります。
「初期化子リスト」をとても強引に説明すると、{式, 式, 式...}
という記法でstd::initializer_list<T>型のオブジェクトを作れる機能です。これが「初期化子リスト関連機能」の基本中の基本です。
#include <stdio.h>#include <initializer_list>// std::initializer_listを利用するのに必要void func( std::initializer_list<int> list ) { for ( int value : list ) { printf( "%d\n", value ); } } int main() { func( {1, 2, 8} ); // そのまま渡す std::initializer_list<int> list = {4,5,6}; // 一旦変数に入れてから func( list ); // 渡すreturn0; }
std::initializer_list<int>という型を見てわかるとおり、{式, 式, 式...}
のそれぞれの式の型は同じである必要があります。
「初期化子リスト」はコンストラクタでも使うことができます。std::initializer_list<int>を1つだけ引数に取るコンストラクタを「初期化子リストコンストラクタ」と呼びます。初期化子リストコンストラクタでオブジェクトを構築する場合は()
を省略して記述することができます。さらに、クラス名 識別子 = クラス名();
形式の変数定義の場合は右辺のクラス名
も省略することができます。
このように()
やクラス名を省略した初期化子リストコンストラクタ呼び出しを「リスト初期化(list initialization)」と呼びます。*1
#include <stdio.h>#include <initializer_list>class A { public: A( std::initializer_list<int> list ) { for ( int value : list ) { printf( "%d\n", value ); } } }; int main() { // 「クラス名 識別子();」形式の変数定義 { A a( {1, 2, 8} ); // 普通にオブジェクト構築 A a2{1, 2, 8}; // ()を省略 } // 「クラス名 識別子 = クラス名();」形式の変数定義 { A a = A( {1, 2, 8} ); // 普通にオブジェクト構築 A a2 = A{1, 2, 8}; // ()を省略 A a3 = {1, 2, 8}; // A()を省略 } return0; }
「統一初期化」あるいは「一様初期化」は、先程の「リスト初期化」を初期化子リストコンストラクタでない通常のコンストラクタの呼び出しでも使えるようにした機能です。つまり、通常のコンストラクタ呼び出し時の(式,式,式...)
ではなく{式,式,式...}
形式で引数を与えることができます。
#include <stdio.h>class B { public: B( int v0, int v1, bool flag ) { } }; int main() { // 「クラス名 識別子();」形式の変数定義 { B b( 1, 2, false ); // 普通にオブジェクト構築 B b2{ 1, 2, false }; // リスト初期化と同じ形式でオブジェクト構築 } // 「クラス名 識別子 = クラス名();」形式の変数定義 { B b = B( 1, 2, false ); // 普通にオブジェクト構築 B b2 = B{ 1, 2, false }; // リスト初期化と同じ形式でオブジェクト構築 B b3 = { 1, 2, false }; // リスト初期化と同じ形式でオブジェクト構築 } return0; }
上記Bクラスのコンストラクタの引数はstd::initializer_list<T>
になっていないのに、{式,式,式...}
によるオブジェクト構築ができています。これが「統一初期化」の機能です。なお、「統一初期化」で利用する{式,式,式...}
はstd::initializer_list
ここまで「リスト初期化」「統一初期化」のコード例では変数定義のみしか扱っていませんでした。しかし、初期化、つまりオブジェクト構築の場面は変数定義以外にもたくさんあります。以下はC++でのオブジェクト構築が発生する場面一覧です。
これらのオブジェクト構築の場面でも、「リスト初期化」「統一初期化」を使うことができます。基本的には()
またはクラス名()
を省略して{}
による記述が使えると考えておけばOKです。
上記、「初期化子リスト」「リスト初期化」「統一初期化」が初期化子リストの基本です。初期化子リストにはこれ以外にも注意すべき仕様がいくつかあります。
{}
は基本的にはstd::initializer_list<T>型のオブジェクトになりますが、それ以外に解釈される場合もあります。
{}
を使いリスト初期化を行った場合 ->デフォルトコンストラクタが呼ばれる{}
の中身が通常の引数として解釈される#include <stdio.h>#include <initializer_list>class A { public: A(){ printf( "A()\n" ); } A(int,constchar*){ printf( "A(int,const char*)\n" ); } A(std::initializer_list<int>){ printf( "A(std::initializer_list<int>)\n" ); } }; int main() { A a0{}; // 出力:A() // 空の{}でリスト初期化を行っているので、std::initializer_list<T>にならない A a1{1,"abc"}; // 出力:A(int,const char*) // 統一初期化なので、std::initializer_list<T>にならない A a2{1,2}; // 出力:A(std::initializer_list<int>) // リスト初期化だが空の{}ではないのでstd::initializer_list<T>になる A a3({}); // 出力:A(std::initializer_list<int>) // 空の{}だがリスト初期化ではないので、std::initializer_list<T>になるreturn0; }
「初期化子リスト」は実は式ではありません。式を記述する場所ではたいてい「初期化子リスト」も記述できるので、式のように見えますが式ではありません。
なので、「初期化子リスト」はdecltype()
では使えなかったりします。
「リスト初期化」と「統一初期化」は見た目上同じ記法になるため、初期化子リストコンストラクタと通常のコンストラクタのどちらの呼び出しとも解釈できる場合があります。このような場合、初期化子リストコンストラクタが優先して呼び出されます。通常のコンストラクタの方を呼び出したい場合、「統一初期化」記法は諦めて{}
ではなく()
を使う必要があります。
#include <stdio.h>#include <initializer_list>class A { public: A(){ printf( "A()\n" ); } A(int,int){ printf( "A(int,int)\n" ); } A(std::initializer_list<int>){ printf( "A(std::initializer_list<int>)\n" ); } }; int main() { A a1{1,2}; // 出力:A(std::initializer_list<int>) // 統一初期化だと解釈するとA(int,int)になるが、リスト初期化が優先される A a2(1,2); // 出力:A(int,int) // {}をやめれば、当然通常のコンストラクタであるA(int,int)と解釈されるreturn0; }
#include <stdio.h>#include <initializer_list>template<class T> void func1( T value ){} template<class T> void func2( std::initializer_list<T> value ){} int main() { // auto v1 = {}; // コンパイルエラー: 空の初期化子リストは型推論させることはできないauto v2 = {1,2,3}; // 空でない場合は型推論が働くauto v3 = {1}; // C++14まではstd::initializer_list<int>と推論される。C++17ではintと推論される// func1( {1,2,3} ); // コンパイルエラー: テンプレート引数を明示せずに関数テンプレートの引数として初期化子リストを渡すことはできない func1<std::initializer_list<int>>( {1,2,3} ); // テンプレート引数を明示すればOK func2( {1,2,3} ); // std::initializer_list<T>ならOKreturn0; }
()
あるいはクラス名()
を省略できる機能*1:正確には()の省略の有無は関係なく初期化子リストコンストラクタを使ったオブジェクト構築をリスト初期化と呼ぶような気がしますが、ここではわかりやすさのため、「()を省略して{}を使った初期化」を「リスト初期化」と説明しています。
弱参照というものをご存知でしょうか。弱参照はとても便利で個人的にも気に入っている仕組みなのですが、この便利さがあまり知られていないような気がします。そこで今回は弱参照の良さを紹介したいと思います。
(注意)
この記事はC++を前提としています。他のプログラミング言語だとまた事情が変わってきます。
弱参照を知るにはガベージコレクションについて知る必要があります。ガベージコレクションとは不要になったオブジェクトを自動で削除する仕組みのことです。このガベージコレクションでオブジェクトの削除の判断に使われるのが参照です。ガベージコレクションはオブジェクトが参照されている間は削除せず、オブジェクトへの参照がなくなったら削除します。そして、弱参照はガベージコレクションが参照とみなさない参照です。オブジェクトがいくら弱参照されていても、通常の参照がないのであればガベージコレクションはそのオブジェクトを削除します。このように、ガベージコレクションにおいてオブジェクトの生存期間を延命させない参照を弱参照と呼びます。
C++には言語自体にはガベージコレクションの機能がありませんが、C++11から標準ライブラリにスマートポインタという形でガベージコレクションが導入されました*1。
先ほど説明したガベージコレクションでの「参照」と「弱参照」は、C++のスマートポインタでは以下のような対応になります。
種類 | スマートポインタ | ||||||||
---|---|---|---|---|---|---|---|---|---|
参照(強参照) | std::shared_ptr | ||||||||
弱参照 | std::weak_ptr | ||||||||
用語 | 説明 |
---|---|
強参照 | ガベージコレクションでオブジェクトの生存期間を延命させる参照 |
弱参照 | ガベージコレクションでオブジェクトの生存期間を延命させない参照 |
参照(T&) | C++言語機能の参照 |
参照 | 一般的な意味での参照 |
C++11から標準ライブラリにstd::weak_ptr
が入りました。std::weak_ptr
を使うことで弱参照を行えるようになるのですが、ちょっと使い勝手が悪いので弱参照クラスを自作してみました。
ちなみに、弱参照の良さについてはこちら↓で紹介しています。
弱参照は「ガベージコレクションにてオブジェクトの延命を行わない参照」のことですが、一般的な仕様として「オブジェクトの削除を検知できる機能」も持っています。この記事ではそこに注目し、オブジェクトの削除を検知できる参照という意味で弱参照という言葉を使用しています。
なので、この記事で作成する弱参照クラスはガベージコレクションとは関係ありません。他の名称を付けるなら、ダングリングポインタにならない安全なポインタという意味で「SafePointer」とかでしょうか。または、自動で無効化されるということで「AutoNullPointer」とか…?まぁ、とりあえずこの記事中では弱参照として「WeakPtr」という名前にします。
弱参照といえば、C++11から標準ライブラリにstd::weak_ptr
が入りましたが、以下のような仕様になっており少し使い勝手が悪いです。
std::shared_ptr
からしかstd::weak_ptr
を作ることができないので、利用できる場面が限られるstd::weak_ptr
から直接参照先にアクセスできない(std::shared_ptr
を経由する必要がある)std::weak_ptr
はstd::shared_ptr
からしか作ることができないので、自動変数やメンバ変数から作ることができません。これはだいぶ不便です。
// WeakPtr.h#include <assert.h>#include <type_traits>class PtrInfo { public: void* m_Ptr = nullptr; int m_RefCount = 0; PtrInfo( void* ptr ) : m_Ptr( ptr ){} void Inc( void ){ ++m_RefCount; } staticvoid Dec( PtrInfo*& p_info ) { if (!p_info){ return; } --p_info->m_RefCount; if ( p_info->m_RefCount == 0 ) { delete p_info; p_info = nullptr; } } bool IsNull( void ) const { return m_Ptr == nullptr; } template<class T> T* GetPtr( void ) const { returnreinterpret_cast<T*>(m_Ptr); } int GetRefCount( void ) const { return m_RefCount; } }; template<class T> class WeakPtrController; #define nonvirtualtemplate<class T> class WeakPtr { friendclass WeakPtrController<T>; PtrInfo* m_pPtrInfo = nullptr; public: nonvirtual ~WeakPtr() { PtrInfo::Dec( m_pPtrInfo ); } WeakPtr() { } explicit WeakPtr( PtrInfo* p_ptrInfo ) : m_pPtrInfo( p_ptrInfo ) { if ( m_pPtrInfo ) { m_pPtrInfo->Inc(); } } WeakPtr( const WeakPtr& other ) : m_pPtrInfo( other.m_pPtrInfo ) { if ( m_pPtrInfo ) { m_pPtrInfo->Inc(); } } WeakPtr& operator=( const WeakPtr& other ) { PtrInfo::Dec( m_pPtrInfo ); m_pPtrInfo = other.m_pPtrInfo; if ( m_pPtrInfo ) { m_pPtrInfo->Inc(); } return *this; } void Clear( void ) { PtrInfo::Dec( m_pPtrInfo ); m_pPtrInfo = nullptr; } template<class U> WeakPtr<U> GetUpcasted( void ) { static_assert( std::is_base_of<U,T>::value, "" ); // U が T の基底クラスである必要があるreturn WeakPtr<U>( m_pPtrInfo ); } template<class U> WeakPtr<U> GetDowncasted( void ) { static_assert( std::is_base_of<T,U>::value, "" ); // U は T の派生クラスである必要があるif ( dynamic_cast<U*>( GetPtr() ) ) // ポインタが実際に U型 インスタンスであるかどうかをチェック { return WeakPtr<U>( m_pPtrInfo ); } return WeakPtr<U>( nullptr ); // ダウンキャストに失敗したらNULLポインタ(のWeakPtr)を返す } bool IsNull( void ) const { return !m_pPtrInfo || m_pPtrInfo->IsNull(); } T* GetPtr( void ) const { return m_pPtrInfo ? m_pPtrInfo->GetPtr<T>() : nullptr; } T& operator*() const { assert( !IsNull() ); return *GetPtr(); } T* operator->() const { assert( !IsNull() ); return GetPtr(); } operator T*() const { return GetPtr(); } int GetRefCount( void ) const { return m_pPtrInfo ? m_pPtrInfo->GetRefCount() : 0; } }; template<class T> class WeakPtrController { WeakPtr<T> m_WeakPtr; public: nonvirtual ~WeakPtrController() { if ( m_WeakPtr.m_pPtrInfo ) { m_WeakPtr.m_pPtrInfo->m_Ptr = nullptr; } } explicit WeakPtrController( T* ptr ) : m_WeakPtr( new PtrInfo( ptr ) ){} WeakPtrController() = delete; WeakPtrController( const WeakPtrController& other ) = default; WeakPtrController& operator=( const WeakPtrController& other ) = default; template<class U> WeakPtr<U> GetDowncasted_unsafe( U* p_this ) { (void)p_this; static_assert( std::is_base_of<T,U>::value, "" ); return WeakPtr<U>( m_WeakPtr.m_pPtrInfo ); } WeakPtr<T> GetWeakPtr( void ){ return m_WeakPtr; } }; #define DEF_WEAK_CONTROLLER(type_) \public: WeakPtr<type_> GetWeakPtr( void ){ return m_WeakPtrController.GetWeakPtr(); } \protected: WeakPtrController<type_> m_WeakPtrController{this}#define DEF_WEAK_GET(type_) public: WeakPtr<type_> GetWeakPtr( void ){ return m_WeakPtrController.GetDowncasted_unsafe( this ); }class{}
#include "WeakPtr.h"class A { DEF_WEAK_CONTROLLER( A ); // WeakPtrを作れるようにするための仕込みpublic: virtual ~A(){} void Print( constchar* p_mes ) const { printf( "%s\n", p_mes ); } }; class B : public A { /* 基底クラスAですでに仕込みが行われているので不要 DEF_WEAK_CONTROLLER( B ); */ DEF_WEAK_GET( B ); // 必須ではないが、これを仕込んでおくと GetWeakPtr() が派生クラス型になるpublic: }; class X {}; int main() { // 基本的な使い方 { WeakPtr<A> weakA; { A a; weakA = a.GetWeakPtr(); if ( A* pA = weakA.GetPtr() ) // このようにif文でポインタを受け取るとオブジェクトの生存チェックが同時にできて楽 { pA->Print( "[1]" ); } if ( A* pA = weakA ) // T*への暗黙の型変換あり { pA->Print( "[2]" ); } } if ( A* pA = weakA.GetPtr() ) // aは破棄されたのでNULLポインタが返る { pA->Print( "[2] Not print" ); } } // アップキャスト { B* pB = new B; WeakPtr<B> weakB = pB->GetWeakPtr(); /* 暗黙の変換によるアップキャスト WeakPtr<A> weakA = pB->GetWeakPtr(); // 変換演算子を用意していないのでコンパイルエラー */// 専用関数(GetUpcasted)でのアップキャスト WeakPtr<A> weakA = pB->GetWeakPtr().GetUpcasted<A>(); // 基底クラスの方のGetWeakPtr()を使うことでアップキャストせずに基底クラスのWeakPtrを取得することも可能 WeakPtr<A> weakA2 = pB->A::GetWeakPtr(); /* 無関係なクラスへのアップキャストはコンパイル時にエラー WeakPtr<X> weakX = pB->GetWeakPtr().GetUpcasted<X>(); */ } // ダウンキャスト { B* pB = new B; WeakPtr<A> weakA = pB->GetWeakPtr().GetUpcasted<A>(); /* 暗黙の変換によるダウンキャスト WeakPtr<B> weakB2 = weakA; // 変換演算子を用意していないのでコンパイルエラー */// 専用関数(GetDowncasted)でのダウンキャスト WeakPtr<B> weakB2 = weakA.GetDowncasted<B>(); // 実行時にダウンキャストが行われる。weakAの参照先がBのインスタンスでない場合はダウンキャストに失敗しweakB2はNULLになる。 assert( weakB2.GetPtr() != nullptr ); // 今回はweakAの参照先がBのインスタンスなので、NULLにはならない。// ダウンキャスト失敗例 { A* pA = new A; WeakPtr<A> weakA2 = pA->GetWeakPtr(); WeakPtr<B> weakB3 = weakA2.GetDowncasted<B>(); // BはAの派生クラスなのでコンパイルは通るが、実行時にダウンキャストに失敗し、weakB3はNULLになる。 assert( weakB3.GetPtr() == nullptr ); } /* 無関係なクラスへのダウンキャストはコンパイル時にエラー WeakPtr<X> weakX = weakA.GetDowncasted<X>(); */ } // shared_ptrやunique_ptrと組み合わせて使うことも可能 { std::shared_ptr<B> sharedB = std::make_shared<B>(); WeakPtr<B> weakB = sharedB->GetWeakPtr(); assert( weakB.GetPtr() != nullptr ); sharedB.reset(); assert( weakB.GetPtr() == nullptr ); { std::unique_ptr<B> uniqueB(new B); weakB = uniqueB->GetWeakPtr(); assert( weakB.GetPtr() != nullptr ); } assert( weakB.GetPtr() == nullptr ); } return0; }
基本的な使い方は以下のとおりです。
DEF_WEAK_CONTROLLER
を仕込むその他細かいところ
DEF_WEAK_CONTROLLER
を仕込んだクラスの派生クラスにはDEF_WEAK_CONTROLLER
は不要DEF_WEAK_CONTROLLER
を仕込んだクラスの派生クラスにDEF_WEAK_GET
を仕込むことで、GetWeakPtr()
が派生クラス型になるGetUpcasted<T>
を使うことでT型へアップキャストされたWeakPtr型を取得できる(型が相応しくない場合はコンパイルエラーになる)GetDowncasted<T>
を使うことでT型へダウンキャストされたWeakPtr型を取得できる(型が相応しくない場合はコンパイルエラーになる。型が相応しくても実行時の型が相応しくない場合はNULLになる)各弱参照は参照先オブジェクトへのポインタを直接持つのではなく「参照先オブジェクトへのポインタを持つクラス」へのポインタを持つようにします。実際のクラス名で説明すると、PtrInfo
が実際の参照先オブジェクトへのポインタを持ち、WeakPtr
はそのPtrInfo
へのポインタを持つという形になります。このPtrInfo
は同じ参照先を表すWeakPtr
間で共有されるので、PtrInfo
が持つ参照先オブジェクトへのポインタがNULLになると、そのPtrInfo
を共有しているすべてのWeakPtr
でオブジェクトへの参照がNULLになるという仕組みです。
このPtrInfo
に参照先オブジェクトのポインタをセットしたり、NULLにしたりするのがWeakPtrController
の仕事です。また、WeakPtr
を生成するのもWeakPtrController
の仕事です。
WeakPtrController
は初期化時にコンストラクタで引数として受け取ったポインタを「PtrInfo
が持つ参照先オブジェクトへのポインタ」にセットします。そしてデストラクタでNULLをセットします。なので、WeakPtrController
をメンバ変数として持たせた上で初期化時にthisポインタを渡すことで、「WeakPtrController
をメンバ変数として持ったクラス」が破棄されるときに自動で「PtrInfo
が持つ参照先オブジェクトへのポインタ」にNULLがセットされます。これで、オブジェクトが死ぬとそのオブジェクトを参照していた弱参照が無効化されるという仕組みの完成です。
PtrInfo
、WeakPtr
、WeakPtrController
の関係を図にするとこんな感じです。
PtrInfo
を共有する仕組み先ほど、WeakPtr
がPtrInfo
を共有すると説明しました。さて、あるオブジェクトを共有するには「オブジェクトの所有権」、つまり、オブジェクトの生存期間の管理ができる必要があります。例えば、オブジェクトを自動変数として生成してしまうとオブジェクトの生存期間をそのスコープに限れられてしまうので、オブジェクトの所有権を持っているとは言えなくなります。
オブジェクトの所有権を持つには、基本的にはnewでオブジェクトを生成する必要があります。WeakPtr
で共有するPtrInfo
も、所有権を管理するためにnewで生成されます。
問題はnewしたPtrInfo
をいつdeleteするかです。PtrInfo
は共有されるものなので、最後の所有者がPtrInfo
を放棄するときにdeleteすべきです。これはまさにstd::shared_ptr
のような挙動です。そしてPtrInfo
は、実際にstd::shared_ptr
のように参照カウンタで自動でdeleteされるようになっています。
WeakPtr
とWeakPtrController
は生成/破棄のたびに、共有するPtrInfo
の参照カウントを加算/減算します。そして減算時に参照カウントが0になったらPtrInfo
をdeleteします。
つまり、弱参照の内部の実装に強参照が使われているというわけです。ややこしいですけど。
上記、「弱参照が自動で無効化される仕組み」と「PtrInfo
を共有する仕組み」で弱参照クラスとして最低限の機能が実現できますが、使い勝手を良くするために以下のようなものも用意しました。
WeakPtr
を取得できるようにするためのDEF_WEAK_GETマクロアップキャスト/ダウンキャストでは、static_assert
とstd::is_base_of
を使うことで、そもそも継承関係にない型への変換はコンパイル時にエラーになるようにしました。C++11様様です。
今回作った弱参照クラス、いくつか改善できる箇所があります。
現在の実装ではPtrInfoをnewでヒープメモリから確保するようにしています。これは、PtrInfoを共有できるようにヒープなどの独立したメモリ上に配置する必要があるためですが、PtrInfoのサイズは固定なのでPtrInfoのプールを用意することで高速化とメモリの断片化の回避になります。
プールを使用するとPtrInfoの個数に上限ができてしまいますが、PtrInfoは64bit環境でも1つ16バイトなのでプールサイズをかなり大きめにしても問題ないでしょう。
WeakPtr
のコピーは、PtrInfoへのポインタのコピーと参照カウントの増減くらいなので、たいした処理コストではないと思いますが、それでもムーブに対応すればPtrInfoへのポインタのコピーだけで済むようになるので結構おいしいかもしれません。
今回の実装ではマルチスレッドが全く考慮されていません。同じ参照を指す複数のWeakPtrが別々スレッドに存在する場合まともに動かないはずです。マルチスレッド対応するのか、マルチスレッド対応バージョンのWeakPtrを別に用意するのか、それともマルチスレッド非対応を明確にし別スレッドを検知したらabortするようにするのか…、何かしら対策は必要かもしれません。
C++関連でよく使うサイトや気になった記事をここにまとめておきます。
C++11から導入されたラムダ式はとても便利な機能ですが、その使われ方から不正な参照いわゆるダングリングポインタが発生しやすい機能でもあります。今回はその回避策を考えてみます。
ラムダ式は変数をキャプチャした上で、その呼び出しを遅延できるからです。C++のポインタや参照は、その参照先オブジェクトの寿命が切れることで簡単に不正な参照(ダングリングポインタ)となってしまします。ラムダ式は呼び出しをいくらでも遅延できるため、その間にキャプチャしたポインタや参照の指す先のオブジェクトの寿命が切れてしまい、不正な参照が発生しやすくなっています。
また、ラムダ式内では見た目上はキャプチャした変数のスコープ内かのように見えてしまうのも、不正な参照が発生しやすい原因かもしれません。
{ int a = 0; std::function<void()> func = [&](){ a = 10; // ↑このaはちゃんと寿命内のスコープにいるように見えるが、// このラムダ式が呼び出されたときには寿命外のスコープにいるかもしれない }; }
#include <stdio.h>#include <functional>class A { int m_ID = 777; public: void Print( constchar* p_mes ) const { printf( "[ID:%d]%s\n", m_ID, p_mes ); } }; int main() { std::function<void()> func; { A* pA = new A; func = [=](){ if ( pA == nullptr ){ return; } pA->Print( "Lambda" ); }; // pAインスタンス削除delete pA; pA = nullptr; } func(); // 不正な参照が行われるreturn0; }
pA
をラムダ式でコピーキャプチャ([=]
)し、std::function
に格納std::function
を呼び出す前にA型インスタンスを削除。pA
にもnullptr
をセットpA
はnullptr
にはならずstd::function
を呼び出し、そのまま不正にアクセスしてしまうこの例では、キャプチャ元のpA
にnullptr
をセットしたものの、コピーキャプチャ([=]
)なためキャプチャの方のpA
には反映されずそのまま不正アクセスになっています。それではコピーキャプチャではなく、参照キャプチャ([&]
)にすれば不正なアクセスを回避できるのでしょうか。答えはNOです。たしかに、参照キャプチャにすればキャプチャ元のpA
にnullptr
をセットすることで、キャプチャの方のpA
もnullptr
になります。しかし、そのあとのstd::function
を呼び出すときには、キャプチャの方のpA
の指す先ではなく、キャプチャの方のpA
自体が不正な参照になってしまいます。なぜなら、キャプチャ元のpA
の寿命が切れているからです。
このように、ポインタや参照キャプチャは参照元の破棄(寿命切れ)を検知できないため、容易に不正なアクセスになってしまいます。
先ほどの不正な参照の発生例であった、ポインタの不正なアクセス、つまりダングリングポインタについては弱参照で簡単に回避できます。弱参照といえばstd::weak_ptr
ですが、いろいろ面倒臭さがあり個人的に好きではないので今回は自作の弱参照クラスを利用します。弱参照クラスの実装については当サイトのこちらの記事をご覧ください ->「【C++】弱参照クラスを自作する」
#include "WeakPtr.h"#include <stdio.h>#include <functional>class A { DEF_WEAK_CONTROLLER( A ); // WeakPtrを作れるようにするための仕込みint m_ID = 777; public: void Print( constchar* p_mes ) const { printf( "[ID:%d]%s\n", m_ID, p_mes ); } }; int main() { std::function<void()> func; { A* pA = new A; auto weakA = pA->GetWeakPtr(); func = [=](){ if ( weakA.IsNull() ){ return; } pA->Print( "Lambda" ); }; // pAインスタンス削除delete pA; pA = nullptr; } func(); // 弱参照により不正な参照が回避されるreturn0; }
変更点
DEF_WEAK_CONTROLLER
)クラス側に弱参照のための仕込みが必要ですが、それを含めても3行のコードの追加で不正な参照の回避ができるようになりました。かなりお手軽ではないでしょうか。
ラムダ式の前に弱参照変数を用意している部分は、C++14以降なら初期化キャプチャを利用できます。
func = [=,weakA=pA->GetWeakPtr()](){ if ( weakA.IsNull() ){ return; } pA->Print( "Lambda" ); };
std::shared_ptr
)による回避策強参照(std::shared_ptr
)でも不正な参照は回避することができますが、以下の理由であまり好きではありません。
もちろん強参照が有効な手段の場合もあります。
この辺は「【C++】弱参照のすすめ」でも紹介しています。
今回紹介した弱参照による回避策は、弱参照を生成できるクラスへの参照(ポインタ)にしか利用できません。なので例えば、int型ポインタのコピーキャプチャ、あるいint型の参照キャプチャなどでは、弱参照による対策が利用できません。
int型の参照キャプチャでの不正アクセスの例
#include <stdio.h>int main() { std::function<void()> func; { int value = 10; func = [&](){ printf( "value = %d\n", value ); }; } func(); // この時点で、valueの寿命は尽きているので不正なアクセスになるreturn0; }
で、ちょっと考えてみたところ、弱参照(WeakPtr)を生成するためのクラスであるWeakPtrController
を使えばそれなりに対策ができそうな気がしてきました。以下のような感じです。
#include <stdio.h>#include "WeakPtr.h"int main() { std::function<void()> func; { int value = 10; WeakPtrController<int> weakCtrl{ &value }; WeakPtr<int> weakValue = weakCtrl.GetWeakPtr(); func = [&,weakValue](){ if ( weakValue.IsNull() ){ return; } printf( "value = %d\n", value ); }; } func(); // valueと同じ寿命であるweakCtrlが破棄されることでweakValueの参照が無効化され、不正なアクセスが回避されるreturn0; }
ポイント
WeakPtrController<int> weakCtrl{ &value };
とすることで、プリミティブ型からもWeakPtr
が作れるようになるWeakPtrController
は対象のプリミティブ型と同じ寿命になる場所に配置するWeakPtr
はコピーキャプチャにする実際に試してみた所ちゃんと動作したようなのでいけそうです。ただ、WeakPtr
はコピーキャプチャにしなければいけないところが忘れやすそうで危ないと思いました。WeakPtr
をコピーキャプチャにするのを忘れて参照キャプチャにしてしまうと、WeakPtr
自体の寿命切れによる不正なアクセスとなってしまいます。これはコンパイルエラーにならず、場合によってはまともに動作しているようにも見えてしまうのがやっかいです。
プリミティブ型への参照の不正アクセス対策として紹介してきましたが、この方法はプリミティブ型以外にもDEF_WEAK_CONTROLLER
マクロによる弱参照の仕込みができないクラス、例えば標準ライブラリのクラスなどでも使えそうです。
#include <stdio.h>#include <string>#include "WeakPtr.h"int main() { std::function<void()> func; { std::string value = "aaa"; WeakPtrController<std::string> weakCtrl{ &value }; WeakPtr<std::string> weakValue = weakCtrl.GetWeakPtr(); func = [&,weakValue](){ if ( weakValue.IsNull() ){ return; } printf( "value = %s\n", value ); }; } func(); // valueと同じ寿命であるweakCtrlが破棄されることでweakValueの参照が無効化され、不正なアクセスが回避されるreturn0; }
参照キャプチャはポインタのコピーキャプチャと比べても、更に罠の度合いが強い気がします。なので、できることなら参照キャプチャは極力避けてた方がいいのではないかと思います。参照キャプチャを使うとしても[&]
によるデフォルトキャプチャは使うわず[&var]
による個別のキャプチャ指定を使い、さらに先程のWeakPtrController
を使った例のように参照先オブジェクトの寿命切れを検知できる仕組みを一緒に使うべきかもしれません。
参照キャプチャの利用方針をまとめると以下のとおりです。
[&]
にしない)先ほどの例もよく考えたら参照キャプチャする必要ありませんね。
#include <stdio.h>#include "WeakPtr.h"int main() { std::function<void()> func; { int value = 10; WeakPtrController<int> weakCtrl{ &value }; WeakPtr<int> weakValue = weakCtrl.GetWeakPtr(); func = [=](){ if ( weakValue.IsNull() ){ return; } printf( "value = %d\n", *weakValue ); }; } func(); // valueと同じ寿命であるweakCtrlが破棄されることでweakValueの参照が無効化され、不正なアクセスが回避されるreturn0; }
C#を勉強中なのですが、C#はC++とかなり似ていながらも微妙な違いもあったりして、そこが結構ややこしいです。そこでC#とC++の違いをまとめてみました。
注意
すべて挙げるとキリがないので、大雑把に紹介
enum
はC++のenum class
に相当)::
ではなく.
を使う)using namespace XXX;
, C#:using XXX;
)::識別子
, C#:global::識別子
でアクセスする )private class
など)??
)#if
などはC#にもある)const
と読み取り専用変数のためのreadonly
はある)public:
、C#ではpublic
)void
を記述することはできない(int func(void)
はC++では問題ないが、C#ではコンパイルエラー)this->
ではなく、this.
でメンバへアクセスするコンストラクター初期化子
)の書き方が異なる。C++:X():X(123){}
、C#:X():this(123){}
Dog():Animal(123){}
、C#:Dog():base(123){}
初期化子リスト
、C#:オブジェクト初期化子
。機能も微妙に違う。C#のオブジェクト初期化子
はC99の指示付きの初期化子に近い&
を付けることで参照型になるif(0)
と記述できない
if ( int a = 0 ){}
みたいなやつ)はできないint
などの整数型のデータサイズが決まっている0;
というような無意味な式はC++ではコンパイルが通るがC#ではコンパイルエラーになる)override
を付ける必要がある(C++では任意)ref
キーワードを使うが、C++と違い実引数側(呼び出し側)にもref
キーワードが必要