Quantcast
Channel: Flat Leon Works
Viewing all 120 articles
Browse latest View live

【開発中】2Dアクション「センメツコースター(仮)」

$
0
0

2016年4月から「センメツコースター(仮)」というiOS用のゲームを作っています。

2016:08:18 20-37 from shinya on Vimeo.

ジェットコースター+アクションゲームな感じです。

最初は1ヶ月で完成させることを目標にしていましたが、4ヶ月も経ってしまいました。ゲーム開発って怖い…。 まだ完成の目処は立っていませんが、2016年内リリースを目標にしています。

どうか完成しますように🙏


Xcode7.3へのアップデート後、C++プロジェクトのビルド時間が異様に長くなる問題への対処

$
0
0

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を使ってみる(Macで)

$
0
0

プログラミングをしていてソースコードの行数が一体どれくらいになっているのか気になることがよくあるのですが、Gitリポジトリを解析して統計情報を生成してくれるGitStatsというツールがあることを知りました。ソースコード行数はもちろん、ファイル数、コミット回数などの推移も見ることができます。

公式サイト GitStats - git history statistics generator

どんな感じで表示されるのかはこちらの記事で詳しく紹介されています。

Gitリポジトリに蓄積された歴史を可視化、グラフ化する·GitStats MOONGIFT

今回はメモもかねて私がMacでGitStatsを利用するためにやったことを記しておきたいと思います。

手順

試した環境はMacOSX 10.11です。

大まかな手順は以下の通り

gnuplotをインストール

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が起動するか確認してみます。

GitStatsをGitHubから入手

ターミナルを起動して、適当な場所に移動してから以下のコマンドを実行します。

git clone git://github.com/hoxu/gitstats.git

GitStatsをインストール

git cloneで入手したgitstatsディレクトリに移動して以下のコマンドを実行します。

make install

これでGitStatsのインストールは完了です。

GitStatsを使ってみる

ターミナルを起動して以下のコマンドを実行します。

gitstats 【リポジトリ(.git)の場所】 【生成場所】

コマンドを実行すると指定した生成場所に40個ぐらいのファイルが生成されます。その中にあるindex.htmlをブラウザで開けばリポジトリの統計情報を見ることができます。

ところで、GitStatsが生成した統計情報の行数(Lines)項目はバイナリファイルでもカウントされているようです。FilesタブのExtensions項目で拡張子ごとの行数が見れるので、ソースコードの行数を知りたい場合はそこを見たほうがいいかもしれません。

【C++ アイデア】複数の名前空間に同じ別名を与える

$
0
0

C++では名前空間に別名を与えることができます。

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

【センメツコースター】ライセンス表記

$
0
0
-------------------------------
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アクションゲーム「センメツコースター」の紹介

$
0
0

長い間作っていたゲームがようやく完成しました。

センメツコースターはiOS用2Dアクションゲームです。広告無しの有料アプリ(240円)です。 レールの上をコースター(乗り物)で移動して敵を倒す(センメツ)するのが目的です。

こんな人におすすめです。

  • iPhoneで仮想パッドを使ったアクションゲームをやりたい人
  • ちまちま動くのを見るのが好きな人
  • ロッコが好きな人

5つのワールドにそれぞれ5ステージで合計25ステージあります。クリアするだけなら1〜2時間くらいのボリュームだと思いますが、各ステージでスコアレートで星3つを目指したり、チャレンジモードで遊んだりすればもうちょっと長く遊べます。

正直地味なゲームですが、丁寧に作ったつもりなので楽しんでいただけたらうれしいです。

※ゲーム内の音楽はPANICPUMPKINさんのフリー素材を使わせていただきました。

スクリーンショット

AppStoreリンク

ゲームエンジンを使わずにC++とOpenGLでゲームを作った話

$
0
0

先日、iOS用ゲームアプリ「センメツコースター」をリリースしました。このゲームの開発にはゲームエンジンは使っておらず、C++OpenGLOpenALなどで実装されています。最近はゲームエンジンを使うことが当たり前になっており、ゲームエンジンを使わないゲームの作り方があまり知られていない気がしたので「センメツコースター」を例にして、どうやってC++OpenGLでゲームを作るのかを(大雑把に)紹介したいと思います。記事中で取り上げたツールやライブラリへのリンクは最後にまとめて掲載してます。

作業環境

作業はすべてMac上で行いました*1が、基本的にクロスプラットフォームなツールやライブラリしか使っていないのでWindowsでも同じように開発できると思います。また、「センメツコースター」はiOS用アプリですが開発自体はMac上で行っていたので、PCゲーム開発の話として読んで下さい。iOS対応の話はまた別の機会に…。

最低限必要なもの

まず、そもそもゲームを作るとはどういうことなのか。ゲームは何で出来ているのか。大きく分けると以下の要素になります。

  • ソフトウェアの生成
  • 絵の表示
  • 音の再生
  • 入力の受付
  • ファイルの読み書き
  • メインループ

ソフトウェアの生成とファイル読み書き以外、すべて素のC++では実現できません。外部のライブラリに頼る必要があります。逆にいうと、これらさえ用意できればあとはC++でコードを書くだけでゲームを作ることができます(理論的には)。

ソフトウェアの生成 : C++コンパイラ

ゲームとは何なのか。実態はソフトウェアです。アプリケーション、実行ファイルとも言います。ソフトウェアを生成する手段はいろいろありますが、「センメツコースター」ではC++コンパイラを使っています。つまりC++コードをコンパイルしてソフトウェアを生成しているわけです。

C++コンパイラはいろいろな種類がありますが、今回はClangを使いました。WindowsだとVC++が一番手軽かもしれません。コンパイラは直接使うことも出来ますが、一般的には統合開発環境(IDE)経由で使います。C++統合開発環境もいろいろありますが、私はQtCreatorを好んで使っています。QtCreatorはクロスプラットフォームなのでWindowsでも使うことができます。他の統合開発環境MacだとXcodeWindowsだとVisualStudioが有名です。

絵の表示 : OpenGL + GLFW

次に必要なのは絵の表示です。素のC++では絵の表示を行うことができないので外部ライブラリに頼ることになります。C++を使ったゲーム開発で一般的なのはOpenGLDirectXです。記事のタイトルにもあるように今回はOpenGLを選びました。OpenGLの魅力は何と言っても動作環境の多さです。MacWindowsはもちろんiOSAndroidでも動作します。ただしOpenGLと言っても1枚岩ではなく、複数のバージョンとOpenGLESというコンパクト版があります。OpenGLESは組み込み用のOpenGLなので、Mac,WindowsOpenGLのみに対応、iOS,AndroidはOpenGLESのみに対応しています。ですが、OpenGLESはOpenGLのコンパクト版なので、OpenGLESを使っておけばそのままOpenGL環境でも動作します(バージョンは合わせる必要があります)。「センメツコースター」ではOpenGLES2.0を選びました。

ところで絵の表示には、絵を表示する場所も用意する必要があります。絵を表示する場所とは、PCゲームの場合はウインドウのことです。また、ウインドウを用意した上でOpenGLを利用できるように関連付けを行う必要もあります。これらをまとめてやってくれるのがGLFWというライブラリです。GLFWを使うことで、簡単にOpenGLの描画先となるウインドウを用意することができます。さらにGLFWは入力の受付やメインループを作成するための機能も持っています。OpenGLでゲームを作るためにうってつけのライブラリです。

音の再生 : OpenAL + ALURE

効果音やBGMの再生もゲームには必須でしょう。音の再生も素のC++では行うことができないので外部のライブラリが必要になります。「センメツコースター」ではOpenALを選択しました。OpenALは名前からわかるようにOpenGLの音声版のような存在です。OpenALOpenGLと同じように多くの環境で動作します。OpenGLとは違いコンパクト版(組み込み版)や大きなバージョン違いはありません。

OpenALは音の再生を行うことができるのですが、wavやoggのような音声ファイルを読み込むことはできません。そこで利用するのがALUREというライブラリです。ALUREはOpenALのユーティリティライブラリです。音声ファイルを読み込んでOpenALへ渡してくれます。

入力の受付(マウス、キーボード、ゲームパッドなど) : GLFW

ゲームにはユーザーからの入力を受け付ける処理も必要です。これも素のC++ではできません*2。これには「絵の表示」のところでも出たGLFWが利用できます。

ファイルの読み書き : C言語標準関数

ゲームにセーブ機能をつける場合、ファイルの読み書き機能が必要になってきます。セーブ機能をつけなくても、テクスチャファイルなどの読み込みで必要です*3。これはC言語の標準関数、fopenとfwriteで対応可能です。fopenとfwriteはC言語の標準関数なので基本的にはどの環境でも動きます*4

メインループ : GLFW

最後に必要なのは定期的にゲームの更新処理と描画処理を実行することです。これをメインループと呼びます。メインループは正しく一定の周期で回す必要があるのですが、それを行うにはモニタの垂直同期待ちを行うか高精度タイマー(時間計測)が必要です。どちらも素のC++では実現できないことですが、GLFWにはどちらも用意されています。

現実的に必要になってくるもの

ここまででゲームを作るために必要最低限のものが揃いました。しかし、このままゲームを作り始めようとしてもいろいろ足りないことに気がつくでしょう。そんな足りないものの中でも特に必要になってくるであろうものを紹介します。これらは自分で実装してもいいし、他の人が作ったライブラリを使ってもいいでしょう。

OpenGLOpenALラッパー

描画を行うために必要なOpenGLですが、OpenGLAPIは使いづらい上に低レベルなので直接使うのではなく扱いやすくしたラッパークラス、ラッパー関数経由で使うことになると思います。

OpenALも同様です。

数学系クラス、関数

  • ベクトル
  • 行列
  • 矩形
  • 当たり判定その他の処理

ベクトルは座標データとして使いますし、当たり判定などでも使います。行列はOpenGLで頂点データを移動回転拡縮をさせるのに必要です。当たり判定はアクションゲームでは特に必要になってくる処理です。

これら数学系クラス、関数は自分で実装するよりすでに存在するライブラリを利用した方がいいかもしれません。なぜならその方が正確かつ高速な可能性が高いからです。「センメツコースター」では全部自分で実装しましたが…。

画像ファイルローダー

画像ファイルローダーとは画像ファイルを読み込んで解析しOpenGL用テクスチャデータとして変換する機能のことです。「センメツコースター」ではTGAファイルのローダーのみ作りました。TGAは簡単なフォーマットなので自作しましたが、pngなどの複雑なフォーマットの場合はライブラリを利用したほうがいいでしょう。

フォント描画システム

フォント描画システムは必須ではありませんが用意しておかないと、テキストを描画する箇所でいちいち画像データを用意する必要が出てきます。フォント描画システムの実装は、まともにやろうとするとかなり大変です。一番楽に必要最低限の実装をするとしたら、文字を0-9A-Zの範囲のみで固定文字幅のフォントテクスチャをペイントソフトなどで用意することでしょうか。固定文字幅でないテキスト描画や、日本語にも対応しようとするとかなり実装難易度が上がります。もしかしたらゲームエンジンを使わなかったことを一番後悔する場面がフォント描画システムを作るときかもしれません。テキストを描画したいだけなのになんでこんなに苦労しているんだろうと。

「センメツコースター」では、フォントテクスチャ生成ツールを作成し、そのツールから生成されたテクスチャデータと文字情報テーブル(jsonファイル)からテキストを描画するシステムを実装しました。

タスクシステム的なもの

ゲームは画面内にいろいろな物が表示されます。そしてよく動きます。これを実現するための古典的な方法がタスクシステムです。別にタスクシステムである必要はないのですが、個々のオブジェクトが毎フレーム更新処理を呼び出される仕組みはゲームと相性がいいのでまず必要になってくると思います。

参考: 【C++ ゲームプログラミング】STLで実装する最小のタスクシステム - Flat Leon Works

さらに実装、導入したもの

「最低限必要なもの」と「現実的に必要になってくるもの」を紹介してきました。ここでは「センメツコースター」でさらに実装、導入したもの紹介します。

UTF8対応文字列クラス

C++には文字列クラスとして標準でstd::stringがありますがUTF8に対応しておらず*5、日本語を含む文字列の場合1文字1文字を正確に扱うことができません。これが問題になるのは、例えばフォント描画システムで日本語を描画する場合などです。またstd::stringは機能も少ないといった不満もあったので、独自に文字列クラスを作成しました。

GUIシステム

GUIシステムとは、ボタンやウインドウなどのGUIを実装するための仕組みです。具体的には以下のような機能群です。

  • Widget(Form)のような共通の基底クラス
  • Widgetの親子関係(位置や表示状態の連動、マウスイベントなどの伝搬制御)
  • マウスやタッチイベント発生時のコールバック
  • ボタンなどの汎用的機能の提供

このようなGUIシステムを用意することでゲーム中のUIの実装が楽になります。またデバッグ機能やツールを作る場合にも利用できます。

Jsonの導入

Jsonとは汎用データフォーマットです。Jsonを使うことで、構造化されたデータをファイルに書き出し/ファイルから読み出しすることができるようになります。ゲームでは、設定ファイルやセーブデータのフォーマットとして利用できます。Json以外の選択肢としてはxml、ini、バイナリなどがありますが、Jsonが一番扱いやすいと思います。Jsonをバイナリ化し高速化したMessagePackというものもあります。

Jsonを利用するためにはJsonフォーマットの読み取りと書き出し処理が必要ですが、「センメツコースター」ではPicoJsonというライブラリを利用しました。

Luaの導入

Luaは軽量スクリプト言語です。単体で使うのではなくプログラムに組み込んで使うことに特化しています。Luaは柔軟なのでC++では大量にコーディングする必要があることを数行で書くことができるようになります。「センメツコースター」ではLuaをゲーム全体の制御と敵生成処理ルーチン、各種イベント、チュートリアルの実装に使っています。特にゲーム全体の制御にLuaを使うことの効果は抜群で、今はLuaを使わずにゲームを作ることは考えられないくらいです(個人の感想です)。

各種データ作成ツールのファイルローダー

データ作成ツールというのは、例えばドット絵エディタやマップエディタのことです。「センメツコースター」ではドット絵エディタとしてAseprite、ドット絵のアニメーションデータエディタとしてDarkFunctionEditor、マップエディタとしてTiledMapEditorを利用し、それぞれのファイルローダーを実装しました。*6

まとめ

以上が「センメツコースター」を作るためにやったことです。もちろん実際にはもっとたくさんのことをやったのですが、C++OpenGLでゲームを作るという意味ではだいたいカバーできていると思います。ただ、さらっと書いてますがいろいろな場面で苦労は多いです。例えばOpenGLで板ポリを出すだけでも相当つまづくポイントが多いです。ですが、この記事で紹介しているようなことを実装できた時点でもう自分だけのゲームエンジンが出来ているようなものです。あとは自分の好きなようにゲームエンジンを強化してくだけです。楽しい!でも、ゲームエンジンばかり作り込んでしまいゲームが完成しないということには注意しましょう。おすすめなのは実際にゲームを作りながら必要になった機能だけゲームエンジンに追加していくことです。

リンク集

*1:ねねっち誰かさんみたいにMacWindowsを積んだりもしていません

*2:標準入力という仕組みでキーボードからの文字入力を受け付けることはできますがゲームとしては使い物になりません

*3:テクスチャデータをC++の配列などで作ることもできますが、現実的ではないでしょう

*4:環境ごとに用意されたAPIを使わないといけない場合や使った方が効率が良い場合もあります

*5:対応はしていませんがUTF8の性質上、文字単体を扱うことを考慮しなければ普通に使えます

*6:この記事の趣旨から離れるのでこれらのツールの説明は省きます

2Dアクションゲーム「センメツコースター」の開発で使ったもの

$
0
0

先日「センメツコースター」の開発が完了しました。そこで開発に使ったものを紹介したいと思います。

制作ツール

IDE : QtCreator

QtCreator : Mac, Windows

C++IDEです。個人的に好きなIDEです。

テキストエディタ : Visual Studio Code

Visual Studio Code : Mac, Windows

C++のコードはIDEでコーディングしていましたが、Luaやその他のテキストはVisual Studio Codeを使っていました。最初はSublime Text2を使っていたのですが、次にAtomを使い始め、最終的にVisual Studio Codeにたどり着きました。

ドット絵作成 : Aseprite

Aseprite(有料) : Mac, Windows

ドット絵作成にはAseprite(v1.1.3)を使いました。Asepriteは有料ですが、使い勝手がよくアニメーションの作成も可能です。Asepriteの良いところはゲーム開発での利用に特化した機能が備わっていることです。具体的には以下のような機能です。

  • スプライト情報のJsonフォーマットなどでの出力機能
  • TGA書き出し
  • パック化(複数のスプライトを1枚のテクスチャにまとめる機能)
  • これらをコマンドラインから利用することが可能

パック化は例えばこんな感じでびっちりと敷き詰めてくれます。

スプライトアニメーション作成 : DarkFunctionEditor

DarkFunctionEditor(Java製) : Mac, Windows

DarkFunctionEditorは、スプライトのアニメーションデータを作成するためのツールです。複数のスプライトを組み合わせてセルを作り、それを並べてアニメーションを作ります。Asepriteにもアニメーション機能はありますが、このツールのようにスプライトを組み合わせてアニメーションを作るということができません。DarkFunctionEditorはタイムラインや各種補間と言った機能がありませんが、2Dのドット絵なゲームなら十分な機能を持っていると思います。ただし、DarkFunctionEditorをゲーム開発に使うにはちょっと手間が必要です。まず、DarkFunctionEditorが読み取るスプライトシートリスト(xml)を作らなくてはいけません。これはAsepriteのスプライト情報の吐き出し機能を使って、自動生成するPythonスクリプトを書きました。次に、DarkFunctionEditorで作成したアニメーションデータ(xml)を読み込んで表示されるプログラムを実装する必要があります。つまりDarkFunctionEditorのローダーを実装する必要があるわけです。

作業フローとしてはこんな感じでやってました

  • Asepriteでスプライト作成
  • Pythonスクリプトを実行
    • Asepriteのスプライト情報吐き出し機能を使ってスプライト情報を吐き出し(json)
    • 吐き出したスプライト情報をDarkFunctionEditorが読み取る形式(xml)へ変換
  • DarkFunctionEditorでアニメーション情報を作成
  • Pythonスクリプトを実行
    • DarkFunctionEditorで作ったアニメーションデータ(xml)を自分のプログラムで扱いやすい形式(json)へ変換
  • ゲーム内で確認

スプライトアニメーションツールは他にも探したのですが、高機能過ぎたり(高機能すぎるとゲーム側でローダーを実装するのが大変になる)、値段が高かったりで、今回のプロジェクトに一番合っていたのがDarkFunctionEditorでした。

絵素材作成、アイコン作成、企画書作成 : Sketch(Mac)

Sketch(有料) : Mac

Sketchはベクターグラフィックソフトです。グラフィックソフトですが、「センメツコースター」では主に企画書として使いました。テキストエディタと違ってテキストを自由な位置に配置できる上に色や大きさを変えることもできる、枠で囲ったり画像を張り付けたりもできる、1つのファイル内でいくつもページ分けができるなど企画書やアイデア出しに便利でした。他にもiOSアプリアイコンを作成したり*1、このサイトでも利用している「センメツコースター」のバナー画像もSketchで作成しました。99ドルと少し高いですがその価値はあります。ただし、販売モデルが変わってしまい、買い切りではなく1年単位のサブスクリプションとなってしまいました。買い切りモデル時に買った人はアップデートを行わなければそのまま使えますが、アップデートを行ってしまうと即ライセンス切れとなってしまい新たにライセンスを買わないと使えなくなってしまいます。ご注意を*2

音楽作成 : GarageBand(Mac)

GarageBand : Mac

「センメツコースター」ではBGMはPANICPUMPKINさんのフリー素材を使用させていただきましたが、ゲーム成功時と失敗時にジングル(短い音楽)だけどうしてもマッチするものが見つからず、ためしにGarageBandで作ってみたらそれっぽいのができたのでそのまま採用しました。

また、GarageBandファミコン風サウンドを利用するためにMagical 8bit Plugを利用しました。

フォント作成 : bitfontmaker2(Webサービス)

bitfontmaker2 : Webサービス

最初はゲーム中のUI用のテキストをAsepriteで描いて使用箇所ごとにテクスチャ化していました。ところがだんだん面倒になってきたので、フォントを自分で作れないか調べていたところ、このWebサービスを発見しました。ブラウザ上でぽちぽちとドットを打って自分だけのフォントを作ることができます。ちなみに私が作ったフォントはここからダウンロードできます。収録されているのは英大文字小文字数字といくつかの記号だけです。一応英小文字も描きましたがサイズの都合上、見た目が悪いです。

制作サポートツール

バージョン管理 : Git + Source Tree

Source Tree : Mac, Windows

ソフトウェア開発には必須なバージョン管理。ソースコードだけでなくリソース(アセット)含めて全部Gitでバージョン管理してました。クライアントはSource Treeを利用。

バックアップ : Dropbox, Time Machine(Mac)

Dropbox : Mac, Windows
Time Machine : Mac標準搭載(ただし外付けHDDなどが必要)

Dropboxはバックアップのためのツールではありませんが、実質的にバックアップにもなります。ただし、Dropboxが対象としているフォルダのみです。Time MachineはMac標準搭載のバックアップソフトウェアです。システム全体の復旧だけでなく、ファイルを1つだけ選んで復旧なんてこともできます。

クリップボード拡張 : Clipy(Mac)

Clipy : Mac

クリップボード履歴を扱えるクリップボード拡張はコーディングでほぼ必須とも言えるものです。昔はClipMenuを使っていたのですが、頻繁にハングするようになってしまったのでClipyに乗り換えました。

動画キャプチャ : QuickTimePlayer(Mac)

動画キャプチャはゲーム開発の進捗動画や、AppStore用の動画を撮るときに必要になってきます。幸いなことにMacデスクトップの動画キャプチャもiPhone実機の動画キャプチャも無料のQuickTimePlayerで行うことができます。ただしデスクトップの動画キャプチャは標準では音声を撮ることができないので注意が必要です。

動画編集 : iMovie(Mac)

iMovie : Mac

AppStore用の動画は最大30秒までしか許可されていないので、動画を編集する作業が発生します。今回、動画編集ツールとしてはiMovieを利用しました。iMovieは無料な上になんとAppStore用のテンプレートもあります。iMovieは今回初めて使いましたが、そんなに苦労せずに動画を編集することができました。

カラーピッカー : Sip(Mac)

Sip(有料) : Mac

Sipはカラーピッカーです。デスクトップ上の色を見たり、RGB値としてコピーできたりします。カラーピッカーはMacに標準で用意されているものがありますが、色をコピーするのにキーボードショートカットが必要だったりフォーマットを指定できないなど不満があります。Sipは簡単に色をコピーできたり、フォーマットを指定することができます。カラーピッカーを使うことで、ペイントソフトで画面デザインを試行錯誤したあとにそのままカラーピッカーで色をコピーしてソースコードにペーストということができるようになります。ソースコード内で直接色を指定する場面が結構あったので重宝しました。

画像コンバート : XnConvert

XnConvert : Mac, Windows

XnConvertは強力な画像コンバータです。複数のファイルをまとめて、しかも複数の処理を行うことができます。リサイズ、クリッピング、回転、フォーマット変換、やりたいことだいたい用意されています。補間なしでリサイズを行うこともできるのが2Dゲーム開発的にポイント高いです。

パフォーマンス、メモリリーク調査 : Instruments(Mac)

InstrumentsはXcodeに付属するツールでプログラムのパフォーマンス調査ツールです。またメモリリークの調査も行うことができます。ソースコードではなく実際に動いているプログラム(プロセス)にアタッチして使います。Xcodeから起動しなくても単体で起動しますし、Xcodeでビルドしたプログラムでなくてもアタッチすることができます(ただし、デバッグモードでビルドする必要がある…気がします)。「センメツコースター」はPC(Mac)上ではQtCreatorでビルドして動作確認をしていましたが、このQtCreatorでビルドしたプログラムでもアタッチして調査することができました。Instrumentsの仕組みが良くわかっていないので予想でしかないのですが、QtCreatorでのビルドに使っていたコンパイラXcode付属のClangコンパイラだったからかなと思っています。ちなみにInstrumentsはiOS実機のアプリの調査も行うことができます。

汎用スクリプト : Python

Python : Mac, Windows

AsepriteやDarkFunctionEditorで作成したデータをコンバートするのに、Pythonを利用しました。その他いろいろちょっとしたコマンドラインツールとして利用しています。

スクリプト実行ボタン(メニューバー作成) : rumps(Mac)

rumps : Mac

rumpsはMacのデスクトップ上部のメニューバーにメニューを追加できるようにするPythonのライブラリです。Pythonライブラリなので、メニューの追加もPythonコードで書くことになります。「センメツコースター」の開発では、各Asepriteファイルのコンバートや各ステージの直接起動、ステージエディタの起動のためのランチャーとして使っていました。この仕組みを用意することでたいぶ開発効率が上がったと思います。

ブラウザ : Vivaldi

Vivaldi : Mac, Windows

プログラミングをしていると、ネットで検索する必要が必ず出てきます。しかも必要な情報はすぐに得られるとは限らないので大量のタブを開くことになります。前はブラウザとしてFirefoxを使っていましたが、タブを閉じる動作があまりにも遅かったのでVivaldiに乗り換えました。Vivaldiの良いところはタブの動作が高速なことはもちろん、標準でタブの縦並びに対応していることです。さきほど言ったようにプログラミングをしていると調べ物で大量のタブを開くのでタブは縦に並べたいのです。VivaldiChromeと同じ内部エンジンを使っているためChrome拡張が使えたりします*3。最近まで日本語入力バグがありましたが、それも解消され最強のブラウザになりました(個人の感想です)。ただしメモリは大量に喰います。

タスク(Todo)管理 : Trello(Webサービス)

Trello : Webサービス

Trelloはタスク(Todo)管理のためのツール(Webサービス)です。ブラウザ上で動いているとは思えないくらい使いやすいツールです。タスク(カード)をリストに分けたりタグをつけた、詳細説明をmarkdownで書けたりと機能も申し分ないです。さらに複数のボードを利用できるのもポイントが高いです。あまりタスク数が多いと破綻してしまいそうですが、個人〜少人数で使う分には問題なさそうです。Trelloは「センメツコースター」の開発の終盤から使い始めたのですが、もっと早く知りたかったです。

使用ライブラリ

  • OpenGL
  • OpenAL
  • ALURE
  • GLFW(PC上での開発中のみ)
  • Lua
  • PicoJson

使用ライブラリに関しては詳しくはこちらの記事で。

ゲームエンジンを使わずにC++とOpenGLでゲームを作った話 - Flat Leon Works

使用素材

上でフォントを自作したと書きましたがあれは英数字だけなので、ひらがなだけでなく漢字まで含まれているPixelMplusはとても助かりました。音楽はファミコン風の音楽素材を公開してくださっているPANICPUMPKINさんのものを利用しました。やっぱりドット絵にはピコピコなサウンドが良いのです。

最後に

今回紹介した中でとくに良かったのがAsepriteです。Asepriteのスプライト情報出力機能とパック機能のおかげでだいぶ楽できたかなと思います。ところで、「センメツコースター」の開発では今回紹介したツールだけでなく、自分で作ったツールもいくつかありました。その紹介はまた今度ということで。

*1:iOSアプリアイコン作成用のテンプレートも用意されています

*2:私はこのコンボを喰らってSketchをTimeMachineから古いバージョンに復旧しました

*3:使えなかったりもします


プログラミング言語を自作した話

$
0
0

数年前、「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はFlanソースコードバイトコードへ変換し、それをFlanVM(Flanの仮想マシン)が実行することで動作します。Luaと同じ仕組みです。ただし将来的にはC++ソースコード生成による実行も考えています。

静的型付け

FlanはLuaとは違い静的型付けです。つまり、全ての変数には型があり、型が一致しないと(変換不可だと)コンパイル時点でエラーが発生します。

オブジェクト指向

FlanはC++から影響を受けたオブジェクト指向プログラミング言語です。C++に存在する、継承やオーバーライドなどの機能はたいてい言語仕様として含まれています。ただし、多重継承はできません。そのかわりにトレイトがあります。

文末にセミコロン不要

FlanはC++とは違い、文末にセミコロンは不要です。セミコロンを使って1行に複数の文を記述することも可能です。

入れ子可能なコメントアウト

FlanはC++とは違い、コメントアウトを入れ子にすることができます。これはLuaの影響を受けています。

## ここはコメント#-ここもコメント
-##-入れ子
#--なコメントアウトも
--#可能
-##- その場コメント -##- -10 -##!-ここはコメントではない
-#

前方宣言不要

FlanはC++とは違い、前方宣言が不要です。なぜ不要かというと、構文解析(パース)を2パスで行っているからです*2

暗黙のreturn

Flanではreturnを明示的に記述しなくても、関数内で最後の式文が自動的にreturn文になります。

deffunc:[int]([int]a,[int]b)
    a+b ## 最後の文が自動でreturnされる
end

auto型(型推論)あり

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 )

3種類の参照。所有、共有、弱参照。

Flanには3種類の参照が存在します。1つは所有。これはC++のunique_ptrのようなもので、所有者が1人であることを保証します。この参照が破棄されるとき、参照先も破棄されます。2つめは共有。これはC++のshared_ptrのようなもので、複数の所有者が存在できることを表します。この参照が破棄されるとき、他に所有者がいない場合は破棄されます。3つめは弱参照です。これはC++のweak_ptrのようなもので、所有ではなく"参照"を表します。参照先が破棄されると弱参照はnullとなります。

ソースコードとしては、所有は[owner:Hoge]、共有は[ref:Hoge]、弱参照は[wref:Hoge]という記述方法になります。

すべてが参照ではない

JavaPythonLuaなど多くのプログラミング言語ではプリミティブ型以外のすべてが参照なことが多いですが、Flanは値型と参照型が個別に存在します。

[A]: a ## 値型
[ref:A]: ref_a ## 参照型

参照系演算子

Flanの参照型への操作は、基本的にデリファレンスしてから行われます。つまりC言語でいうところの常に*ptrが行われるということです。例えば参照型変数aと参照型変数bがあったとして、a = bとした場合、abへの参照がコピーされるのではなく、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

static/非staticで同名のメンバ関数

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に記事がありますが、言語機能としてトレイトを持つプログラミング言語でも、その意味は微妙に違っているようです。

トレイト - 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' ) ## 名前トレイト(機能)のメンバ関数を使える

トレイトテンプレート

トレイトもクラスのようにテンプレートが存在します。

ファイバー(コルーチン)

ファイバー - Wikipedia

ファイバーは中断できる関数オブジェクトのようなものです。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:()が自動で呼び出される

FlanIDE

実はプログラミング言語と同時にIDE(統合開発環境)も作っていました。このIDEはQtを使って作りました。下の画像を見ればだいたいわかると思いますが、機能としては以下のようなものを実装しました。一部、実行形式がC++コード生成だったときの名残もあります。

  • コードエディタ
    • 行数
    • 構文カラー
    • エラー箇所に下線
    • カーソル位置の抽象構文木の表示(ウインドウ下部参照)
  • ファイルリスト
  • コードモデル(クラスやメンバ一覧)の表示、ソースコードジャンプ
  • エラーリスト
  • VM(仮想マシン)デバッガー

FlanIDE

FlanIDE

VMデバッガーは最初は実装していなかったのですが、プリントデバッグVMの動作をデバッグするのがとても辛かったので作りました。世のVM開発者の方々はどうやってデバッグをしているのでしょうか…。

プログラミング言語Flanの実装

最後にプログラミング言語Flanをどうやって実装したのかを紹介したいと思います。思い出しながら書いているので間違っている部分があるかもしれません…。また、「言語モデル*3などFlan独特の名称を使ったりしてます。

実行までの流れ

Flanのソースコードから実行までの流れは以下のようになっています。

Flan実行の流れ

パーサー

パーサーはソースコードを受け取り、抽象構文木(AST)を生成します。このパーサーは、パーサージェネレータであるANTLRを使って生成しました。ANTLRはデフォルトではJavaソースコードを生成しますが、C言語コードを生成させることもできます。ちなみに、C言語用のパーサージェネレータは一般的にはlex/yaccが使われるようです。ただし、yaccには抽象構文木の生成機能はありません。

モデルファクトリ

モデルファクトリはパーサーが生成した抽象構文木から、プログラミング言語Flanの言語モデルを構築します。言語モデルとは簡単に言えば、抽象構文木から意味を読み取って新たに構築したデータ構造です。現在の実装ではこの言語モデルバイトコード生成時だけでなくVM実行時にも必要になります。

バイトコードジェネレータ

バイトコードジェネレータは言語モデルを元に、バイトコードを生成します。

FlanVM

FlanVMはバイトコードを実行するための仮想マシンです。FlanVMにはバイトコード言語モデルを与える必要があります。このFlanVMによってバイトコードが実行されることでようやくFlanが実行されたことになります。

プログラミング言語を作るためのステップ

前の節でFlanがどうやってプログラムを実行しているのかを紹介しました。実行の仕組みだけならこれだけで良いのですが、実際にプログラミング言語を作るとなるとより多くの作業が必要になります。その辺を含めたプログラミング言語を作るためのステップを紹介します。

なお、ここでの説明はFlanの場合のもので、必ずしもこの方法が必要というわけではありません。例えばパーサージェネレータを使わずに自分でパーサーを書くこともできます。

文法を決める

プログラミング言語を作るにはまず文法を決める必要があります。さらに文法を決める前にプログラミング言語にどんな機能を持たせる決めなくてはいけません。ここは楽しい場面ですが、機能を追加すればするほどそれを文法に落とし込むのに苦労することになります。文法は最初にすべてを決めるのではなく少しずつ付け足していくことも可能ですが、新しい文法を導入するとすでに決まっていた文法を修正する必要が出てくる場合があります。実装はあとにしても文法だけは最初から考えておいたほうがいいかもしれません。

文法を厳密に定義する

文法が決まったらそれを厳密に定義します。文法を厳密に定義することはソースコードの構造を決めることでもあります。パーサージェネレータは文法の厳密な定義を必要とします。いきなり、パーサージェネレータ用の文法定義を書いてもいいし、BNF記法で一旦書いてからそれをパーサージェネレータ用の文法定義に落とし込んでもいいでしょう。ANTLRの文法定義方法はBNF記法に近いのでいきなりANTLR用の文法定義を書き始めてもあまり困ることはないです。

パーサーを生成する

パーサージェネレータ用の文法定義ができたらパーサーを生成してもらいます。ANTLRは抽象構文木を生成してくれるのでよいのですが、yaccでは自分で抽象構文木を構築するコードを書かないといけないかもしれません。

言語モデルクラスを作成する

言語モデルとはソースコードのデータ構造です。このデータ構造を構築するためのクラス群が必要になってきます。例えばクラス、関数、変数、式、文などを表すクラスです。変数クラス、式クラス、クラスクラスなどを作っていくのはなかなか楽しいかもしれません。言語モデルクラスには各種エラー処理の実装も必要になります。型が一致しない、変数、関数が見つからないなどです。

言語モデルファクトリを作成する

言語モデルクラスができたら、それらを使って言語モデルを構築する言語モデルファクトリを実装します。言語モデルファクトリはパーサーが出力した抽象構文木を走査して言語モデルを構築していきます。

VM(仮想マシン)を作成する

言語モデルの構築までできるようになったら、あとはそれを実行する仕組みを作るだけです。実行するための仕組みとしてFlanではVMを利用しました。VMとは仮想マシンのことでソフトウェアで実装されたCPUのようなものです。CPUは機械語を読み取って動作しますが、VMバイトコードを読み取って動作します。

VMを実装するには以下のようなことをします。

  • バイトコードのフォーマットを考える
    • オペコードのデータサイズ
    • 各種定数格納方法
  • バイトコード実行の仕組みを作る
  • 言語機能の実装に必要な命令セットを考える
  • 命令を実装していく

長くなってしまうので詳細は省きます。「バイトコード」「スタックマシン」当たりで検索してみてください…。

バイトコードジェネレータを作成する

VMが出来たので、そのVMが利用するバイトコードを生成するバイトコードジェネレータを作ります。バイトコードジェネレータは言語モデルを走査してバイトコードを生成していきます。

コンパイラを作成する

ここまでで「パーサー」、「言語モデルファクトリ」、「バイトコードジェネレータ」が出来ました。これら順番に使うことでソースコードからバイトコードを生成することができます。ただ、このままでは不便なのでこれらの機能をまとめた「コンパイラ」を作りましょう。「コンパイラ」はソースコードを受け取り、「パーサー」、「言語モデルファクトリ」、「バイトコードジェネレータ」を順に使い、バイトコードの生成します。

ソースコードを実行する仕組みを用意する

コンパイラVMが出来たのであとは、ソースコードを受け取り、コンパイルし、VMバイトコード(と言語モデル)を渡して実行する仕組みを用意するだけです。

まとめ

プログラミング言語を作るというのは、本当に楽しくて気付いたら1年くらい経過してました。Flanは現在、開発を中断していますがここまで作ったんだからいつか完成まで持っていきたいと思う…ような思わないような。*4

*1:時間をかけすぎてしまったので、本来の目的であるゲーム制作を始めることにした

*2:正確にはちょっと違いますが

*3:今思うと「言語モデル」ではなく「コードモデル」の方が適切だったような気がします

*4:最近Nimというすごく良さげな、C言語ソースコードを生成するタイプのプログラミング言語を見つけて、「これ欲しかったやつだ!」となっています

【Mac】ブラウザ(HTML)上から任意のプログラムを起動する

$
0
0

通常、ブラウザ上から任意のプログラムを起動することはできないようになっています。これはおそらく安全性のためなので、ブラウザ上から任意のプログラムを起動することは不可能だと諦めていたのですが、最近ついにその方法を発見したので紹介したいと思います。

注意

  • 一部のブラウザしか対応していません。SafariFirefoxでは動作を確認しましたが、Vivaldiではダメでした。Chromeは未調査です。

方法

発見した方法というのはURLスキームを使った方法です。URLスキームというのはURLリンクからアプリケーションを起動する仕組みで、iOSでは多くのアプリがURLスキームに対応しています。URLスキームといえばiOSと思っていたのですが、Macにもあるのではと思い調べてみたところ今回の方法が見つかったわけです。

URLスキーム機能だけでは、任意のプログラムの起動はできないのですが、URLスキーム機能を応用することで任意のプログラムを起動できるようになります。具体的には、URLスキームのパラメータをコマンドとして実行するプログラムを用意すればいいのです。これにより、そのプログラム経由で任意のプログラムが起動できるようになります。

手順としては以下のようになります。

  1. URLスキーム経由でコマンド文字列を受け取り、そのコマンドを実行するプログラムを用意する
  2. そのプログラムをURLスキーム経由で起動できるようにする

URLスキーム経由でコマンド文字列を受け取り、そのコマンドを実行するプログラムを用意する

URLスキームに対応したプログラムを作成するには、Macのアプリケーションとして作成する必要があります。大変そうですが、AppleScriptを使えば簡単にMac用アプリケーションを作成することができます。

手順

  1. プログラム名(アプリケーション名)を決める
  2. スクリプトエディタ(/Applications/Utilities/Script Editor.app)」を起動する
  3. AppleScriptのコードを入力する
  4. アプリケーションとして書き出す

プログラム名(アプリケーション名)を決める

まずプログラム名を決めます。URLスキームとしての名前にもなることを考えて決めましょう。この記事では「URLSchemeExec」という名前にします。URLスキームとしては、「URLSchemeExec://」または「urlschemeexec://」という感じで利用することになります。

スクリプトエディタ(/Applications/Utilities/Script Editor.app)」を起動する

スクリプトエディタを起動し「新規書類」を選ぶと、こんな画面が出るはずです。

AppleScriptのコードを入力する

以下のコードを入力します。"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スキーム呼び出し時の処理は実装されていますが、実際にURLスキーム経由で起動できるようにはなっていません。これをできるようにするには、アプリケーション内のinfo.plistを書き換える必要があります。info.plistはアプリケーションバンドル内のURLSchemeExec.app/Contents/Info.plistにあります。バンドル内に入るには「URLSchemeExec.app」のコンテキストメニューから「パッケージの内容を表示」を選びます。

info.plist を書き換える

以下のコードを追加します。「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スキームを認識させる

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

参考にしたサイト

MacとWindowsで共通のバッチファイル(シェルスクリプト)を利用できるようにする

$
0
0

Windowsにはダブルクリックでスクリプトを実行できるバッチファイルという便利な仕組みがあります。Macにはバッチファイルというものはありませんが、拡張子を.commandにしたシェルスクリプトを用意することでバッチファイルと同じようにダブルクリックでスクリプトを実行させることができます。ところで、WindowsでもMSYS等を導入することでシェルスクリプトを実行することができるようになります。ということは、MacWindowsで共通のバッチファイルを利用することも可能なのではないでしょうか。ということで、いろいろ模索してみた結果、実際に実現することができたのでその方法を紹介したいと思います。

執筆時の環境

  • Windows 10(1709)
  • Msys2 (version 20161025)

注意

  • 「バッチファイル」は正確にはWindowsの用語ですが、ここでは「ダブルクリックで実行できるスクリプトファイル」のことをバッチファイルと呼ぶことにします
  • 今回紹介する方法ではWindowsにMsys2+MinGWを導入する必要があります

実現方法

記事のタイトルは「MacWindowsで共通のバッチファイル(シェルスクリプト)を利用できるようにする」となっていますが、具体的には「MacWindowsシェルスクリプトをダブルクリックで起動できるようにする」という形になります。Macで「シェルスクリプトをダブルクリックで起動できるようにする」は比較的簡単なので、今回紹介する方法のメインは、Windowsで「シェルスクリプトをダブルクリックで起動できるようにする」の実現方法です。

Macシェルスクリプトをダブルクリックで起動できるようにする

Macシェルスクリプトをダブルクリックで起動できるようにするには、シェルスクリプトを記述したファイルの拡張子を.commandにし、さらにファイルに実行権限を与える必要があります。やり方は以下の記事などが参考になります。

参考リンク

Windowsシェルスクリプトをダブルクリックで起動できるようにする

Windowsでのシェルスクリプトの実行環境について

まず、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++】C++11勉強メモ

$
0
0

ずっとC++03縛りを続けてきましたが、そろそろC++11に手を出してもいいだろうと思いC++11の勉強を始めてみました。C++11の情報はcpprefjpさんのページを参考にしています*1

注意

  • 個人的な勉強メモです
  • 筆者はC++11のコードをまだ全然書いたことないです
  • 的はずれなことを書いているかもしれません
  • わかったことがあったら追記していきます

新機能/変更点

auto

  • 変数の宣言時に型の記述を省略できる機能
  • 基本的には変数の宣言はすべてautoに置き換えてしまってもいいのかもしれない
  • ただし、autoを使わない場面ももちろんあると思う。ちょっと考えてみて思いついたのは以下のような場面。

autoを使用しない場面(予想)

  • 暗黙の型変換を行いたい場合
  • 型を明確にしたい場合(autoを使うとソースコードがわかりづらくなるなど)
  • 関数の戻り値型の変更を検知したい場合
    • autoを使っていると関数の戻り値型が変わってもコンパイルが通ってしまう可能性がある(autoを使っていなくても暗黙の型変換でコンパイルが通ってしまう可能性はあるけど)
    • 基本的には外部ライブラリの関数の戻り値に対してはautoは使わないほうがいいかもしれない
コードサンプル
auto hoge = 0;

参考リンク : auto - cpprefjp C++日本語リファレンス

decltype

  • 式を型として利用できる機能
  • あまり利用場面は思いつかないけど、マクロでは大活躍しそう
コードサンプル
decltype(1+2) hoge; // int hoge; となる

参考リンク : decltype - cpprefjp C++日本語リファレンス

範囲for文

  • コンテナの走査コードを楽に記述できる機能
  • コンテナというのは具体的には、iteratorを返すbegin,endメンバ関数を持っているクラスのこと
  • それらを実装すれば自作クラスでも範囲for文を利用できるようになる
  • begin/endの範囲を回すので、コンテナの途中からのループなどは無理っぽい?
    • begin/end以外の範囲を回すのであれば、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>ヘッダーをインクルードする必要がある
  • 初期化子リストは普通の関数の引数としても使える模様 (https://ideone.com/Y3WuIv)
  • 古くからある...記法より扱いやすい可変個引数としても便利かもしれない

参考リンク : 初期化子リスト - cpprefjp C++日本語リファレンス

一様初期化

  • コンストラクタ側がstd::initializer_listで受け取るようになっていなくても、初期化子リストで引数を渡せる機能?
  • この機能によって、オブジェクトの構築時にクラス名を省略できる場合がある(関数の戻り値など)。ちょっと便利?

参考リンク : 一様初期化 - cpprefjp C++日本語リファレンス

ムーブ(ムーブセマンティクス)

  • 「ムーブセマンティクス」は「所有権の移動という概念」のこと
  • ムーブセマンティクスの導入 ->所有権の移動が可能になった(言語レベルでサポートされるようになった)ということ
  • なので、「ムーブセマンティクスを行う」という言い方は間違いで、「ムーブセマンティクスにおけるムーブを行う」が正確な表現になるのかな?
  • あるオブジェクト(クラス)でムーブに対応するには、「ムーブコンストラクタ」と「ムーブ代入演算子」を実装する必要がある
  • ムーブが行われる場面は2つある。1つはstd::moveを利用した場合。もう1つは代入操作の最適化によりムーブが行われる場合。
    • 代入操作の最適化によるムーブは、代入時の右辺の値が一時オブジェクト(右辺値参照)の場合に発生する

参考リンク:

右辺値参照

  • 左辺値とは簡単に言えば「名前がついたオブジェクト」のこと。(例)変数など
  • 右辺値とは簡単に言えば「名前がない一時オブジェクト」のこと。(例)リテラル、関数の戻り値など
  • 左辺値参照/右辺値参照は左辺値/右辺値の型のこと(あるいはその型の変数)
  • 左辺値参照型/右辺値参照型の変数を宣言することができる
    • 左辺値参照型は変数は&で宣言される。C++03時代から存在しており所謂「参照」のこと
    • 右辺値参照型は変数は&&で宣言される。わかりづらいことに、右辺値参照型の変数は左辺値である。しかし型としては右辺値参照である
  • 「ムーブコンストラクタ」「ムーブ代入演算子」では引数として右辺値参照型をとる
  • std::moveを使うことで、左辺値から右辺値参照を取り出すことができる。(この右辺値参照を使って代入などが行われた場合、ムーブ(所有権の移動)が発生する)

参考リンク:

ラムダ式

  • いわゆる無名関数、クロージャ
  • ラムダ式は意味的には関数オブジェクトの定義と生成の糖衣構文らしい
  • 一般的なクロージャーと同じく環境を束縛(キャプチャ)することができる
  • さらにC++11のラムダ式では、キャプチャ対象とキャプチャ方法(コピーか参照か)を選択することができる
  • コピーによりキャプチャした変数のラムダ式内で変更する場合はmutable指定が必要
  • クラスのメンバ関数内でラムダ式を定義すると、そのラムダ式(の関数オブジェクト)はそのクラスのフレンドクラスになる
  • キャプチャを行わないラムダ式は同じシグネチャの関数ポインタへの変換が可能(変換演算子が定義される)
  • 参照によるキャプチャを行う場合、参照先変数の寿命に気をつける

参考リンク : mutable

noexcept

  • 2つの使い方がある
    • [1]関数の定義時に使う : 関数が例外を送出するかどうかを明示できる
    • [2]演算子として使う : 式が例外を送出するかどうかをコンパイル時に判定することができる

参考リンク : noexcept - cpprefjp C++日本語リファレンス

constexpr

  • 定数式(コンパイル時に評価が可能な式)として評価される関数と変数を定義できる機能
  • constexpr関数
    • 関数の定義時にconstexprを付けるとconstexpr関数となり、可能であればコンパイル時に評価されるようになる
    • 実行時に呼び出される場合もある
    • constexpr関数はreturn文1つで構成されている必要がある
    • 関数内部で変数を書き換えることはできない
    • 引数と戻り値の型はリテラル型である必要がある
  • constexpr変数
    • 書き換えることができない
    • constexpr変数にできるのはリテラル型である必要がある

参考リンク : constexpr - cpprefjp C++日本語リファレンス

nullptr, nullptr_t

  • nullptr
    • ヌルポインターを表す定数
    • 今までヌルポインターを表す方法は標準では存在せずNULLマクロなどが使われていた
    • なので、C++11以降ではNULLマクロではなくnullptrの使用が推奨される
  • nullptr_t

参考リンク : nullptr - cpprefjp C++日本語リファレンス

インライン名前空間

  • アクセス時に名前を省略可能な名前空間を定義できる
  • イメージとしては自動でusing namespaceされる名前空間という感じかな

参考リンク : インライン名前空間

ユーザー定義リテラル

  • リテラルサフィックスによる変換処理を可能にする機能
    • 基本的にはリテラルに単位を与えるための機能(時間の単位とか便利そう)
  • サフィックス名は_で始まり、2文字目は小文字である必要がある(_無しと_大文字C++標準規約で予約されているため)
  • イデア次第ですごく悪い面白いことに使えそう

参考リンク : ユーザー定義リテラル - cpprefjp C++日本語リファレンス

関数のdefault/delete宣言

  • = default
    • 暗黙に定義されるメンバ関数を明示的に宣言するための機能
    • 暗黙に定義されるメンバ関数inlinevirtual指定を加えるためにも使うことができる
  • = delete
    • 暗黙に定義されるメンバ関数を定義されないようにするための機能
    • C++03以前でのコピー禁止のためにコンストラクタなどをprivateにするテクニックは、この= deleteで置き換えることが可能になる
    • 通常の関数やメンバ関数にも使用することができる
      • その場合、使用禁止または実装禁止という意味合いになる?
      • 特定の型のオーバーロードを禁止するという用途にも使える
  • = defaultまたは= deleteを使った場合、他のメンバ関数が暗黙に定義されなくなる(面倒だな…)

参考リンク : 関数のdefault/delete宣言 - cpprefjp C++日本語リファレンス

移譲コンストラクタ

  • コンストラクタから自身の別のコンストラクタを呼ぶ機能
  • 便利というか、なんで今までできなかったんだという感じ

参考リンク : 移譲コンストラクタ - cpprefjp C++日本語リファレンス

非静的メンバ変数の初期化

  • メンバ変数の定義と同時に初期値の設定ができる機能
  • 個人的にC++11で嬉しい機能TOP3には入る
  • staticメンバ変数はこの方法で初期化できないらしい。なんで!
    • static constメンバ変数ならできるらしい

参考リンク : 非静的メンバ変数の初期化 - cpprefjp C++日本語リファレンス

継承コンストラクタ

  • 基底クラスと同じ引数のコンストラクタを簡単に定義できる機能
  • 便利と言えば便利だけど、あまり使う機会はなさそう

参考リンク : 継承コンストラクタ - cpprefjp C++日本語リファレンス

overrideとfinal

  • override
    • オーバーライドであることを明示できる機能
    • overrideを付けているのにオーバーライドになっていない場合はコンパイルエラーになる
    • C++03ではオーバーライドであることを示すためにわざと不要なvirtualを残しておいたりしたが、C++11以降では素直にoverrideと記述できるようになった
  • final
    • クラス定義時に使えば継承禁止となる
    • メンバ関数に使えばオーバーライド禁止となる
  • まとめると、「オーバーライドの明示」「オーバーライドの禁止」「継承の禁止」ができるようになったということ

参考リンク : overrideとfinal - cpprefjp C++日本語リファレンス

明示的な型変換演算子オーバーロード

  • 型変換演算子は暗黙/明示的両方の型変換が可能になるが、explicitを付けることで暗黙的変換を禁止させることができる
  • 実質的に型変換機能である1引数コンストラクタにexplicit機能がC++03の時点で存在したことを考えると、同じく型変換機能である型変換演算子explicit機能が付くのは順当な進化だと思う

参考リンク : 明示的な型変換演算子のオーバーロード - cpprefjp C++日本語リファレンス

friend宣言できる対象を拡張

  • テンプレートパラメータ、型の別名もfriend宣言できるようになった
  • 自由度が増すのはいいこと

参考リンク : friend宣言できる対象を拡張 - cpprefjp C++日本語リファレンス

メンバ関数の左辺値/右辺値修飾

参考リンク : メンバ関数の左辺値/右辺値修飾 - cpprefjp C++日本語リファレンス

列挙型の変更点

  • 列挙クラス(enum class)の導入
    • 独自のスコープを持つ
    • 整数型への暗黙の型変換がされない
    • 先行宣言が可能になった
    • 基底型(データサイズ)を指定可能になった
  • 従来の列挙型の変更点
    • 先行宣言が可能になった
    • 基底型(データサイズ)を指定可能になった
  • 列挙クラスがなかったC++03以前は↓のようにstructで囲うことで独自スコープを実現していたので、列挙クラス導入でそれが楽にできるようになって嬉しい
// 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++日本語リファレンス

共用体の制限解除

  • C++03以前は共用体のメンバにはPOD型と呼ばれる強い制限がかかったクラスしか利用できなかったのが、その制限が緩和された
    • コンストラクタ/デストラクタが定義されていても共用体のメンバにできる
      • ただしその場合は、配置newでオブジェクトを構築し、手動でデストラクタを呼ばなければいけない

参考リンク : 共用体の制限解除 - cpprefjp C++日本語リファレンス

テンプレートの>>対応

  • C++03以前はstd::vector<std::vector<int>>のようなテンプレートで>>が連続する書き方はコンパイルエラーになっていたがC++11でそれが解消された
  • これは本当にダサかったから改善されて嬉しい

参考リンク : テンプレートの右山カッコ - cpprefjp C++日本語リファレンス

extern template

  • テンプレートのインスタンス化を抑制する機能
  • コンパイル時間とオブジェクトサイズの抑制が期待できる
  • どこかの翻訳単位(.cppファイル)でインスタンス化しておく必要はある
  • ライブラリ側で使用頻度の高いテンプレートパラメータをインスタンス化しておくという用途がメイン?

参考リンク

usingによるエイリアス(型エイリアス)

  • using newType = oldType;という書き方で型の別名を作れる機能
  • 機能的にはtypedefと同一だが、typedefより分かりやすい記述方法なっている(特に関数ポインタ型で)
  • テンプレートの別名も作ることができる(typedefではできなかった)
  • もうtypedefを使うメリットはないらしい

参考リンク

可変引数テンプレート

  • 可変個のテンプレート引数を受け取るようにできる機能
  • テンプレートを可変個引数にするにはtypenameの替わりにtypename...を使う(class...でも可)
  • 可変引数テンプレートの引数リストを「パラメータパック」と呼ぶ
  • テンプレート引数名...でパラメータパックを展開できる
    • 関数の仮引数部分にテンプレート引数名... 仮引数名とすることで、パラメータパックを引数とする関数を定義できる(これを「関数パラメータパック」と呼ぶ)
    • その歳、...仮引数名とすることで関数パラメータパックを展開することができる
  • 関数パラメータパックの中身を個別に取り出したい場合は再帰などを使う必要がある
  • 関数パラメータパックの中身に個別に変換処理を行いたい場合は「パラメータパックの拡張」という機能が使える
    • pを変換関数とすると、p(仮引数名)...とすることでパラメータパックの拡張が可能
    • 変換関数ではなく、t<仮引数名>...というような、テンプレートのインスタンス化の形でもパラメータパックの拡張は可能
  • sizeof...(テンプレート引数名)で与えられたテンプレート引数の個数を取得できる

参考リンク : 可変引数テンプレート - cpprefjp C++日本語リファレンス

無名enum型のテンプレート引数対応

  • 無名enum型をテンプレート引数として渡せるようになった
  • 無名enum型の型はdecltype(列挙子)で表現できる

参考リンク : ローカル型と無名型を、テンプレート引数として使用することを許可 - cpprefjp C++日本語リファレンス

スレッドローカルストレージ

  • スレッドごとに用意されるstatic変数のようなもの
  • thread_local 型名 変数名;でスレッドローカルストレージとなる

参考リンク : スレッドローカルストレージ - cpprefjp C++日本語リファレンス

staticローカル変数の初期化のスレッドセーフ化

  • staticローカル変数の初期化がスレッドセーフであることがC++の規格により保証されることになった

参考リンク : ブロックスコープを持つstatic変数初期化のスレッドセーフ化 - cpprefjp C++日本語リファレンス

関数戻り値の後置き記法

  • int hoge(void);auto hoge(void) -> int;と記述できるようになった
  • これにより、関数パラメータを利用した戻り値型の記述が可能になる( -> decltype(引数名)など)
  • 関数テンプレートで便利なのかもしれない

参考リンク : 戻り値の型を後置する関数宣言構文 - cpprefjp C++日本語リファレンス

コンパイル時アサート(static_assert)

  • assertのコンパイル時バージョン
  • C++03以前では変なテクニックで実装する必要があったコンパイル時アサートがいつでもどこでも使える!

参考リンク : コンパイル時アサート - cpprefjp C++日本語リファレンス

raw文字列

  • エスケープシーケンスがエスケープされない文字列の記法
  • R"(文字列)"という記法でraw文字列になる
  • R"xxx(文字列)xxx"というふうに()の前後に任意の文字を与えて文字列の開始/終了マークとすることができる
    • これを使うことで()"を文字列中に使うことができるようになる
    • R"xxx(const char* temp = "(aiueo)";)xxx"は文字列としてはconst char* temp = "(aiueo)";になる
  • raw文字列では改行も行うことができる(便利!)

参考リンク : 生文字列リテラル - cpprefjp C++日本語リファレンス

UTF-8文字列

  • u8"文字列"とすることでUTF-8エンコードされた文字列となる
  • 文字列中に\uXXXXと記述すると、コードポイントXXXXのユニコード文字として解釈される(\Uにすると8桁での記述が可能になる)
  • コードポイントによる文字表現は別として、ソースコードUTF-8なら文字列もUTF-8になると思っているので、わざわざ指定する意味があるのかよく分からない(SJISソースコード上でUTF8文字列を埋め込みたい場合に使う?)

参考リンク : UTF-8文字列リテラル - cpprefjp C++日本語リファレンス

属性構文

  • いろいろな箇所に[[属性]]という記述が可能になる
  • 属性はコンパイラに情報を伝えるために利用する
  • C++11ではnoreturncarries_dependencyのみが利用できる

参考リンク : 属性構文 - cpprefjp C++日本語リファレンス

alignas, alignof

  • alignas
    • オブジェクトのメモリ上の配置を特定の境界(アラインメント)にするための記法
    • 変数、メンバ変数、構造体(クラス)にたいして利用できる
  • alignof
    • 特定の型あるいは変数のアラインメントを取得する
  • 今までコンパイラ拡張として存在していたものが標準に入った形

参考リンク

標準ライブラリの変更点

  • std::tupleを追加 : 関数で複数の値を返すのが楽になるそう
  • std::list::size()の計算量がO(1)を保証するように変更
  • std::arrayが追加 : 固定長配列。配列を使うよりこっちを使う方がいいと思う
  • std::forward_listが追加 : 単方向リスト。std::listに対するアドバンテージがわからない…
  • std::unordered_mapが追加 : 待望のハッシュマップ。std::mapより高速なのでほとんどの場合、こっちを使った方がよいと思う
  • std::unordered_setが追加 : std::unordered_mapと同じく、std::setよりこっちを使うようにした方がよい
  • std::shared_ptrが追加 : 待望のスマートポインタ。個人的にはstd::unique_ptrの方が好き
  • std::unique_ptrが追加 : 待望のスマートポインタ
  • std::auto_ptrは非推奨になった
  • std::functionを追加 : 関数ポインタ、関数オブジェクト、メンバ関数ポインタを同じように扱えるようにできるクラス
  • std::threadを追加 : やっと標準でスレッドを扱えるようになった
  • chronoライブラリを追加 : 時間系のライブラリ
  • type_traitsライブラリを追加 : 型特性を得るためのライブラリ
  • regexライブラリを追加 : 待望の正規表現ライブラリ
  • randomライブラリを追加 : C++標準でメルセンヌ・ツイスタが使えるように!

コーディングへの影響

  • 変数の定義時に基本的にはautoを使うようにする(ここは意見が別れるかもしれない)
  • コンテナの走査には範囲for文を使う(または、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嬉しい
  • 範囲for文嬉しい
  • やっとstd::list<std::list<int> >って書かなくてすむ
  • ムーブで処理コスト減るの嬉しい
  • std::list::size()の計算量がO(1)嬉しい(というか今までO(N)だったのが解せない)
  • nullptr、タイピングめんどいな…
  • noexcept、デフォルトにして欲しかった…
  • C++03からの変化が大きすぎる

*1:情報の充実っぷりが素晴らしいです

【C++】C++11以降の戻り値型の記述方法まとめ

$
0
0

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; }

戻り値型を記述しない記述方法です。通常関数では利用できず、ラムダ式にのみ許されています。

戻り値型の記述形式まとめ

  • 通常関数は前置き戻り値が必須(後置きする場合もプレースホルダーとしてautoが必要)
  • 逆に、ラムダ式は前置き戻り値は禁止
  • 通常関数は戻り値型を省略できないが、ラムダ式は省略できる

戻り値型での型推論(auto)

戻り値型を明示せず、関数本体から推論させることができます。やり方がいくつかあります。

戻り値型での型推論の記述方法

  • 通常関数
    • 前置き戻り値型にautoを指定する(C++14から)
    • 後置き戻り値型にautoを指定する(C++14から)
  • ラムダ式
    • 後置き戻り値型にautoを指定する(C++14から)
    • 戻り値型を省略する

型推論の挙動

  • 関数本体にreturn文がなければ戻り値型はvoidになります
  • 関数本体にreturn文が複数ある場合は、共通の型が戻り値型になります

プレースホルダーとしての前置きauto

通常関数で後置き戻り値型記法のために前置きするautoはただのプレースホルダーであることに注意が必要です。

auto Hoge( void ) { return0; } // 前置きautoは戻り値型の型推論を意味するauto Hoge( void ) -> int { return0; } // 前置きautoは後置き記法のためのプレースホルダーを意味するauto Hoge( void ) -> auto { return0; } // 前置きautoは後置き記法のためのプレースホルダーを意味する。後置きautoは戻り値型の型推論を意味する

参照として型推論させる(auto&)

autoのかわりにauto&と記述することで、型推論させた上でその型の参照型とすることができます。

戻り値型としてのdecltype(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から)
  • 前置きauto : 後置き戻り値型のプレースホルダー or 戻り値型の型推論(C++14から)
  • 後置きauto : 戻り値型の型推論(C++14から)
  • auto& : 型推論しつつ、参照型にする
  • autoとdecltype(式)の違い : decltype(式)は式が参照型の変数の場合、戻り値型も参照型になる
  • decltype(auto) : decltype(式)の式記述を省略した記法

(おまけ)戻り値型の記述パターンまとめ

「前置き」「後置き」「auto」「なし」の組み合わせを羅列してみました。

  • 前置きのみ
  • 後置きのみ
  • 前置きauto+後置き
  • なし(戻り値型の省略)
  • 前置きautoのみ
  • 後置きautoのみ
  • 前置きauto+後置き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; }; // ラムダ式*/

参考リンク

【C++】C++14勉強メモ

$
0
0

C++11のお勉強」に続いて、C++14のお勉強です。

注意

  • 個人的な勉強メモです
  • 筆者はC++11/C++14のコードをまだ全然書いたことないです
  • 的はずれなことを書いているかもしれません
  • わかったことがあったら追記していきます

2進数リテラル

  • 0b0011みたいな書き方ができるようになる
  • ビットをフラグのように使う場合、1<<2みたいな書き方をしていたけど、これからは0b0010のように書いた方が分かりやすいかも

参考リンク : 2進数リテラル - cpprefjp C++日本語リファレンス

通常関数での戻り値型の型推論

  • ラムダ式にあった戻り値の型推論を通常の関数でも利用できるようにしたもの?
  • 戻り値型をautoにしておき、return文からコンパイラに推論させることができる
  • 複数のreturn文がある場合は共有の型が推論される。かしこい

参考リンク : 通常関数の戻り値型推論 - cpprefjp C++日本語リファレンス

decltype(auto)

  • decltype(auto)を使うことでdecltype(式)より簡潔に書くことができる
  • ちなみに、decltype(auto)(decltype(式))とautoの違いは、式が参照だった場合、参照型になるかどうか(autoは参照が解除される)

参考リンク : decltype(auto) - cpprefjp C++日本語リファレンス

後置き戻り値型でのauto

  • 後置き戻り値型でautoが利用できるようになる
  • この記法はラムダ式のために導入された(通常関数でautoを利用したければ前置きにすれば良いので)
    • ラムダ式では戻り値型を省略できるので後置き戻り値型でautoを利用できるようになるメリットはなさそうに見えるが、auto&という記述ができるようになるというメリットがある。つまり型推論を利用しつつ参照型で返せるようになる

ラムダ式の初期化キャプチャ

  • キャプチャ方法を式として記述できる機能
  • 今までのキャプチャを初期化キャプチャで記述すると以下のような感じ?
    • コピーキャプチャ : [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の制限緩和

  • C++11でのconstexprにはreturn文1つのみ、変数書き換え禁止など大きな制限があったが、C++14で大幅に緩和された
  • ほぼ普通の関数と同じような記述が可能になった印象

参考リンク : constexprの制限緩和 - cpprefjp C++日本語リファレンス

deprecated属性

  • 属性構文にdeprecatedが追加され、関数やクラスを非推奨だと明記できるようになった
  • 非推奨な関数やクラスを使うとコンパイラにより警告がでる

参考リンク : [[deprecated]]属性 - cpprefjp C++日本語リファレンス

数値リテラルの桁区切り文字

  • 123'456'789のように数値に桁区切りを入れられるようになった
  • 0b1111'1111のように2進数記法でも利用可能

参考リンク : 数値リテラルの桁区切り文字 - cpprefjp C++日本語リファレンス

【C++】初期化子リスト関連機能を大雑把に理解する

$
0
0

C++11から初期化子リストが導入されました。初期化子リストは簡単に見えて結構難しい機能です。そこで初期化子リストを大雑把に理解するための記事を書いてみることにしました。理解することを優先しているので言葉の定義などが不正確だったりするかもしれません。というか筆者もC++11を勉強中の身なので大嘘書いている可能性もあります。間違っていたらごめんなさい。

はじめに

初期化子リスト自体は「std::initializer_list<T>型のオブジェクトを楽に構築するための機能」です。そこから発展したものとして、「リスト初期化」「統一初期化」という機能が存在します。この記事では、「初期化子リスト機能」と「初期化子リスト全般に関連する機能」を区別するために、前者を「初期化子リスト」、後者を「初期化子リスト関連機能」と呼ぶことにします。つまり、今後この記事内では単に「初期化子リスト」と記述した場合は、「std::initializer_list<T>型のオブジェクトを楽に構築するための機能」を指します。

さて、この「初期化子リスト関連機能」が難しいのは3つの機能が1つの機能のように見えてしまっていることです。3つの機能とは「初期化子リスト」「リスト初期化」「統一初期化」のことです。これらの3つの機能は別々の機能ですが関連した機能でもあります。そこで、この記事ではこれらの3つの機能を順番に説明していきます。そして「初期化子リスト関連機能」をさらに難しくしているいくつかの特殊な仕様もあるのでそれも紹介します。

というわけで、この記事は以下のような構成になります。

  • 「初期化子リスト」の解説
  • 「リスト初期化」の解説
  • 「統一初期化」の解説
  • 初期化子リスト関連機能の注意すべき仕様

「初期化子リスト」 〜 std::initializer_list<T>型オブジェクトを楽に構築 〜

「初期化子リスト」をとても強引に説明すると、{式, 式, 式...}という記法で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++でのオブジェクト構築が発生する場面一覧です。

  • 変数/配列の定義時のオブジェクト構築
  • 一時オブジェクトの構築
  • newによるオブジェクト構築
  • 派生クラスコンストラクタから基底クラスを構築
  • 関数の引数の値渡し時(コピーによるオブジェクト構築)
  • 関数の戻り値を返すとき(コピーによるオブジェクト構築)
  • 例外を投げるとき(コピーによるオブジェクト構築)
  • 例外をキャッチするとき(コピーによるオブジェクト構築)

これらのオブジェクト構築の場面でも、「リスト初期化」「統一初期化」を使うことができます。基本的には()またはクラス名()を省略して{}による記述が使えると考えておけばOKです。

初期化子リストの注意すべき仕様

上記、「初期化子リスト」「リスト初期化」「統一初期化」が初期化子リストの基本です。初期化子リストにはこれ以外にも注意すべき仕様がいくつかあります。

{}を使ってもstd::initializer_list<T>にならない場合がある

{}は基本的には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;
}

型推論

  • 空の「初期化子リスト」は型推論させることはできない
  • 素数が1の「初期化子リスト」はC++17以降ではstd::initializer_list<T>ではなくT型に推論される
  • テンプレート引数を明示せずに関数テンプレートの引数として「初期化子リスト」を渡すことはできない
    • ただし、std::initializer_list<T>形式にすれば可能
#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;
}

まとめ

  • 「初期化子リスト関連機能」は大きく3つの機能にわけられる
  • 「初期化子リスト」はstd::initializer_list<T>型オブジェクトを楽に構築するための機能
  • 「リスト初期化」は「初期化子リスト」でオブジェクトを構築する際の()あるいはクラス名()を省略できる機能
  • 「統一初期化」は通常のコンストラクタでも「リスト初期化」記法を使えるようにした機能
  • いくつか注意すべき仕様がある

参考リンク

*1:正確には()の省略の有無は関係なく初期化子リストコンストラクタを使ったオブジェクト構築をリスト初期化と呼ぶような気がしますが、ここではわかりやすさのため、「()を省略して{}を使った初期化」を「リスト初期化」と説明しています。


【C++】弱参照のすすめ

$
0
0

弱参照というものをご存知でしょうか。弱参照はとても便利で個人的にも気に入っている仕組みなのですが、この便利さがあまり知られていないような気がします。そこで今回は弱参照の良さを紹介したいと思います。

(注意)

この記事はC++を前提としています。他のプログラミング言語だとまた事情が変わってきます。

弱参照とは

弱参照を知るにはガベージコレクションについて知る必要があります。ガベージコレクションとは不要になったオブジェクトを自動で削除する仕組みのことです。このガベージコレクションでオブジェクトの削除の判断に使われるのが参照です。ガベージコレクションはオブジェクトが参照されている間は削除せず、オブジェクトへの参照がなくなったら削除します。そして、弱参照はガベージコレクションが参照とみなさない参照です。オブジェクトがいくら弱参照されていても、通常の参照がないのであればガベージコレクションはそのオブジェクトを削除します。このように、ガベージコレクションにおいてオブジェクトの生存期間を延命させない参照を弱参照と呼びます。

C++での弱参照

C++には言語自体にはガベージコレクションの機能がありませんが、C++11から標準ライブラリにスマートポインタという形でガベージコレクションが導入されました*1

先ほど説明したガベージコレクションでの「参照」と「弱参照」は、C++のスマートポインタでは以下のような対応になります。

スマートポインタでの参照方法
種類スマートポインタ
参照(強参照)std::shared_ptr
弱参照std::weak_ptr

ところで、C++にはすでにT&という記法による参照が存在します。ガベージコレクションの「参照」と混同しないように、この記事の以降の文章ではガベージコレクションでの「参照」を「強参照」と記述します。またT&の参照は「参照(T&)」と記述します。

記事中の「参照」の使い分け
用語説明
強参照ガベージコレクションでオブジェクトの生存期間を延命させる参照
弱参照ガベージコレクションでオブジェクトの生存期間を延命させない参照
参照(T&)C++言語機能の参照
参照一般的な意味での参照

弱参照の使い所

弱参照は強参照あるいはガベージコレクションの文脈で名前が出ることが多いので、強参照の代わりに使うという印象があります。例えば、強参照ではオブジェクトが延命されてしまうのでそれを避けるために弱参照を使う。あるいは、強参照の天敵である循環参照を回避するため部分的に強参照の代わりに弱参照を使うなどです。ですが、弱参照の一番の使い所はそこではないのです。

オブジェクトの削除を検知できる参照としての弱参照

弱参照の一番の使い所は、オブジェクトの削除を検知したい場合です。弱参照は参照先のオブジェクトが削除された場合に参照が無効化されるので、弱参照を使うことでオブジェクトがまだ生きているかどうかを知ることができます。C++での通常の参照(T&)やポインタではこれができません。通常の参照(T&)やポインタではオブジェクトが削除された場合、それを知ることができないので削除されたオブジェクトに不正にアクセスすることになってしまいます。これはダングリングポインタと呼ばれ、C/C++での危険な罠の一つになっています。

ダングリングポインタを回避する方法は弱参照だけではありませんが、弱参照を使った方法が手軽で確実かつコストパフォーマンスも良いと思います。

弱参照で「オブジェクトの削除を検知する」vs 強参照で「オブジェクトを延命させる」

弱参照を「オブジェクトの削除を検知する」ために利用できると述べましたが、安全性のため、つまりダングリングポインタを回避するためということであれば、強参照によって「オブジェクトを延命させる」という回避策もあります。では、「オブジェクトの削除を検知する」のと「オブジェクトを延命させる」のはどちらが良いのでしょうか。

弱参照で「オブジェクトの削除を検知する」メリット

私は基本的には、弱参照で「オブジェクトの削除を検知する」方を選ぶのが良いと思います。弱参照を選ぶ理由、あるいは強参照を選ばない理由は以下のとおりです。

  • オブジェクトの所有権は明確にしたほうがよい
  • 延命されたオブジェクトへの処理は意味がないことが多い
  • 強参照はメモリによくない

オブジェクトの所有権は明確にしたほうがよい

強参照の仕組みは本質的には「オブジェクトの共有」です。複数の者がオブジェクトの共有を行うことになります。これはオブジェクトの所有権が曖昧になることでもあります。オブジェクトの所有権が曖昧だとオブジェクトの寿命も曖昧になり、オブジェクトがいつまで生存しているのか予想がつかなくなります。これはプログラムの不具合の原因となる可能性があります。

もちろん、オブジェクトの所有権を明確にすることよりも、所有権を曖昧にしてオブジェクトを確実に生存させることの方が重要な場合もあります。そのような場合は強参照を使ったほうがいいのですが、そういう状況は少ないのではないかと思います。

延命されたオブジェクトへの処理は意味がないことが多い

本来削除されているべきオブジェクトが強参照により延命させられていた場合、そのオブジェクトへの処理は基本的には意味がないものです*2。オブジェクトが延命されたことによりプログラムは落ちないかもしれませんが、意味のない処理を行うのは無駄ですしそこからバグが発生する可能性もあります。

オブジェクトが「延命させられている」のではなくの「意図して生存させている」のであれば問題ありませんが、先ほども言ったようにそのような状況は少ないと思います。

強参照はメモリによくない

強参照はメモリにとって良くないことが多いです。

  • 意図せずメモリに残り続ける可能性がある
  • 循環参照によりメモリリークが発生する可能性がある
  • メモリの断片化を引き起こしやすい

意図せずメモリに残り続ける可能性がある

強参照で参照されているオブジェクトは、どこか1箇所からでも参照されている限り削除されません。そのため、意図せずオブジェクトが生存を続けてしまう可能性があります。当然、それは無駄にメモリを食うことになります。

弱参照の場合、オブジェクトの延命を行わないので、意図せずオブジェクトを生存させてしまうことによる無駄なメモリ消費はありません。

循環参照によりメモリリークが発生する可能性がある

C++の強参照であるstd::shared_ptrには、循環参照があるとオブジェクトが永遠に削除されないという問題があります。オブジェクトが削除されないということは永遠にメモリに残り続けることになるのでメモリリークとなり最悪の場合、メモリ不足でプログラムが落ちます。

弱参照の場合は、そもそもオブジェクトの延命が行われないので循環参照をしていても問題ありません。

メモリの断片化を引き起こしやすい

メモリは確保と解放をスタックのようにLIFO(後入れ先出し)にしないと断片化していきます。メモリの断片化はメモリが潤沢な環境ではたいした問題になりませんが、ゲーム機などメモリが少ない環境だとメモリの確保ができなくなり致命傷となってしまいます。

強参照によるオブジェクトの延命はメモリの解放の順番を変えてしまうため、容易にメモリの断片化を引き起こしてしまいます。

弱参照を使う場合はオブジェクトが延命されないので、強参照のようにオブジェクトが延命されることによるメモリの断片化は発生しません。ただし、弱参照でも参照カウントなどを管理するデータ領域は弱参照同士で共有するため、その管理データ領域が延命され、メモリの断片化が発生する可能性はあります。これを防ぐためには管理データ領域用のメモリ確保を工夫する必要があります。*3

強参照の使い所

弱参照のメリットが多いとはいえ、もちろん強参照にも使い所はあります。

  • 参照先のオブジェクトの生存を保証する
  • オブジェクトの所有権を扱う

参照先のオブジェクトの生存を保証する

強参照で参照されている間、オブジェクトは絶対に破棄されません*4。つまり、強参照で参照している間は確実にオブジェクトへアクセスできるわけです。これは強参照の大きなメリットです。弱参照では、参照している間にオブジェクトが削除される可能性を考慮してプログラミングを行う必要があります。

オブジェクトの所有権を扱う

強参照はオブジェクトの所有権を保持/複製(共有)/放棄/移動*5することができます。これらは弱参照ではできないことです。

まとめ

  • 弱参照を使うことでオブジェクトの削除を検知できる
  • 弱参照を使うことで不正な参照(ダングリングポインタ)を回避できる
  • 強参照でも不正な参照は回避できるが、弱参照にはメリットが多い
  • もちろん強参照を使うべき場面もある

(補足)std::weak_ptrの使い勝手の悪さについて

C++での弱参照といえばstd::weak_ptrですが、残念ながら「オブジェクトの削除を検知するための参照」としては使い勝手が悪いです。

  • std::shared_ptrからしstd::weak_ptrを作ることができない*6
    • つまり、std::shared_ptr管理下にないオブジェクトや自動変数からstd::weak_ptrを作り出すことができない
  • std::weak_ptrから直接参照先にアクセスできない(std::shared_ptrを経由する必要がある)

このようにstd::weak_ptrstd::shared_ptrに強く結びついており、手軽には扱えないようになっています。おそらく、std::weak_ptrはオブジェクトの削除を検知するためではなく、std::shared_ptrの弱点を補うために用意されたクラスなのでしょう。まぁ本来、弱参照はそういうものですし。

弱参照の仕組み自体はシンプルなので、「オブジェクトの削除を検知するための参照」が欲しい場合は自作するという手段もあります。

*1:C++のスマートポインタはガベージコレクションとみなされない場合もありますが、ここではガベージコレクションとして扱います。

*2:オブジェクトが処理対象なのではなく、処理を補佐するためのものならそうとも限りませんが

*3:std::weak_ptr の場合は、その元となる std::shared_ptr の作成時に std::make_shared ではなくアロケータを指定可能な std::allocate_shared を使い独自のアロケータを渡すことでメモリの断片化を防ぐことができるかもしれません。

*4:強参照を正しく使っている限りは

*5:所有権の複製と放棄を組み合わせれば所有権の移動になる

*6:もちろんstd::weak_ptrの複製は可能

【C++】弱参照クラスを自作する

$
0
0

C++11から標準ライブラリにstd::weak_ptrが入りました。std::weak_ptrを使うことで弱参照を行えるようになるのですが、ちょっと使い勝手が悪いので弱参照クラスを自作してみました。

ちなみに、弱参照の良さについてはこちら↓で紹介しています。

【C++】弱参照のすすめ

はじめに

弱参照という言葉の定義

弱参照は「ガベージコレクションにてオブジェクトの延命を行わない参照」のことですが、一般的な仕様として「オブジェクトの削除を検知できる機能」も持っています。この記事ではそこに注目し、オブジェクトの削除を検知できる参照という意味で弱参照という言葉を使用しています。

なので、この記事で作成する弱参照クラスはガベージコレクションとは関係ありません。他の名称を付けるなら、ダングリングポインタにならない安全なポインタという意味で「SafePointer」とかでしょうか。または、自動で無効化されるということで「AutoNullPointer」とか…?まぁ、とりあえずこの記事中では弱参照として「WeakPtr」という名前にします。

std::weak_ptrを使わない理由

弱参照といえば、C++11から標準ライブラリにstd::weak_ptrが入りましたが、以下のような仕様になっており少し使い勝手が悪いです。

  • std::shared_ptrからしstd::weak_ptrを作ることができないので、利用できる場面が限られる
  • std::weak_ptrから直接参照先にアクセスできない(std::shared_ptrを経由する必要がある)

std::weak_ptrstd::shared_ptrからしか作ることができないので、自動変数やメンバ変数から作ることができません。これはだいぶ不便です。

紹介するコードの注意点

  • C++11以降が必須です
  • 紹介するコードは自由に使ってもらって構いませんが、あまりちゃんと動作テストをしていないので参考程度にすることをおすすめします

自作してみた弱参照クラスのソースコード

// 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{}

特徴

  • クラス定義時に仕込みを行う必要あり
  • インスタンスの作成時に特別なことをする必要なし
  • 自動変数でも利用可能
  • アップキャストやダウンキャストにも対応
  • shared_ptrやunique_ptrと組み合わせて使うことも可能

使い方

#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を仕込む
  • そのクラスのインスタンスからGetWeakPtr()で弱参照(WeakPtr<T>)を取得
  • WeakPtr<T>::GetPtr()でポインタを取得( 参照先のオブジェクトが削除されていたらNULLが返る )

その他細かいところ

  • DEF_WEAK_CONTROLLERを仕込んだクラスの派生クラスにはDEF_WEAK_CONTROLLERは不要
  • DEF_WEAK_CONTROLLERを仕込んだクラスの派生クラスにDEF_WEAK_GETを仕込むことで、GetWeakPtr()が派生クラス型になる
  • GetUpcasted<T>を使うことでT型へアップキャストされたWeakPtr型を取得できる(型が相応しくない場合はコンパイルエラーになる)
  • GetDowncasted<T>を使うことでT型へダウンキャストされたWeakPtr型を取得できる(型が相応しくない場合はコンパイルエラーになる。型が相応しくても実行時の型が相応しくない場合はNULLになる)

解説

クラス一覧

  • WeakPtr : 弱参照クラスです
  • WeakPtrController : 弱参照を生成するためのクラスです。弱参照を扱えるようにしたいクラスにメンバ変数として持たせます
  • PtrInfo : ポインタ情報です。こWeakPtrControllerとWeakPtr間で共有されます

弱参照が自動で無効化される仕組み

各弱参照は参照先オブジェクトへのポインタを直接持つのではなく「参照先オブジェクトへのポインタを持つクラス」へのポインタを持つようにします。実際のクラス名で説明すると、PtrInfoが実際の参照先オブジェクトへのポインタを持ち、WeakPtrはそのPtrInfoへのポインタを持つという形になります。このPtrInfoは同じ参照先を表すWeakPtr間で共有されるので、PtrInfoが持つ参照先オブジェクトへのポインタがNULLになると、そのPtrInfoを共有しているすべてのWeakPtrでオブジェクトへの参照がNULLになるという仕組みです。

このPtrInfoに参照先オブジェクトのポインタをセットしたり、NULLにしたりするのがWeakPtrControllerの仕事です。また、WeakPtrを生成するのもWeakPtrControllerの仕事です。

WeakPtrControllerは初期化時にコンストラクタで引数として受け取ったポインタを「PtrInfoが持つ参照先オブジェクトへのポインタ」にセットします。そしてデストラクタでNULLをセットします。なので、WeakPtrControllerをメンバ変数として持たせた上で初期化時にthisポインタを渡すことで、「WeakPtrControllerをメンバ変数として持ったクラス」が破棄されるときに自動で「PtrInfoが持つ参照先オブジェクトへのポインタ」にNULLがセットされます。これで、オブジェクトが死ぬとそのオブジェクトを参照していた弱参照が無効化されるという仕組みの完成です。

PtrInfoWeakPtrWeakPtrControllerの関係を図にするとこんな感じです。

PtrInfoを共有する仕組み

先ほど、WeakPtrPtrInfoを共有すると説明しました。さて、あるオブジェクトを共有するには「オブジェクトの所有権」、つまり、オブジェクトの生存期間の管理ができる必要があります。例えば、オブジェクトを自動変数として生成してしまうとオブジェクトの生存期間をそのスコープに限れられてしまうので、オブジェクトの所有権を持っているとは言えなくなります。

オブジェクトの所有権を持つには、基本的にはnewでオブジェクトを生成する必要があります。WeakPtrで共有するPtrInfoも、所有権を管理するためにnewで生成されます。

問題はnewしたPtrInfoをいつdeleteするかです。PtrInfoは共有されるものなので、最後の所有者がPtrInfoを放棄するときにdeleteすべきです。これはまさにstd::shared_ptrのような挙動です。そしてPtrInfoは、実際にstd::shared_ptrのように参照カウンタで自動でdeleteされるようになっています。

WeakPtrWeakPtrControllerは生成/破棄のたびに、共有するPtrInfoの参照カウントを加算/減算します。そして減算時に参照カウントが0になったらPtrInfoをdeleteします。

つまり、弱参照の内部の実装に強参照が使われているというわけです。ややこしいですけど。

使い勝手を良くする工夫

上記、「弱参照が自動で無効化される仕組み」と「PtrInfoを共有する仕組み」で弱参照クラスとして最低限の機能が実現できますが、使い勝手を良くするために以下のようなものも用意しました。

  • WeakPtrControllerの用意を楽にするDEF_WEAK_CONTROLLERマクロ
  • 派生クラスでは派生型のWeakPtrを取得できるようにするためのDEF_WEAK_GETマクロ
  • WeakPtrをポインタのように扱えるようにするための演算子オーバーロード(->,*,変換演算子)
  • WeakPtrをアップキャスト/ダウンキャストするための関数

アップキャスト/ダウンキャストでは、static_assertstd::is_base_ofを使うことで、そもそも継承関係にない型への変換はコンパイル時にエラーになるようにしました。C++11様様です。

改善案

今回作った弱参照クラス、いくつか改善できる箇所があります。

  • PtrInfoをプールで使い回すようにする
  • ムーブセマンティクス対応
  • マルチスレッド対応
PtrInfoをプールで使い回すようにする

現在の実装ではPtrInfoをnewでヒープメモリから確保するようにしています。これは、PtrInfoを共有できるようにヒープなどの独立したメモリ上に配置する必要があるためですが、PtrInfoのサイズは固定なのでPtrInfoのプールを用意することで高速化とメモリの断片化の回避になります。

プールを使用するとPtrInfoの個数に上限ができてしまいますが、PtrInfoは64bit環境でも1つ16バイトなのでプールサイズをかなり大きめにしても問題ないでしょう。

ムーブセマンティクス対応

WeakPtrのコピーは、PtrInfoへのポインタのコピーと参照カウントの増減くらいなので、たいした処理コストではないと思いますが、それでもムーブに対応すればPtrInfoへのポインタのコピーだけで済むようになるので結構おいしいかもしれません。

マルチスレッド対応

今回の実装ではマルチスレッドが全く考慮されていません。同じ参照を指す複数のWeakPtrが別々スレッドに存在する場合まともに動かないはずです。マルチスレッド対応するのか、マルチスレッド対応バージョンのWeakPtrを別に用意するのか、それともマルチスレッド非対応を明確にし別スレッドを検知したらabortするようにするのか…、何かしら対策は必要かもしれません。

参考リンク

【C++】C++関連ブックマーク

$
0
0

C++関連でよく使うサイトや気になった記事をここにまとめておきます。

リファレンス

ライブラリ

WEBサービス

トピック

【C++アイデア】ラムダ式で発生しがちな不正な参照(ダングリングポインタ)を回避する

$
0
0

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;
}
  • A型インスタンスへのポインタであるpAラムダ式でコピーキャプチャ([=])し、std::functionに格納
  • そのstd::functionを呼び出す前にA型インスタンスを削除。pAにもnullptrをセット
  • しかし、コピーキャプチャなのでキャプチャされたpAnullptrにはならず
  • std::functionを呼び出し、そのまま不正にアクセスしてしまう

この例では、キャプチャ元のpAnullptrをセットしたものの、コピーキャプチャ([=])なためキャプチャの方のpAには反映されずそのまま不正アクセスになっています。それではコピーキャプチャではなく、参照キャプチャ([&])にすれば不正なアクセスを回避できるのでしょうか。答えはNOです。たしかに、参照キャプチャにすればキャプチャ元のpAnullptrをセットすることで、キャプチャの方のpAnullptrになります。しかし、そのあとの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;
}

変更点

  • Aクラスから弱参照を生成できるように仕込み(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++の比較

$
0
0

C#を勉強中なのですが、C#C++とかなり似ていながらも微妙な違いもあったりして、そこが結構ややこしいです。そこでC#C++の違いをまとめてみました。

注意

  • C#C#3.0(.Net Framework 3.5) を想定*1しています
  • C++C++14あたりを想定しています
  • C#C++の比較」というよりは、「C++との違いで理解するC#」という感じの記事です

C++C#の両方にある機能

すべて挙げるとキリがないので、大雑把に紹介

  • 基本
    • コメント(文法も同じ)
    • ブロック文(文法も同じ)
    • 条件分岐(if,else if,else,switch,goto) *2
    • 反復(while,do while,for,範囲for*3 )
    • プリミティブ型(型名もだいたい同じ)
  • 関数
  • クラス
    • コンストラクタ、デストラクタ(ただし、C#のデストラクタはGCの都合上呼び出しタイミングが不定)
    • 移譲コンストラクタ
    • オーバーライド(virtualが必要な点も同じ)
    • メンバアクセス制御(public,protected,private) *5
    • 継承(文法もだいたい同じ。C#にはアクセス制御がないだけ)
    • 継承禁止(C++:finalC#:sealed)
    • 抽象メソッド(C++では純粋仮想関数)
  • 列挙型(C#enumC++enum classに相当)
  • 演算子オーバーロード
  • 例外機構
  • 短絡評価
  • 名前空間(ただし、C#では::ではなく.を使う)
  • 名前空間の省略( C++:using namespace XXX;, C#:using XXX; )
  • グローバル名前空間への明示的アクセス( C++:::識別子, C#:global::識別子でアクセスする )

C++C#の両方にない機能

  • 名前付き引数(ただし、C#は4.0から可能)

C#にしかない機能

  • ガベージコレクション(C++には標準ライブラリとして参照カウント式GCが存在)
  • クラスの宣言が不要(ヘッダファイルを用意する必要がない)
  • クラスの分割定義*6
  • インターフェイス
  • メンバ変数の自動初期化(未初期化のローカル変数はコンパイルエラー)
  • プロパティ構文
  • ユーザー定義属性*7
  • クラスに対するアクセス制御(private classなど)
  • 逐語的識別子 : @を付けることで予約語も識別子として利用可能になる
  • null 合体演算子(??)
  • オーバーフロー例外(デフォルトでは無効)
  • 列挙型の文字列化
  • 静的コンストラクタ
  • 静的クラス(インスタンス不可+静的メンバのみ定義可能)
  • 拡張メソッド
  • 抽象クラス
  • インターフェイス
  • LINQ
  • Nullable型
  • 式木

C++にしかない機能

  • プリプロセスマクロ(#ifなどはC#にもある)
  • コンストラクタでのメンバ変数の初期化
  • 多重継承
  • private継承、protected継承
  • メンバ関数(C#ではメンバ関数しか許されていない)
  • グローバル変数、static変数(staticメンバ変数はある)
  • ポインタ
  • const修飾(定数定義のためのconstと読み取り専用変数のためのreadonlyはある)
  • デフォルト引数(C#3.0時点では無し、C#4.0から導入)
  • 無名名前空間(グローバル名前空間はある)

微妙に違うところ

  • クラス定義の末尾にセミコロン不要
  • クラスメンバのアクセス制御にコロンはつけない(C++ではpublic:C#ではpublic)
  • 引数なしの関数の引数としてvoidを記述することはできない(int func(void)C++では問題ないが、C#ではコンパイルエラー)
  • this->ではなく、this.でメンバへアクセスする
  • C++では構造体とクラスの違いはデフォルトアクセス制御の違いのみだが、C#ではコピー時の挙動、継承の可否などの違いがある
  • 移譲コンストラクタ(C#ではコンストラクター初期化子)の書き方が異なる。C++:X():X(123){}C#:X():this(123){}
  • 基底クラスの初期化方法が少しだけ異なる。C++:Dog():Animal(123){}C#:Dog():base(123){}
  • C++:初期化子リストC#:オブジェクト初期化子。機能も微妙に違う。C#オブジェクト初期化子はC99の指示付きの初期化子に近い
  • C#のデストラクタは、GCの都合上C++と呼び出しタイミングが異なる。RAII目的ならばデストラクタではなく、using ステートメントを使う
  • 値型と参照型の違い。C#では型によって値型か参照型かどうかが分かれる。C++では型に&を付けることで参照型になる
  • if文での暗黙のbool型変換が行われない。なので、if(0)と記述できない
    • なので、if文での変数定義(if ( int a = 0 ){}みたいなやつ)はできない
  • switch文でのフォールスルーは禁止。ただし、ラベルが連続している場合は可能
  • intなどの整数型のデータサイズが決まっている
  • 単純で無意味な式はコンパイルエラー(0;というような無意味な式はC++ではコンパイルが通るがC#ではコンパイルエラーになる)
  • オーバーライド時はoverrideを付ける必要がある(C++では任意)
  • 基底クラスでの仮想関数呼び出しがちゃんと動作する(オーバーライドされている場合はそちらが呼ばれる)
  • 引数の参照渡し(値の参照渡し)にはrefキーワードを使うが、C++と違い実引数側(呼び出し側)にもrefキーワードが必要
    • C++と違い、参照の参照渡しも可能

*1:執筆時点のUnityのデフォルト設定

*2:ただし、一部禁止項目あり

*3:C#ではforeach

*4:C#のデリゲートは複数の関数オブジェクトを格納可能という違いがある

*5:C#はこれに加えて internal がある

*6:C++メンバ関数の実装を場所を分けることならできるが、クラスの定義(宣言)を分割して行うことはできない

*7:C++にも属性構文はあるがユーザーで定義できない

Viewing all 120 articles
Browse latest View live