学校では習わないコーディングスキル:オブジェクト所有権

プログラマとして身に着けるべきスキルはたくさんありますが、中には、ソフトウェアエンジニアリングの標準カリキュラムに組み込まれていないものもあります。そうしたスキルは少しずつ自然に、あるいは経験豊富な人と一緒に仕事をする中で学ぶ必要があります。1つDavid MacIverが取り上げているのは、値の型を追跡するスキルです。

他には、コード中のオブジェクト所有権を理解するスキルも必要です。つまり、コードのどの部分がメモリ内の特定オブジェクトを所有し、それがどんなアクセスを予期しているかを知るということです。その理解なしにコードを書くと、プログラムがクラッシュしたり厄介なバグに悩まされたりすることになるかもしれません。さらに悪いことに、プログラミング言語の中には、この問題に役立つ手段さえ提供してくれないものもあるでしょう。

自然に身に付ける

これは、私がこのスキルを学んだ方法です。私は大学生の時に一度、Cで赤黒木を実装しなければならないことがありました。C言語入門の講義はほとんどサボりましたが、それでも満点の成績でしたので、学校の中で言えば私はCでのコーディングを知っていました。

しかし現実には、自分が何をしているのか全く分かっていませんでした。実際に使えるツリーの実装を書くことができなかったのです。書いたコードはセグメンテーション違反を起こしてばかりで、私は構造をまっすぐに保つことができませんでした。結局は、中途半端な解決しかできず、100点中60点を取るのがやっとでした。

次の5年間はPythonコードを書き、それからCとC++を書く仕事に就きました。最初のプロジェクトで毎晩クラッシュが起きるなど、確かにミスもしました(私のメールマガジンではそのような話をご紹介しています)。ですが、何年もCやC++をまともに書いていなかったにもかかわらず、実際に使えるコードを書くことができました。

何が変わったのでしょうか?

私が学んだ鍵となるスキルの1つは、オブジェクト所有権だと思います。これは、私がPythonで様々な並列処理を書いていたこと、さらにオブジェクト所有権に関してC++がCより優れたモデルを持っていることから得られたスキルです。その時代に私が何を学んだか、ご紹介していきたいと思います。

メモリ解放のためのオブジェクト所有権

以下のC関数をご覧ください。

char* do_something(char* input);

ある人はinputの割り当てを解放し、またある人はdo_something()から返された結果の割り当てを解放しなくてはなりません。でも誰が? もし2つの異なる関数が同じ割り当てを解放しようとすれば、プログラムのメモリは破壊されるでしょう。もし誰もメモリを解放しなければ、プログラムはメモリリークを引き起こすでしょう。

ここで登場するのがオブジェクト所有権です。必ず、各割り当ての所有者がただ1人となり、その所有者だけが解放を行うようにするのです。GNOMEプロジェクトの開発者向けドキュメントでは、GNOMEのコードベースでそれを実現する仕組みが説明されています

各割り当ての所有者は1人だけです。この所有者は、所有権を別のコードに移すことで、プログラムの実行時に変わる場合もあります。各変数の所有者の有無は、変数のスコープが常にその所有者となるかどうかによって決まります。各関数のパラメータと戻り型は、渡される値の所有権を移すか移さないかのどちらかです。(中略)どの変数が所有されているかを静的に計算することで、メモリ管理は単純なタスクになります。つまり、スコープを離れる前に所有されている変数を無条件に解放し、所有されていない変数は解放しないということです。

GNOMEには、上記を実現するためのライブラリや規約、ルールが一式そろっていますが、これはC言語に所有権を扱うための手段があまり組み込まれていないためです。

他方のC++には、所有権管理だけを目的とした広範なユーティリティが備わっています。例えば、ある割り当てをshared_ptrオブジェクトでラップすることができます。このオブジェクトは、コピーされる度にカウンタを1増やし、解放される度にカウンタを1減らします。カウンタがゼロになると、ラップされた割り当ては解放されます。すなわち、割り当て解放の目的で所有権を追跡する必要がないということです。shared_ptrこそが所有者であり、適切な時期に割り当て解放を行うのです。

JavaやPythonのようなガベージコレクションを備えた言語を使えば、さらに簡単です。言語のランタイムが全て引き受けてくれるからです。メモリ解放の目的で所有権を追跡する必要は全くありません。

オブジェクトアクセス権

メモリ割り当てが言語のランタイムで処理される場合でも、オブジェクト所有権について考える理由はやはり存在します。特に、可変性、つまりオブジェクトのコンテンツ変更の問題があります。メモリ解放は可変性の究極形ですが、普通の可変性がプログラムを壊してしまうこともあるのです。

以下のPythonプログラムで考えてみましょう。

words = ["hello", "there", "another"]
counts = wordcount(words)
print(words)

何が表示されると思いますか? 大抵の人は、["hello", "there", "another"]と予想するでしょうが、実はそうでない場合もあります。もしwordcount()が以下のように実装されていれば、[]と表示されるかもしれないのです。

def wordcount(words):
    result = Counter()
    while words:
        word = words.pop()
        result[word] += 1
    return result

この実装では、wordcount()は与えられたリストを変更しています。ここでオブジェクト所有者の概念を思い出すと分かりやすくなります。各オブジェクトはスコープに所有されていますが、そのスコープは関数にオブジェクトを渡す際、オブジェクトへの書き込み権を与えたくないかもしれないのです。

あいにくPythonやJavaなどでは、関数が入力を変更するかどうかや、関数のパラメータが変更可能かどうかを呼び出し元が見分ける真の方法はありません。したがって、いつそれが起こり、いつそうするのが安全かについての規約や想定を学ぶ必要があります。つまり、オブジェクトの所有権とアクセス権のメンタルモデルを構築するのです。Pythonプログラマの大半は、wordcount()が入力を変更することを予期しないのではないかと思います。オブジェクトの所有権とアクセスについて私たちが持っているメンタルモデルに反しているからです。

プライベート属性の概念(Javaでは明示的、Pythonでは規約上)は、アクセス権が制御される1つの方法ですが、あらゆるケースでこの問題を解決できるわけではありません。規約が役に立たず確信がない場合は、誰がオブジェクトを変更できるか知るため、APIドキュメントや、時には実装さえ参照しなければなりません。これはCプログラマがメモリ割り当てを扱う方法に似ています。

面白いことにCとC++には、多くの場合にこの問題を解決できるメカニズムがあります。constです。以下の要領で、引数がconst、すなわち変更不可能であると定義することができます。

std::map<string,int> wordcount(const vector<string> &words);

プログラマが引数を変更しようとすれば、コンパイラがエラーを出してそれを阻止するのです。

他のアプローチ:Rustと関数型言語

オブジェクト所有権管理に関するC++の機能は、constの外側で、時とともにライブラリコードとして発展しました。Rust言語は対照的に、オブジェクト所有権をコンパイラ自体の機能として提供しています。これは、メモリ割り当てを目的とする所有権にだけでなく、可変性を目的とする所有権にも当てはまります。Rustはこの機能をCとC++の機能に加えて提供しようとしており、特にメモリ割り当ての制御に注力しています。

Rustのコードではプログラマがオブジェクト所有権について明示的な判断をする必要がありますが、関数型言語は異なったアプローチを取っています。HaskellやClojureのような関数型言語では、オブジェクトは不変で変えることができないので、wordcount関数を呼び出しても渡す引数は変更されないと分かっています。オブジェクトが変更不可能なら、所有者は誰でも構わないのです。オブジェクトはどこでも変わらないのですから。

可変性制御のためにオブジェクト所有権を頭の中やコード内で追跡することは、オブジェクトを不変にすれば必要なくなります。これとガベージコレクションを組み合わせることで、純粋関数型のコードを書く際はオブジェクト所有権について考える時間がはるかに短く済むのです。

まとめ:知っておくべきこと

どのプログラミング言語でコーディングしているかによって、オブジェクト所有権に関して学ぶべき思考モデルは異なってきます。

  • 関数型のコードを書いているなら、このことにあまり時間を割く必要はありません。
  • Java/Ruby/Python/JS/Goで書いているなら、オブジェクト所有権について考える必要があります。「オブジェクトを変更できるのは誰か?」、「関数はプログラマの予期に反してオブジェクトを変更するか?」といった具合に可変性が関係してくるからです。並行処理のコードを書いている場合は、このことはずっと重要になります。規約ではもはや間に合わず、アクセスはロックを用いて明示的に制御される必要があります。
  • Rustで書いているなら、コンパイラがオブジェクト所有権に関する広範な明示的アノテーションを理解し、安全なインタラクションを強制してくれます。
  • C++で書いているなら、可変性制御はある程度constに、自動的なメモリ解放はライブラリコードに頼ることができます。
  • Cで書いているなら、可変性制御はある程度constに頼れますが、それ以外はプログラマが行う必要があります。

今度コードを書く時は、それぞれのオブジェクトを誰が所有しているか、そして他のコードにオブジェクトを渡す際に所有者がどんな保証を予期するか考えましょう。次第に、所有権の仕組みについてより良いメンタルモデルを構築できるようになるはずです。

そして非関数型言語で書いている場合は、不変な(「永続的な」とも表現されます)データ構造を使うことを検討しましょう。可変性の及ぶスコープを単純に狭めれば、オブジェクト所有権を追跡する必要性も減るので、関数型言語の恩恵の多くを享受できます。