Flat Leon Works

アプリやゲームを作ってます。

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備考
voidvoid
真偽boolboolC++、Nimともに真偽値は、true, false を使用
整数intint整数型リスト
【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
浮動小数点数floatfloat浮動小数点数型リスト
【Nim】float32,float64
C++】float,double
文字charchar
文字列(低レベル)char*cstring
文字列std::stringstring
固定長配列[](配列)array
動的配列std::vectorseq
リストstd::listSinglyLinkedList
DoublyLinkedList
【Nim】listsモジュールで提供
列挙型enumenum【Nim】enum値の文字列表現が言語の機能として備わっている。これはとても嬉しい。
構造体/クラスstruct/classobject【Nim】何も継承していないobjectはC++でいう構造体のようにすべてのメンバ変数が公開される
タプルstd::tupletupleC++C++11以降
セット(集合)std::setset【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備考
モジュール化宣言と実装をそれぞれ別ファイルにする別ファイルにする
モジュールのインポート#includeimport文
単純な別ファイルの取り込み#includeinclude文

こうして表にしてみるとたいしたことないように見えるが、宣言と実装を別ファイルにする必要がないというのは大きなメリット。

名前空間

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:関数の実引数からテンプレート引数を推定してくれる機能