KOTET'S PERSONAL BLOG

#dlang DとCのインターフェース:配列 Part 1【翻訳】

Created: , Last modified:
#dlang #tech #translation #d_blog #d_and_c #advent_calendar #cpplang

これは1年以上前の記事です

ここに書かれている情報、見解は現在のものとは異なっている場合があります。

これは Interfacing D with C: Arrays Part 1 – The D Blog許可を得て 翻訳した D言語 Advent Calendar 2018 - Qiita 15日目の記事です。

誤訳等あれば気軽に Pull requestを投げてください


この投稿はDとCとのインターフェーシングにおいて潜む問題と、 その回避法についてのシリーズの記事です (訳注:翻訳版はこちら)。

CのAPIとのインターフェーシングにおいて特に問題として立ちはだかるのは配列でしょう (文字列も同列に並ぶものかもしれません。これは”D and C”シリーズの将来の記事の話題となります)。 Dの配列はCと直接の互換を持つような方式で実装されているわけではありませんが、基本的には同じです。 このため、両者の違いを把握している限り互換性に問題は生じません。 この記事ではまずその違いを見ていきましょう。

CのAPIをDから使う際は、コードをCからDに翻訳しなければならないことがあります。 新しいDのプログラムはCのAPIの既存の使用例を利用できて、 CのAPIを使うCのプログラムからのDへの移植は既存のCのコードから多くを書き換えることなく可能です。 両者の宣言と初期化構文、その翻訳方法がその根拠です。 このシリーズの後の記事では多次元配列、Dの配列の内部構造、 Cの関数との配列のやり取り、GCがどのように働くかを取り扱います。

このトピックを取り扱う際触れる予定だった範囲は最初非常に狭いものでした。 それは私が読者はCの基礎を十分に理解しており、何がなぜどのようになるかを推測できると仮定しており、 退屈な詳細の説明を取り去ったためでした。 Cの経験者が1人しかいないグループに対してDのチュートリアルプレゼンテーションを行う前の話です。 Dフォーラムの一般ユーザー のなかにCのコードに触れたことのない人がいることにも気づきました。 そのため、私は当初よりも基本的なところからカバーしていくことにしました (そのため2パートだった記事は3本以上に膨れ上がっています)。 知識が古い人も流し読みして満足しないようにすることをおすすめします! Cで快適に過ごしてきた経験は、私が説明していく落とし穴を紛らす役には立ちません。

配列の宣言

1次元配列の単純な宣言から見ていきましょう。

int c0[3];

この宣言はintの値3つを保持するのに十分なメモリをスタックに確保します。 値はメモリの連続した領域に保存され、それぞれ隣り合っています。 c0が初期化されるかはそれが宣言された場所に依存します。 グローバル変数とstaticローカル変数は、以下のCプログラムで示されるようにデフォルトで0に初期化されます。

definit.c

#include <stdio.h>

// グローバル変数(staticとして宣言することもできます)

int c1[3];

void main(int argc, char** argv)

{

    static int c2[3];       // staticローカル変数

    int c3[3];              // 非staticローカル変数

    printf("one: %i  %i  %i\n", c1[0], c1[1], c1[2]);

    printf("two: %i  %i  %i\n", c2[0], c2[1], c2[2]);

    printf("three: %i  %i  %i\n", c3[0], c3[1], c3[2]);

}

私には、これは以下の出力をしました。

one: 0 0 0

two: 0 0 0

three: -1 8 0

c3の値はちょうどそのメモリ位置にあった値です。 さて、これと等価なDの宣言は以下になります。

int[3] d0;

オンラインで試す

ここに最初の注意点があります。

Dの普遍的な指針として、CのコードをDのソースファイルにコピペしてきた際には、 Cと同じように動作するかコンパイルが失敗するかのどちらかが起きるようになっています。 ながらくCの配列宣言構文は前者にあり、Dの構文中で合法な選択肢でした。 現在は廃止され言語から削除されたために、int d0[3]はコンパイラに怒られるようになりました。

Error: instead of C-style syntax, use D-style int[3] d0

独断的な制約に思えるかもしれませんが、そうではありません。 本質には、いくつかのレベルにおける一貫性があります。

ひとつは、Dにおいて宣言は右から左に読むということです。 d0の宣言のなかで、すべては右から左に読むと、我々が普段言うのと同じ順番に流れます。 “(d0) is an (array of three) (integers)” というように。 Cスタイルの宣言はそうなっていません。

もうひとつはd0の型がint[3]だということです。 以下のようなポインタの宣言を考えてみましょう。

int* p0, p1;

p0p1もその型はint*です(Cでは、p0だけがポインタになります。p1はただのintです)。 これはDにおけるあらゆる型宣言で同じです。 型は左、シンボルは右。 このようなコードを考えてみます。

int d1[3], d2[3];

int[3] d4, d5;

型を不定詞のように分割し、配列宣言に2つの構文を用意することは、 一貫性のなさと混乱を生みかねない状況を生み出します。 Cスタイルの構文を違法にすることで、一貫性が強制されます。 コードの可読性は保守性の重要な要因です。

d0c0のもうひとつの相違点は、 d0の要素はそれがどこでどのように宣言されたかにかかわらずデフォルトで初期化されるということです。 モジュールスコープ、ローカルスコープ、staticローカル……関係ありません。 とくべつコンパイラに指示がなされない限り、Dの変数は常にデフォルトで 各型のinitプロパティ で指定された値に初期化されます。 配列の要素は要素の型のinitプロパティに初期化されます。 偶然にも、int.init == 0です。 definit.cをDに翻訳してみましょう(そしてrun.dlang.io を開き、試してみましょう)。

CからDへの翻訳をする際、このデフォルト初期化関連の物事で少し混乱することがあります。 以下のようなちょっと作為的なコードを考えてみます。

// Cではstaticな変数はデフォルトで0に初期化されます

static float vertex[3];

some_func_that_expects_inited_vert(vertex);

float.init == float.nanであり0.0fではないため、 Dへ直訳してしまうと期待したような結果は得られません!

ふたつの言語間で翻訳をする際には、 明示的に初期化されていないCの変数が初期化されることを期待されていないか、 そしてDの 各型のデフォルト初期値 に気をつけなければなりません。 ここを忘れるとデバッグで禿げ上がることになります。

Dにおいてデフォルトの初期化は宣言に= voidをつけることで簡単に無効化できます。 これは変数が読まれる前にかならず値がロードされるとき、 もしくはinitの値が入っていると未初期化よりも都合が悪いときに活用できます。

float[16] matrix = void;

setIdentity(matrix);

ちなみに、デフォルト初期化の目的は便利なデフォルト値を提供することではなく、 未初期化の値をわかりやすくすることです (将来あなたがデバッグをするとき感謝することになるでしょう)。 よくある勘違いは、floatcharの「非数」(float.nan)や無効なUTF-8(0xFF) などという初期値を奇妙な外れ値だと思うことです。 そうではありません。 これらの値はそれ以外で役に立たないために、未初期化メモリの素晴らしい目印になるのです。 整数型(とbool)はこのパターンを乱しています。 これらの型は、その値のすべての範囲が意味を持つため、普遍的に「おーい!ボクは初期化されてないよ!」 と叫んでくれるような単一の値がありません。 整数やbool変数は、0falseが明示的な初期化の際によく選ばれる値であるため、 しばしばデフォルト初期値のままにされます。 浮動小数点数や文字型は一般に、できるだけ速やかに明示的初期化や代入をしなければなりません。

配列の明示的初期化

Cでは配列の明示的初期化がさまざまな方法でできます。

int ci0[3] = {0, 1, 2};  // [0, 1, 2]

int ci1[3] = {1};        // [1, 0, 0]

int ci2[]  = {0, 1, 2};  // [0, 1, 2]

int ci3[3] = {[2] = 2, [0] = 1}; // [1, 0, 2]

int ci4[]  = {[2] = 2, [0] = 1}; // [1, 0, 2]

ここからわかることは以下のとおりです。

初期化子は配列より長くなることを想定されていません(gccでは警告を出した上で、 たとえば3要素の配列なら初期化子リストの最初の3つを使って初期化して、残りを無視します)

ちなみに指示初期化子とそうでないものとを混ぜて使うこともできます。

// [0, 1, 0, 5, 0, 0, 0, 8, 44]

int ci5[] = {0, 1, [3] = 5, [7] = 8, 44};

指示のない普通の初期化子は通常通り出現順に適用されます。 指示初期化子がその直前にある場合、それは指示初期化子の次のインデックスの値になり、 それ以外の要素は0で初期化されます。 この例で、01はリストの値の最初の2つのため通常通りci5[0]ci5[1]を指します。 次にci5[3]に対する指示初期化子が来るため、このリストにci5[2]に対応する値はなく、0に初期化されます。 次にはci5[7]に対する指示初期化子が来ます。 ci5[4]ci5[5]ci5[6]はスキップされたので、ぜんぶ0に初期化されます。 最後に44は指示がなく、[7]の直後にあるため、この値はci5[8]の値になります。 最終的にci59要素で初期化されます。

指示配列初期化子はC99でCに追加されました。 Cコンパイラのバージョンによってはサポートされていなかったり、 有効化するためにコマンドラインフラグを必要としたりします。 現実のコードで遭遇するようなものではないかもしれませんが、あなたのC言語歴を推し量る役には立ちます。

これらをDに翻訳しようとするとさらに混乱します。 幸いにも最初のひとつはコンパイルエラーになり、ハイゼンバグは未然に防がれます。

int[3] wrong = {0, 1, 2};

int[3] right = [0, 1, 2];

Dにおける配列初期化子は配列リテラルです。 関数に無名配列を渡すときも、writeln([0, 1, 2])のように同じ構文が使えます。 興味深いことに、wrongの宣言は以下のコンパイルエラーを出します。

Error: a struct is not a valid initializer for a int[3]

Dにおいて{}構文は structの初期化に使われますstructのインスタンスの初期化にも使える 構造体リテラルと混同しないでください )。

ci1の翻訳をしたときにも驚くことでしょう。

// int ci1[3] = {1};

int[3] di1 = [1];

これはコンパイラエラーを出します。

Error: mismatched array lengths, 3 and 1

一体どうなっているんでしょう? まず、ci2の翻訳を見てみましょう。

// int ci2[] = {0, 1, 2};

int[] di2 = [0, 1, 2];

Cのコードにおいて、ci1ci2の間に違いはありません。 両方とも固定長で、スタックに確保される3要素の配列です。 これがDにおける、CのコードをDのモジュールにコピペしてくるという経験則が壊れるケースのひとつです。

Cと違い、Dではint[3]int[]の間に違いが存在します。 前者はCのような固定長配列であり、一般にDでは静的配列と呼ばれます。 後者はCと違い可変長配列であり、動的配列やスライスと呼ばれています。 その長さは必要に応じて伸びたり縮んだりします。

静的配列の初期化子は配列と同じ長さでなければなりません。 Dは宣言された長さより短い初期化子を単に禁止しています。 動的配列は初期化子から長さをとります。 di2は3要素で初期化されますが、さらに後から追加することもできます。 さらに、動的配列に初期化子は必須ではありません。 Cにおいては、配列の長さの宣言が省略できるのは初期化子がある場合のみなので、int foo[]は違法です。

// gccは"error: array size missing in 'illegalC'"と出力します

// int illegalC[]

int[] legalD;

legalD ~= 10;

legalDは空の配列であり、その要素のためのメモリは確保されていません。 要素は追加演算子~=で追加できます。

di2のように明示的な初期化子がある場合、動的配列のメモリは宣言がされた時点で確保されます。 初期化子がない場合、メモリは最初の要素が追加された時点で確保されます。 デフォルトでは、動的配列のメモリはGCヒープに確保され (ただし最適化の結果安全にスタックに確保できるとコンパイラが判断することもあります)、 その後の割り当てをへらすために要素数に対して必要なメモリよりも多くのメモリが初期化されます (要素の初期化をせずに大きいブロックを一度に確保するのに reserve関数が使えます)。 追加された要素は事前に確保されたスロットがなくなるまでそこに配置され、 その次の追加が新しい領域の確保を引き起こします。 Steven Schveighofferの配列に関する素晴らしい記事 ではさらに詳細に触れられており、次のパートで扱う配列の機能についても説明しています (訳注:翻訳版はこちら)。

多くの場合、固定長のスタックに確保されるCの配列と可変長のGCで確保されるDの配列の違いは、 ci2のような宣言をDに翻訳する時は大きな問題になりません。 問題になるのはDの配列が@nogc付きの関数のなかで宣言されたときです。

@nogc void main()

{

    int[] di2 = [0, 1, 2];

}

オンラインで試す

コンパイラはズルを許しません。

Error: array literal in @nogc function D main may cause a GC allocation

配列がstaticのときは、リテラルの要素はその場で解決されスタックに確保されるため、 同じようなエラーは起きません。 DにやってきたばかりのCプログラマーは最初、 そうしないことがまるで自然に反しているかのようになんにでも@nogcを付けたがる傾向があり、 そのためGCが人類の敵ではない とわかるまで壁にぶち当たり続けることになります(訳注:翻訳版はこちら)。

これを解決するために先程のCの指示初期化子の大きなパラグラフが役立ちます。 Dは指示初期化子を、Cと違う構文でサポートしています。

// [0, 1, 0, 5, 0, 0, 0, 8, 44]

// int ci5[] = {0, 1, [3] = 5, [7] = 8, 44};

int[] di5 = [0, 1, 3:5, 7:8, 44];

int[9] di6 = [0, 1, 3:5, 7:8, 44];

オンラインで試す

これは静的、動的の両方で動作し、Cと同じルールに準じて、Cと同じ初期値になります。

このセクションで主に覚えておいてほしいことは次のとおりです。

次回に続く

当初の予定より配列の宣言と初期化についてたくさん書くことになりました。 そしてまだまだ配列について書くことはたくさんあります。 次の記事では、多次元配列、Dの配列の内部構造、 そして人々が「Cの配列はただのポインタでしかない」という時意味していることについて見ていきます。 最後の2つは、DとCの配列を言語間でやり取りする技術について触れるパート3への伏線になっています。