NimとC++を比較してみる
この記事は「Nim Advent Calendar 2017」の記事として登録させてもらっています。
NimはNimソースコードをC言語ソースへ変換するトランスコンパイラ言語です。これは普段C++を使っている人にとって非常に重要なことです。なぜなら、C++(C言語)が使える環境なら基本的にはどこでもNimを使うことができるからです*1。もちろん、普段C++を使っている環境でNimを使うにはメモリ管理やC++側との連携についてなど考慮すべきことはいろいろあると思いますが、やはりC言語のソースコードが生成されるというのは大きなメリットです。普段C++やC言語を使っている人が現実的な選択肢としてNimの利用を考えることができるわけですから。というわけで、この記事ではC++とNimの書き方の違い、機能の違いなどを紹介したいと思います。
Nimの言語機能についての詳細は公式の Nim Manual を見てください。
ノート:
- 筆者はまだNim初心者なので間違っていたりもっと良い方法があったりするかもしれません。
- C++11以降についても詳しくないので間違っているかもしれません。
- C++とNimの比較についてはNimのwikiにもページがあります。 -> Nim for C programmers
- 随時更新予定
コメントアウト
Nimは複数行コメントがネスト可能な点が嬉しい。#if 0
的な用途としてwhen 0:
も使えなくはないがパースはされるのでコンパイルエラーが発生する場合がある。
【Nim】 # 1行コメント #[ 複数行コメント #[ ネスト可能 ]# ]# when 0: コメントアウトとして利用できなくもない( ただし、インデントが必要な上にパース可能なものじゃないとコンパイルエラーが発生してしまう )
【C++】 // 1行コメント /* 複数行コメント /* ネストはできない */ */ #if 0 プリプロセッサをコメントアウトに使うこともできる(if 0を1と書き換えるだけでコメントアウトを切り替えられるので楽) #endif
文
Nimは文末にセミコロンが不要。セミコロンを使うこともできるので、1行に複数の文を記述することも可能。文末にセミコロンをつけるのとてもわずらわしいのでNimのこの仕様は嬉しい。というかモダンな言語では当然の仕様。
【Nim】 echo( "aaa" ) echo( "bbb" ); echo( "ccc" )
【C++】 puts( "aaa" ); puts( "bbb" ); puts( "ccc" );
変数
NimはC++と違い、変数が自動で初期化されるのが嬉しい。型推論はどちらもある(ただしC++の型推論はC++11以降から)
【Nim】 var x: int # 0で初期化される var y: int = 10 var z = 20 # 型推論を利用
【C++】 int x; // 未初期化になってしまう int y = 10; auto z = 20; // ※C++11以降のみ
関数
Nimは関数ではなくプロシージャという名称だがここでは関数として統一。
Nimの関数で嬉しいのはreturn文を省略できること。return文はプログラミングをしていると何度も書くことになるので、それを省略できるのはとても大きい。もう1つ大きな特徴はNimにはメンバ関数という概念が存在しないこと。すべて関数なので、staticメンバ関数にするか関数にするか迷うことがない。そしてすべての関数でメソッド構文*2が使えるので、実質的にどんな型に対しても自由にメンバ関数を追加できるようになっている。
【Nim】 proc hoge( a:int, b:int =10, c=20 ): int = a + b + c # 最後の式は暗黙に返却される proc hoge2( a:int, b:int =10, c=20 ): int = result = a + b + c # 暗黙のresult変数が利用可能 return result proc hoge3() = proc inner() = # 関数やブロック内部に関数を定義することが可能 echo "inner" inner()
【C++】 int hoge( int a, int b=10, auto c=20 ) { return a + b + c } int hoge2( int a, int b=10, auto c=20 ) { int result = a + b + c return result } void hoge3() { /* 内部関数は定義出来ない。 void inner() { puts( "inner" ); }*/ // ただし内部でクラスは定義できるのでstaticメンバ関数を実質内部関数として利用できなくもない。 struct temp{ static void inner() { puts( "inner" ); } }; temp::inner(); }
機能 | C++ | Nim | 備考 |
---|---|---|---|
インライン化 | あり | あり | 【Nim】inlineプラグマを使う |
デフォルト引数 | あり | あり | |
引数のconst参照渡し | あり | あり | 【Nim】Nimは引数の渡し方は実質的にconst参照渡しになっている*3。ただし、ref型の場合は変更可能となってしまう*4。 |
引数の非const参照渡し | あり | あり | 【Nim】varを指定すれば変更可能になるので、実質的に非const参照渡しになる。 |
オーバーロード | あり | あり | |
演算子オーバーロード | あり | あり | |
ユーザー定義演算子 | なし | あり | |
ローカル関数(関数のネスト) | なし | あり | 【Nim】関数内だけでなくブロック内でも定義可能 【C++】ローカルクラスのstaticメンバ関数をローカル関数として利用できなくもない |
メンバ関数 | あり | なし | 【Nim】Nimではメンバ関数が存在しない。ただしメソッド構文でメンバ関数のように呼び出すことが可能になっている。この仕様はメンバ関数が持つ諸問題*5を解決するのでとてもスマートだと思う。 |
constメンバ関数 | あり | なし | 【Nim】近い指定としてnoSideEffectプラグマがあるが、メンバ変数(フィールド)だけでなくすべての変数に対して変更のない、つまり副作用のない関数になる。 |
副作用のない関数(純粋関数) | なし | あり | 【Nim】noSideEffectプラグマを使う |
動的ディスパッチ (仮想関数) | あり | あり | 【Nim】procではなくmethodを使う 【C++】メンバ関数にvirtualを付ける |
ダブルディスパッチ | なし | あり | 【Nim】Nimのmethodは第一引数だけでなくすべての引数の型からディスパッチを行うので多重ディスパッチが可能になっている 【C++】ダブルディスパッチイディオムを使えば可能 |
純粋仮想関数 | あり | なし | 【Nim】メンバ関数の実装を強制するという意味ではconcept機能もしくはcompiles関数を使えば実現可能…? |
関数ポインタ | あり | あり | 【Nim】Nimではプロシージャル型という名称 |
関数宣言(関数プロトタイプ) | あり | あり | |
クロージャ(無名関数) | あり | あり | 【Nim】ローカル関数(ネストされた関数)はすべてクロージャ 【C++】ラムダ式という名称。(C++11以降の機能) |
暗黙のreturn | なし | あり | 【Nim】最後の式が暗黙にreturnされる |
暗黙のresult変数 | なし | あり | 【Nim】戻り値型のresult変数が暗黙に定義される。result変数は暗黙にreturnされる |
関数テンプレート | あり | あり | 【Nim】Nimではジェネリクスという名称 |
関数テンプレート引数の推定*6 | あり | あり | |
関数テンプレートの特殊化 | あり | 不明 | 【Nim】調査不足です…。is演算子でコンパイル時に分岐とか…? |
コンパイル時関数(定数式用関数) | あり | あり | 【Nim】コンパイル時に計算可能なら自動でコンパイル時関数になる。(明示的に定義したい場合はcompileTimeプラグマを使う) 【C++】constexpr を付ける。(C++11以降の機能) |
戻り値の型推論 | あり | あり | 【Nim】戻り値型としてautoを指定 【C++】戻り値型としてautoを指定。(C++14以降の機能) |
コルーチン(中断再開が可能な関数) | なし | あり | 【Nim】procではなくiteratorとして関数を定義する 【C++】boostにはあるらしい |
型を引数として受け取る | なし | あり | 【Nim】引数の型としてtypedescを指定することで、型を引数として受け取ることが可能(実態としてはジェネリクス関数の糖衣構文) |
非推奨指定 | あり | あり | 【Nim】deprecatedプラグマを使う 【C++】deprecated属性を使う(C++14からの機能) |
SFINAE | あり | なし | 【Nim】SFINAEは無いがcompiles関数(任意のコードについてコンパイル可能かどうかをbool値で取得できる関数)+when文でそれ以上に強力なことが可能 -> コード例 |
関数内static変数 | あり | あり | 【Nim】関数内の変数にglobalプラグマを付ける |
可変個引数 | あり | あり | 【Nim】引数の型としてvarargsを使う 【C++】 ... を使う |
関数呼び出し
Nimは関数の呼び出し時の()
を省略することができる。またメソッド記法を使うことですべての関数を第一引数.関数名(...)
の形式で呼ぶことができる。
【Nim】 hoge( 10 ) hoge 10 # ()を省略できる 10.hoge() # メソッド構文でオブジェクト指向風に記述可能 10.hoge # メソッド構文でも()が省略できる hoge( 10, b=7 ) # 名前付き引数も可能
【C++】 hoge( 10 ) #if 0 // 以下のような呼び出し方はできない hoge 10 10.hoge() 10.hoge hoge( 10, b=7 ) #endif
制御構造
機能 | C++ | Nim | 備考 |
---|---|---|---|
if文 | あり | あり | 【Nim】if:elif:else:という形式 |
コンパイル時if文 | あり | あり | 【Nim】when文を使う 【C++】#ifを使う |
switch文 | あり | あり | 【Nim】Nimではswitch-caseではなくcase-ofという文法になっている |
for文(カウンタ方式) | あり | なし | 【Nim】`0..10`というようなレンジ記法を使うことでカウンタ式のfor文とほぼ同じことが可能 |
for文(イテレータ方式) | あり | あり | 【C++】C++11以降の機能 |
while文 | あり | あり | |
break文 | あり | あり | |
ラベル付きbreak文 | なし | あり | 【Nim】ブロックにラベルを付けることが可能で、そのラベルを指定してスコープを抜けることが可能。(多重for文からの脱出などで使う) |
continue文 | あり | あり | |
goto文 | あり | なし | |
if式 | あり | あり | 【Nim】if文をそのまま式として利用可能 【C++】三項演算子(条件演算子)を使う |
コンパイル時if式 | なし | あり | 【Nim】when文をそのまま式として利用可能 |
switch式 | なし | あり | 【Nim】case文をそのまま式として利用可能 |
組み込み型とその他の型
型 | C++ | Nim | 備考 |
---|---|---|---|
無 | void | void | |
真偽 | bool | bool | C++、Nimともに真偽値は、true, false を使用 |
整数 | int | int | 整数型リスト 【Nim】int,int8,int16,int32,int64, uint,uint8,uint16,uint32,uint64 【C++】int8_t,int16_t,int32_t,int64_t, unsigned int,uint8_t,uint16_t,uint32_t,uint64_t |
浮動小数点数 | float | float | 浮動小数点数型リスト 【Nim】float32,float64 【C++】float,double |
文字 | char | char | |
文字列(低レベル) | char* | cstring | |
文字列 | std::string | string | |
固定長配列 | [](配列) | array | |
動的配列 | std::vector | seq | |
リスト | std::list | SinglyLinkedList DoublyLinkedList | 【Nim】listsモジュールで提供 |
列挙型 | enum | enum | 【Nim】enum値の文字列表現が言語の機能として備わっている。これはとても嬉しい。 |
構造体/クラス | struct/class | object | 【Nim】何も継承していないobjectはC++でいう構造体のようにすべてのメンバ変数が公開される |
タプル | std::tuple | tuple | 【C++】C++11以降 |
セット(集合) | std::set | set | 【Nim】型のサイズが2バイト以内という制限あり |
マップ(辞書) | std::map std::unordered_map | OrderedTable Table | 【Nim】tablesモジュールで提供 【C++】std::unordered_mapはC++11以降 |
参照 | & | ref | 【Nim】refはGC対象になる |
ポインタ | * | ptr | |
関数ポインタ | 関数ポインタ | プロシージャル型 | |
型の別名 | あり | あり | 【Nim】type 別名 = 型 【C++】typedef 型 別名; |
独自型 | なし | あり | 【Nim】type 独自型名 = distinct 型 |
スマートポインタ | あり | なし | 【Nim】Nimは言語レベルでGCをサポートしているのでスマートポインタは不要 【C++】unique_ptr, shared_ptrなどがある(C++11以降) |
弱参照 | あり | なし | 【Nim】なし。欲しい。Nim作者による微妙な実装例はあったが…。-> weakrefs.nim 【C++】weak_ptrがある(C++11以降) |
モジュール(ファイル分割)
C++にはモジュールという概念はないが、ここではNimとの比較のために外部ファイルから利用できる形をモジュールと呼ぶことにする。
C++ | Nim | 備考 | |
---|---|---|---|
モジュール化 | 宣言と実装をそれぞれ別ファイルにする | 別ファイルにする | |
モジュールのインポート | #include | import文 | |
単純な別ファイルの取り込み | #include | include文 |
こうして表にしてみるとたいしたことないように見えるが、宣言と実装を別ファイルにする必要がないというのは大きなメリット。
名前空間
Nimには明示的に名前空間を指定する文法は存在せず、モジュールがそれぞれ独立した名前空間となる。複数のファイルに同じ名前空間を与えたい場合は、その名前空間用のファイルを用意しその中でexport文
を使って複数のファイルをインポートさせるようにすれば可能かもしれない…。(要検証)
クラス(オブジェクト指向プログラミング)
Nimにはクラスという概念は存在しないが、object型を使い独自の型を定義することでクラスのように扱うことができる。比較をわかりやすくするため、ここではNimのobject型をクラスと呼ぶことにする。
C++ | Nim | 備考 | |
---|---|---|---|
クラス定義 | あり | あり | 【Nim】type クラス名 = ref object of RootObj 【C++】class クラス名{}; |
継承 | あり | あり | |
多重継承 | あり | なし | |
派生禁止 | あり | あり | 【Nim】finalプラグマを使う 【C++】finalを使う(C++11以降の機能) |
抽象クラス(派生が必要なクラス) | あり | なし | 【Nim】特定のメソッドを持ったクラスを扱いたい場合はconcept機能が使えるかもしれない。 |
メンバ変数 | あり | あり | |
メンバ関数 | あり | なし | 【Nim】Nimにはメンバ関数の概念がない。そのかわりに関数をメンバ関数のように呼べるようになっている。 |
メンバへのアクセス制御 | あり | あり | 【Nim】モジュール外への公開/非公開という制御のみ可能 【C++】クラス外または継承クラス、フレンドクラスへの公開/非公開が制御可能 |
オーバーライド(動的ディスパッチ) | あり | あり | 【Nim】methodを使う 【C++】virtualを使う |
オーバーライド(動的ディスパッチ)の回避 | あり | あり | 【Nim】procCall 関数( (クラス)引数 )を使う 【C++】クラス::メンバ関数()を使う |
多重ディスパッチ | なし | あり | 【Nim】methodを使うとすべての引数が考慮される 【C++】virtualではthisの型しか考慮されない。(Visitorパターンを使えばダブルまでなら可能) |
クラステンプレート | あり | あり | 【Nim】Nimではジェネリクスという名称 |
コンストラクタ | あり | なし | 【Nim】ユーザー定義のコンストラクタは作成できないが、引数でメンバ変数を初期化できるコンストラクタが自動生成される |
デストラクタ | あり | あり | 【Nim】ただし、GC対象オブジェクトはデストラクタが呼ばれない |
メンバ変数のビットサイズ指定 | あり | あり | 【Nim】bitsizeプラグマを使用 【C++】ビットフィールドを使用 |
構造体定義 | あり | あり | 【Nim】何も継承していないクラスはすべてのメンバ変数が公開扱いになり、C++でいう構造体のような扱いになる 【C++】struct クラス名{}; |
コンパイル時処理
ASTをいじれるNimのマクロはとんでもなく強力ですが、引数として渡せるのはASTとして表現可能なコードのみなので、そういう意味ではなんでも渡せるC++のマクロもやはり強力。Nimはコンパイル時に外部プロセスを実行してその出力を受け取ることができるので、コンパイル時処理に関してはもうなんでもあり感がある。
C++ | Nim | 備考 | |
---|---|---|---|
ソースコードのテキスト置換 | あり | なし | 【Nim】マクロやテンプレートはテキストの操作ではなくASTの操作 【C++】プリプロセッサを使用する |
ソースコードのAST置換 | なし | あり | 【Nim】テンプレートを使う |
コンパイル時計算によるAST構築 | なし | あり | 【Nim】マクロを使う |
ソースコードへのファイル展開 | あり | あり | 【Nim】include文を使う 【C++】#includeを使う |
コンパイル時計算(定数式) | あり | あり | 【Nim】普通に記述可能。ただしコンパイル時計算できなければエラーになる。staticブロックや関数にcompileTimeプラグマを指定することでコンパイル時のみの処理を明示的に指定することが可能。 【C++】関数や変数にconstexprを付ける(C++11以降の機能) |
コンパイル時計算でのファイルロード | なし | あり | 【Nim】staticRead関数を使う |
コンパイル時計算での外部プロセス起動 | なし | あり | 【Nim】staticExec関数を使う |
その他
C++ | Nim | 備考 | |
---|---|---|---|
ユニコード対応 | なし | なし (サポートモジュールはある) | 【Nim】文字列はバイト列として扱われるので、utf-8ならそのまま扱うことが可能だが、文字数などは正しく取得することができない。ただし、unicodeモジュールにはutf-8文字列の文字数を取得する関数などが用意されいる。Nimはソースコードの文字コードがutf-8であることを前提としているので文字列リテラルとしてそのままutf-8の文字を使用することは可能。 【C++】Nimと同じく、std::string,strlenなどは文字列をバイト列とみなすのでutf-8文字列の場合正しい文字数は取得できない。文字列リテラルにu8プレフィックスを付けることでutf-8文字列として扱うことが可能。ただしu8プレフィックスはC++11以降の機能。 |
static assert | あり | あり | 【Nim】static: assert( 条件 ) 【C++】static_assert( 条件, メッセージ ); C++11以降 |
現在のファイル名や行数を取得 | あり | あり | 【Nim】instantiationInfo関数を使う。ただしtemplate内でしか使えない(ラップすればよい) 【C++】__FILE__、__LINE__マクロを使う |
*1:資源が乏しい組み込み系などではNim採用が難しい、あるいは不可能な場合もあるかもれませんが…
*2:第一引数.関数名(第二引数..)という形で関数を呼ぶ構文
*3:Nimは仮引数を変更することができない。そして、object型などの非プリミティブ型は参照(ポインタ)で渡されるため、実質的にC++でいうconst参照渡しで引数が渡される。
*4:ref型の場合、ref型のconst参照渡しとなるため、そのref型の参照先については変更可能となってしまう。C++でのポインタ(非const)のconst参照渡しではポインタの指す先は変更可能なのと同じ原理。
*5:ある関数がどのクラスに属するべきなのかという問題や、クラス実装者以外はメンバ関数を追加できないという問題
*6:関数の実引数からテンプレート引数を推定してくれる機能