プログラミング比較文化論試論 進化の方向性にみるC++とObjective-Cの違い

プログラミングの世界では、サピア=ウォーフの仮説がささやかれるほど、プログラミング言語の違いがプログラミングのスタイルや設計に影響を及ぼす現象が見られる。本稿ではC++Objective-Cを取り上げ、言語の進化の方向性がプログラムの実装に及ぼした影響を探る。

C++Objective-Cは、共にC言語を祖先とし、かつ「C言語オブジェクト指向プログラミング機能を追加する」というほぼ同じ目的*1をもって進化の歩みを始めた言語である。しかしながら、進化の初期の段階で下されたデザインの違いにより、C++Objective-Cは(特に現在の姿であるC++11とObjective-C 2.0は)非常に異なる言語となった。この言語デザインの違いが、プログラミングにどのような差異をもたらしているのだろうか?

出発点:C言語

C++Objective-Cを比較するにあたり、共通の先祖であるC言語を押さえておくことは重要だ。本稿ではargsという「引数を全て表示するだけの単純なコンソールアプリケーション」の実装を題材として用いる。C言語(C89)での実装例は次の通り。

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    int i;

    for (i = 1; i < argc; ++i) {
        (void) puts(argv[i]);
    }

    return EXIT_SUCCESS;
}

C言語にはアプリケーション・メインエントリの概念がある。ホスト環境においては、関数mainがプログラムの実行開始位置として用いられる。関数mainの引数argcにてプログラム名を含むコマンド引数の数が、引数argvにてプログラム名を含む各コマンド引数の文字列を指し示すポインタの配列が引き渡される。

言語機能が比較的シンプルであるC言語では、for文を使用して配列argvの各要素を走査し、スクリーンに出力する関数を実行することになる。

C++:Cの派生、Cの改良

C++は、C言語オブジェクト指向プログラミングの機能を追加するにあたり、積極的にC言語の文法・言語機能を拡張する戦略を採用した。classなどのキーワード、try - catchのような制御構文、テンプレート機能などは、C言語には見られない言語機能だ。また標準ライブラリにおいても、std:stringやSTLを始めとする機能が追加された。

C++のオブジェクト機能は静的束縛を採用しているが、これ自体もC言語との親和性が強いものである。

新たな機能を積極的に使用することで、C++でのプログラミングは、C言語でのそれを土台としつつも全く異なるものになる。次に挙げるには、C++11でビルド可能なargsの実装である。

#include <algorithm>
#include <iostream>

int main(int argc, char *argv[])
{
    std::for_each(&argv[1], &argv[argc], [] (char *arg) {
        std::cout << arg << std::endl;
    });
}

argcやargv、ポインタはC言語そのままに、ラムダ式やstd::for_eachを使用することでループの抽象化を実現し、for文や配列走査用の一時変数を取り除いている。

C++は、C言語の機能と新たに追加された機能が混在した言語だ。その影響で、C++のプログラミングもC言語的側面とC++的側面が混在する傾向にある(混在の割合には差異がある)。C++的側面を強く押し出すことでC言語でのプログラミングとは大きく異なるスタイルとなるが、しかしそこにC言語的側面を混入することが可能であるし、混入した時にごく自然に両者が溶け込む傾向にある。

例えばC++では参照やイテレータを積極的に使用することでポインタを極力排除することが可能であると同時に、ポインタの使用を前提とした既存の機能をそのまま混ぜ込むことも可能である。実際、前述のargsのコードにおいても、std::for_eachやstd::coutのようなC++的機能に対して、ポインタargvやargのようなC言語的機能を、何の変換もせずに混ぜて使用している。

また「Better C」のように、C言語的側面を重視しつつ、所々で部分的にC++的側面を混入するスタイルも可能である。

#include <iostream>

int main(int argc, char *argv[])
{
    for (int i = 1; i < argc; ++i) {
        std::cout << argv[i] << std::endl;
    }
}

Objective-C:C + Smalltalk、ハイブリッド言語

C++とは異なり、Objective-CではC言語の言語本体を拡張するのではなく、Smalltalk的な言語を抱き合わせる戦略をとった。Objective-Cは、実質的には2つの言語を抱えたハイブリッド言語である。

Objective-CC言語的機能は、祖先であるC言語から変化していない。あえて言えば、現在ではC99にGCCの独自拡張機能を加えたGNU99が用いられることが多い。Objective-Cによるargsのシンプルな実装は、次のようになる。

#include <stdio.h>

int main(int argc, char *argv[]) {
  for (int i = 1; i < argc; i++) {
    (void) puts(argv[i]);
  }
}

関数mainの引数argc、argvはC言語由来のものだ。Objective-Cにおいて、C言語的なものをそのまま取り扱うには、C言語的機能を使用すると都合がよい。先ほど述べたように、Objective-CC言語的機能は元のC言語に近いため、C言語的機能を全面的に採用したコードは、C言語のコードと近いスタイルとなる(本稿におけるC言語版argsとの差異は、C89とC99の違いにもとづくものにすぎない)。

Objective-CC言語的ではない機能――仮に本稿ではSmalltalk的機能と呼ぶことにする――には、C言語的機能とは相容れない部分がある。Smalltalk的機能の世界は、基本的に「全てがオブジェクトであり、オブジェクト同士のメッセージングによって動作する」というものだ。しかしC言語的なオブジェクト(intなどの基本データ型)はSmalltalk的なオブジェクトではないため、メッセージを送ることも受けることもできない。

例えば、Objective-C 2.0での開発で多用されるFoundation frameworkには、NSArrayやNSMutableArrayなどのコレクション・クラスがあるが、これらはSmalltalk的オブジェクトを扱うことは可能だが、C言語的オブジェクトを扱うことができない*2。次に示すargsの実装のように、C言語的オブジェクトからSmalltalk的オブジェクトに変換する必要がある。

#import <Foundation/Foundation.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
  @autoreleasepool {
    NSMutableArray *args = [NSMutableArray arrayWithCapacity:(NSUInteger) argc - 1];

    for (int i = 1; i < argc; i++) {
      [args addObject:[NSString stringWithUTF8String:argv[i]]];
    }
    [args enumerateObjectsUsingBlock:^(NSString *arg, NSUInteger idx, BOOL *stop) {
      (void) puts([arg UTF8String]);
    }];
  }
}

argv経由で取得可能なC言語の文字列を、NSStringクラスのオブジェクトに変換した上でNSMutableArrayのコレクションに追加している。またC言語由来の関数putsを使う際に、NSStringからC言語の文字列への変換を行っている。

いったんSmalltalk的機能の世界に入ってしまえば、後はオブジェクトにメッセージを送るのみだ。あるオブジェクトにメッセージを送信した結果としてオブジェクトが返されるのなら、メソッドチェーンが可能だ。

#import <Foundation/Foundation.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
  @autoreleasepool {
    NSString *nsargv[argc];

    for (int i = 0; i < argc; i++) {
      nsargv[i] = [NSString stringWithUTF8String:argv[i]];
    }

    [[NSArray arrayWithObjects:&nsargv[1] count:argc - 1]
      enumerateObjectsUsingBlock:^(NSString *arg, NSUInteger idx, BOOL *stop) {
        (void) puts([arg UTF8String]);
      }];
  }
}

Objective-Cではクラスもオブジェクトである。上記コードの後半部は、次のように解釈することが可能だ。

  1. オブジェクトNSArrayにメッセージarrayWithObjects:count:を送信する。
  2. その結果としてNSArrayクラスのオブジェクト(コレクション)が返される。
  3. NSArrayクラスのオブジェクトにメッセージenumerateObjectsUsingBlock:を送信する。

この構造は非常にSmalltalk的であり、コードの冗長さを除けば、例えばGNU Smalltalkによるargsの実装の構造と良く似ている。

Smalltalk arguments do: [:x | x displayNl].
  1. SystemDictionaryクラスのオブジェクトSmalltalkにメッセージargumentsを送信する。
  2. その結果としてArrayクラスのオブジェクト(コレクション)が返される。
  3. Arrayクラスのオブジェクトにメッセージdo:を送信する。

Objective-Cでは、C言語的機能とSmalltalk的機能が分離する傾向にある。何らかの変換を行うことで、どちらか一方の機能を主に用いるようにすることが多い、では、どちらを用いるかといえば、Smalltalk的機能だろう。

Foundation frameworkのようなクラスライブラリは、「クラス」の名前が示すように、オブジェクト指向プログラミングの機能を前提に構成されている。Objective-Cにおいて、オブジェクト指向プログラミングの部分はSmalltalk的機能が担っている。クラスライブラリを用いて効率的に開発を進めたいなら、Smalltalk的機能をメインに用いなくてはならない。

まとめ

C++Objective-Cは、C言語オブジェクト指向プログラミング機能を追加するところから始まった言語だが、初期段階に下された方向性の差異により、現在は全く異なる言語となっている。

C++は、C言語の文法・言語機能を積極的に拡張し、標準ライブラリに新たな機能を追加する戦略をとった。C++では、C言語的側面とC++的側面を持ちつつも、両者が混在している。そのため、プログラミングにおいてもC言語的側面とC++的側面が混在する傾向にある。従来のC言語でのスタイルと、新たなC++的スタイルのどちらでもプログラミングが可能であるが、同時に両者を混在して用いることが可能となっている。プログラミングにおいては、C++的スタイルを全面的に押し出しつつもC言語的スタイルを必要に応じて混入することがあれば、「Better C」のようにC言語的スタイルを全面的に押し出しつつもC++的スタイルを必要に応じて混入することもある。

Objective-Cでは、C言語本体を拡張するのではなく、Smalltalk的な言語を抱き合わせるハイブリッド戦略がとられた。Objective-Cは、C言語的側面とSmalltalk的側面を持つが、C++とは異なり両者は分離する傾向にある。プログラミングにおいては、C言語的機能をSmalltalk的機能に変換した上で、Smalltalk的機能を主に用いて開発を行い、様々なメリットを享受するようにすることが多い。

*1:実際には、オブジェクト指向プログラミング機能といえども、C++は静的束縛を、Objective-Cは動的束縛を選択した、という差異がある。

*2:Objective-C 2.0のオブジェクトでは、参照カウンタによるメモリ管理が行われている。NSArrayやNSMutableArrayのオブジェクトを解放する際、コレクションしている個々のオブジェクトの参照カウンタを-1するためにreleaseが送信されるのだが、C言語のオブジェクトはreleaseを受信することができない。