Previous Page | Next Page

C言語のポインタを再考する(Part1)

- ポインタとはアドレスではなく、C言語がプログラマに公開した間接性のモデルである -


C言語を学ぶ上で「ポインタ」というのは昔からの鬼門らしい。というのは私はポインタで躓いたことがないからで、もちろん最初から全てを完全にマスターしたわけではないですが、それでも『「ポインタ分からない」が分からない』という話になります。
したがって自分のブログでポインタについての説明をしませんでした。要するに私自身「どういうふうに説明したらよいか」分かりませんでした。


一方で、とあるSNSのC言語のポインタの説明記事の


『実用上は「ポインタ=アドレス」で構いません。「XXのアドレス」を「XXへのポインタ」と置き換えても ( あるいはその逆でも ) なんら支障が出ないからです。』


というコメントを読んだ時に「ポインタが難しい」ということの底深さは少し理解しました。


良い子のみんなは真似して「ポインタ=アドレス」と言わないでねと言いたいのですが、
それだけではブログにする意味がないかと思うので、なぜ誤解なのかを説明します。
まず、言葉の意味ですが、
pointer:指し示すもの
address:住所
です。
つまり「ポインタとは道標のようなもので、アドレスとはその場所そのものの情報」ということになります。
この観点に立つと、ポインタとアドレスの違いが分かるかと思います。「ポインタはアドレスを保持する」という認識は間違っていませんが、「ポインタはアドレス」というと重要な見落としが出てきます。


C言語やアセンブリ言語に精通した人なら『”ポインタ=アドレス”ではないという」という主張には同意してもらえるかと思いますし、K&R第2版の巻末には「Is a pointer always the same as an address?」という有名な問題があります。私自身はあたりまえの認識だとおもっていたのですが、この「ポインタ=アドレス」を主張する人が日本でも海外でも一定数いらっしゃるようです。また、それなりにキャリアを重ねた人達もいらっしゃるようで、つまり「ポインタ=アドレス」という主張が市民権を得つつあるようです。


C言語が出現して50年が経ちます。私がC言語を学んだのは約37年前になります。その頃がちょうどC言語の全盛ということでした。おそらくですが、多くの人達がポインタについて十分な知識を持たないでプログラミングをしていたでしょう。ポインタに対する理解が不十分な人達は時にプログラムをキチンと動作させることができませんでした。C言語と現代の言語を例えるのならMT車とAT車ぐらいの違いがあります。つまりC言語のポインタは正しく使わないとエンストするのですが、今の言語はAT車のようにエンストすることなく、上手にポインタを隠して安全に使えるようにしています。
つまり現在の言語を主言語としている人達がC言語を横目でみると「ポインタ=アドレス」という勘違いをしても無理からぬ面もあります。しかしながら「ポインタ=アドレス」という主張はある意味でポインタの意味論を考えたときには後退した考えになります。ここら辺で一度ポインタについて再考するのが良いかと思います。


ポインタを理解する


前おきはこのくらいにしてポインタについて説明します。当初は初心者用の記事と思ったのですが、「ポインタの意義」を強調するあまり、細かな文法についての説明が完全・完璧ではないですので本当の初心者の方がこれを読んでも「?」となるだけかもしれません。解らないところはAIに聞いてもらえればよいですし、一度C言語の文法の解説を一読されてからこのブログを読まれた方が良いかもしれません。


C言語のポインタは様々な機能を実現するツールですが、ここでは
・変数の変数化
・配列の操作
について説明します。実は他にも重要な機能(関数ポインタ等)があるのですがややこしくなるので重要なこの2点に絞ります。


C言語ですが、システム記述用言語ともいわれることがあります。実は多くのプログラミング言語がC言語で書かれています。システムプログラムにおいてはポインタは中心的が技術といっても過言ではありません。実は多くのプログラミング言語の機能も内部ではポインタが使われています。ポインタとはいわば縁の下の力持ちといったところです。別の方向から見るとポインタは機械語から備わっており、プログラミングの基本とも言えますが、C言語ではそのポインタを上手く抽象化しています。


それぞれ、見ていきます。


変数の変数化(間接参照)


変数というのはある種の抽象化の機能ともいえますが、プログラムを汎用的なものにする道具でその必要性は言うまでもないでしょう。
一方で変数の変数化とは、もう一段高い、抽象化とも言えます。
つまり、
 int *v;
とすれば、


 v = &a;


としたり、


 v = &b;


としたりできます。これによりvはaやbと(変数の変数として)関連付けられます。
vが関連付けている変数(aやb)にアクセスしようとすると、
 *v
とします。*記号は&記号の反対の操作を行うことになります。つまり
 printf("%d", *v)
のように*vとするとaまたはbの値を取り出すことができます。
また、
 *v = 1
とすると、変数aまたはbの値が1になります。



『これの何が便利なのか?』


と質問されそうですが、「変数の変数」と言われると、まずは「どこかで使えそう」という認識でよいかと思います。


以上、ポインタについて細部に入らないで利用目的の説明を行うと、「実体はaかもしれないしbかもしれないが値を操作したい時のように、複数の場合分けして扱いたいときは変数の変数化(ポインタを用いた段階的な参照(間接参照))を行うと便利ということが言えます。


具体例をだしますと関数の定義において2つの値を返そうとするとポインタを使う必要が出てきます。 整数除算を行う関数を考えます。a / b の商(d)と余り(r)を返す関数を考えますと例えば以下のようになります。
この例が『なぜ場合分けなのか?』と言われそうですが、つまりmydiv関数にとってはdが指す実態はaかもしれないしbかもしれないさらに別の変数かもしれないということです。ちなみに話は脱線しますが、関数の引数というのはある種の「変数の変数化」が行われるところですが、これ以上は話がややこしくなるので詳しくは次回にします。


int mydiv( int a, int b, int *d, int *r)
{
    if ( b == 0 ) return 0;
    *d = a / b;
    *r = a % b;
    return 1;
}

関数の戻り値で1は成功、0はゼロ割を表します。ちなみに「Cにはライブラリにdiv関数があり、しかも構造体を使って値を返しているからこの関数は無意味」とおしかりを受けそうですが、そのおしかりはそのとおりですが、私的にはたまにこういう関数を作りたくなる場合があるのも事実です。
この関数(mydivとdiv)の例は「C言語で複数の値を返すポインタを使った例」となりますが、「構造体を使えば複数の値を返せる」というのもそのとおりです。ちなみに構造体を返すのは潜在的に効率が悪くなる可能性があります。64ビット時代ではdiv関数の戻り値ぐらいですと逆にパフォーマンス上推奨される場合がありますが、構造体のサイズが大きくなるとパフォーマンスの問題が発生することを頭の片隅に置いておけば将来困らないかもしれません。


ポインタに限った話ではないですが、プログラミングの難しさの1つが「やり方が複数出てくる」ということがしばしば起こります。どちらのやり方を選ぶのか?というのは悩ましい問題となりますが、どちらを選ぶかはある意味設計の範疇になるでしょう。またこの例では必ずしもポインタを使うことが良いとは限らない例としても出しています。


話は変わって、


『「変数の変数」があるのなら「変数の変数の変数」はないのか?』


と質問されそうですが、ポインタを使えばそれも可能です。変数の変数の変数は
  int **p;
と定義できるでしょう。これで、
  p = &v;
として、
  *p
とするとvを取り出すことができます。vは先ほどの例では、aまたはbの値を取り出すことができます。 pと間接参照を2回すれば、一気に先ほどの a または b の値を取り出すことができます。 pのことを「ポインタのポインタ」とよばれることもあります。要は*が2回出てくるのでそのように呼ばれますが、「ポインタ=アドレス」というとこの「ポインタのポインタ」を「アドレスのアドレス」という風に解釈することになります。私が「ポインタはアドレスでない」とする根拠の1つがこのポインタの機能になります。


少し話が高度になりますが、ポインタに似た機能で「参照」というものがあります。参照も「変数の変数化」を行うものですが、「変数の別名」の側面が強くなります。また、多くの言語(C言語は参照はない)でサポートされている参照は「変数の変数の変数」といった数珠つなぎ的な操作は直接は行えないです。またオブジェクト指向(肥大化した構造体をポインタで扱う)との関係で、この参照をポインタという風に認識する人達もいます(実際に一部の言語ではポインタと呼ぶ)。このような経緯から「ポインタ=アドレス」という認識が出てきたようです。


最後になりますが、変数の変数化という言葉はあまり一般的でないので、あくまでも「学習のエッセンス」として覚えていただければと思います。私自身もうあまり使わないかもしれません。
ただAIによると、C言語の解説において、「ポインタはアドレスです」という説明の次によく出てくるのが、「ポインタは、(別の)変数を指し示す変数である」という表現です。
英語圏のチュートリアルでも "A pointer is a variable that stores the memory address of another variable" という表現が頻出します。


値のコピーと変数の変数化の違い


 ここで補足を行いたいのですが、ポインタが難しいところの別の理由になりますが、コピーとポインタの機能的な共通点が挙げられます。


例を出します。


int型の変数、adultprice, childprice, ageがあったとします。それぞれ大人料金、子供料金、年齢としましょう。
規則として13歳以上(12歳より上)が大人料金とします。また、actualpriceに適用料金を入れることにします。


素直にコードを書けば


/* コピー */
if ( age > 12 ) {
    actualprice = adultprice;
} else {
    actualprice = childprice;
}

となるでしょう。つまりactualpriceは、adultpriceかchildpriceのどちらかがコピーされます。
一方で、同じことをポインタを使ってみましょう。


/* ポインタ */
int *actualprice_p;
if ( age > 12 ) {
    actualprice_p = &adultprice;
} else {
    actualprice_p = &childprice;
}
actualprice = *actualprice_p;

「これでは全く便利になっていない!」というおしかりを受けそうですがごもっともです。これは重要な点で「実は単純に値を変数化したいときは、『変数の変数化』を行うより『コピーを行った方が良い』場合があります(考えてみれば自明ですね)。


先ほどの例では「C言語で複数の値を戻り値とする場合、構造体にする方法もあればポインタを使う方法もある」ということですが、今回の例では『場合分けを行い値を決めたいとき、ポインタを使って複数ある値を一つのように扱える』ということが言えます。実はこの時に「値が変化しないのであればコピーで十分の場合が多い」ということがいえます。


この「コピーとポインタの役割の重なり(ポインタよりもコピーで十分)」が私なりの、ポインタの難しさの理由の1つになります。
ここまでですとポインタは使えないと判断されそうですが、もう少しポインタの利用について便利な例を次の章の最後に示します。


配列へのアクセス


C言語の、「コロンブスの卵」的な設計上のすばらしさの1つが


配列は連続したメモリとして扱う


ということになります。
実は、ほとんどのプログラミング言語でも、内部的に見れば、メモリの連続領域を配列として扱っています。で、多くのプログラミング言語がこの事実を隠す方向にデザインされていますが、C言語はこれを前面に出してきています。ポインタを使うと配列の要素に自由にアクセスできるようになります。


C言語では、配列は以下のように定義します。
 int a[10]; /*要素数10(0~9)のint型の配列を確保(注、値は不定) */
ここで、
 int *p = a;
とすると、*pは配列の0番目の要素を指すようになります。先ほどの言葉をかりますと
pはa[0]の変数の変数としてセットされます。


この0番の要素というのは決まり事になります。C言語の場合、配列の先頭は0番目ということになります。


さてここでa[1]やa[2]等にアクセスしたいのですが、pを使うとどのようになるでしょうか?
実は、


*p は a[0]
*(p+1) は a[1]
*(p+2) は a[2]



*(p+9)はa[9]


と言ったように関連付けられます。イメージすると下記の図1のようになります。



また、例えばですが、配列aの値の最初の5個の要素の合計を計算しようとすると


int sum = 0;
for ( i = 0; i < 5; i++ ) {
    sum += a[i];
}

と記述することもできますし、


int sum = 0;
int *p = a;
for ( i = 0; i < 5; i++ ) {
 sum += *p++;
}

と記述することもできます。イメージすると下記の図2のようになります。



これの何が便利か?という話ですが、実は最後の


*p++


がC言語の真骨頂ともいえる記述になります。これは*pにより配列の要素にアクセスするということですが、同時に++によりポインタをインクリメントしています。このインクリメントにより配列の次の要素にアクセスできるようになります(単純にポインタに数値を足すと次の要素にアクセスできるようになるというのはメモリの連続性によるものです)。


つまり、p++とfor文を組み合わせると、


*(p + 0), *(p + 1),… *(p + 4)


と配列を順番にアクセスするようになります(図3)。



ここにさらにC言語の尖がった仕様


・文字列はchar型の配列
・文字列の最後は0


が入ると文字列のコピーは、


char dst[MAX_BUF];
char *src = "文字列";

char *p = dst;
char *q = src;

while ( *p++ = *q++ );

となり(見る人が見れば)美しいコードになります。


ここまでの説明ですが、今一つ分からないかもしれません。*p++ がなぜ美しいのかを充分に理解するにはアセンブリ言語の知識も必要かと思います。


実は一部のCPUになりますが、条件が整えば*p++は1命令にコンパイルされることがあります。
さらにいうと、while ( *p++ = *q++ ); も1命令(実際には準備を行うコードが前に作られたりします)にコンパイルされることがあります。


このあたりの詳細については次回になるかと思いますが、C言語のポインタの基本のゴールの1つがここにあります。
もっとも、これで終わりではありません。この章の最後になりますが、先ほどの料金計算についてポインタを使った例を見せたいと思います。


先ほどのコードですが、料金がadultpriceやchildpriceと大人と子供それぞれ1つでした。例えば電車の運賃を考えますと料金計算は年齢および距離から計算されるでしょう。ということで、下記の仕様でプログラムを記述してみましょう。


/*料金表*/
int adult_table[4] = { 200, 270, 340, 410, };
int child_table[4] = { 100, 140, 170, 210, };

/*計算対象(3名)*/
int people_age[3] = { 20, 10, 21 };
int price_index = 2; /* 距離要素(距離テーブル配列の要素番号を指す)*/

/* コピー */
int totalprice = 0;
int i;
for ( i = 0; i < 3; i++ ) {
    if ( people_age[i] > 12 ) {
        totalprice += adult_table[price_index];
    } else {
        totalprice += child_table[price_index];
    }
}

/* ポインタ */
int totalprice = 0;
int *actualprice_table;
int i;
for ( i = 0; i < 3; i++ ) {
    if ( people_age[i] > 12 ) {
        actualprice_table = adult_table;
    } else {
        actualprice_table = child_table;
    }
    totalprice += actualprice_table[price_index];
}

となります。私にとってですが、実はポインタを使った例の方がコードの保守性(コードリーディングや変更に対する容易性)が高いです。
ポイントは料金計算をしているコードでコピー版の方はtotalpriceへの加算が2か所に分かれてしまっていますが、ポインタ版を一行にとどまっているところです。


totalprice += actualprice_table[price_index];

仮にですが、大人料金・子供料金だけでなく「シニア料金」や「乳児」が足されるとコピー版ではtotalpriceへの加算を行う行が増えます。そして例えば消費税の計算を入れるとか割引処理を入れるとなるとどちらのコードのメンテナンスが厄介でしょうか?ということです。
実は、配列のコピーをおこなえば、コピー版の方でも同様コードが書けますが、C言語の場合は配列のコピーは直接サポートされていないことと配列のコピーは効率的でないのであまりお勧めできないところです。ここがポインタを使うメリットの1つです。
さらに、少し進んだ話をしますとこのようなポインタの使い方(状況に応じて対象(この場合は表))を切り替えることは、はオブジェクト指向言語では良くあるテクニックになります。覚えておいて損はないです。


さて、ポインタを使った方がコードがよりクリアーになるという話をしましたが、実は上手く配列を使えばもっと良くなります。
その例を示します。


/*料金表*/
int price_table[2][4] = {{ 200, 270, 340, 410, }, /* 大人料金 */ 
                         { 100, 140, 170, 210, } };/* 子供料金 */

/*計算対象(3名)*/
int people_age[3] = { 20, 10, 21 };
int price_index = 2; /* 距離要素(距離テーブル配列の要素番号を指す)*/

int totalprice = 0;
int age_index;
int i;
for ( i = 0; i < 3; i++ ) { 
    if ( people_age[i] > 12 ) {
        age_index = 0;
    } else {
        age_index = 1;
    }
    totalprice += price_table[age_index][price_index];
}

料金テーブルを素直に表現している面で、私としてはこれが一番可読性もメンテナンス性も高いですが、ポイントは
・テーブルアクセスにより料金算出の一本化
・テーブルのインデックスの計算は別で行う。
ということを徹底しています。
プログラマーによっては「トリッキー」と感じるかもしれませんし、状況によると前のポインタの方が良い場合もありますが、この例から言える重要なことは、『アプリケーションプログラムを作成する上ではポインタをマスターすることは必ずしも必要ではない』というのは残念ながら今までは正当でありました。
これが、ポインタの理解がいい加減になる理由の一つかもしれません。(アプリケーションプログラム作においてはポインタに対する知識がいい加減でもなんとかなったりする)


さてこの料金計算ですがせっかくなので「サンプルから」の改善ポイントを指摘します。
・通常料金テーブルはデータベースなどに格納されているのでそこから取り出すことになります。
・今回の例では、「家族などのグループの移動」が想定されています。個別の計算も考えられます。
・いわゆるマジックナンバー(3とか12)があります。

最後のマジックナンバーについてですが、これはサンプルということでそのまま出していますが、大人と子供を分ける年齢(12)についてはテーブル化したほうがよいでしょう。具体的には、


/*料金表*/
int price_table[2][4] = {{ 200, 270, 340, 410, }, /* 大人料金 / 
                         { 100, 140, 170, 210, } };/ 子供料金 /
int age_rank[2][3] =    {{ 0, 12, 1 }, / 子供料金対象年齢とテーブルへのインデックス / 
                         { 13, INT_MAX, 0 }, }; /大人料金の対照年齢とテーブルへのインデックス */

/* 計算対象(3名)*/
int people_age[3] = { 20, 10, 21 };
int price_index = 2; /* 距離要素(距離テーブル配列の要素番号を指す)*/

int totalprice = 0;
int age_index;
int i, j;
for ( i = 0; i < 3; i++ ) {
    for ( j = 0; j < sizeof(age_rank)/sizeof(age_rank[0]); j++ ) {
        if ( age_rank[j][0] <= people_age[i] && people_age[i] <= age_rank[j][1] ) {
            age_index = age_rank[j][2];
            break;
        }
    }
    totalprice += price_table[age_index][price_index];
}

となります。テーブルのバグでage_indexに適切な値が入らない場合を考慮する必要があるのですがそのコードについては省略します。
また、age_rankを2次元配列で実現していますが、本来は構造体の配列になります(こうするとDBとの相性が良くなります)。
このコードが強力なのは、年齢区分が増えてもデータ定義の部分(料金表)の変更だけで済みます。また、いわゆるロジックで使われるマジックナンバー(この場合、12)もデータ化しているのでメンテナンス性はかなり高いです。要するにアプリケーションプログラマーとしてはこのようなコードを書くことを目指そうということになります。ポインタの趣旨からは外れますがプログラミングを考えるとこういうことを言及しなければなりません。つまり「ポインタよりもこのようなデータ駆動の方法論を使う方がアプリケーションプログラムにおいては多くの場合有効である」ということになります。


補足とまとめ


ちょっと長くなり様々な論点が出てきましたが、改めて補足とまとめを行います


・C/C++言語のポインタとは、比喩的にいうと「道標」でありアドレスは「道標が示す住所」ということになります。
・変数の値にアクセスしようとした場合に、直接変数にアクセスする方法もありますが、ポインタ(道標)を使うことにより「変数の変数化」が行えます。
・「変数化された変数」を通して元の変数にアクセスすることを「間接参照」ということもあります。
・間接参照は数珠繋が行えます。つまりポインタのポインタも作ることができます。
・ポインタと似た概念(機能)に参照というものがあります。C言語では参照は直接サポートされていませんが、しばしばポインタのことを参照と言ったりします。
・C言語では配列は、連続したメモリブロックとして認識されます。
・ポインタを使うとメモリブロックに順番にアクセスする場合、p++と言った魔法の呪文が使えます。つまりfor each(やeach)と同様の機能がポインタを使えば実現できます。
・ポインタは基本的な機能ですが、C言語上でもアプリケーションプログラムを作る上では完全ではないですがポインタを避けるプログラムもできます。
・構造体をつかったりデータ駆動アリゴリズムという高水準の機能や概念をマスターすることが、ポインタをマスターするよりもアプリケーションプログラム作成においては重要な場合もあります。
・ポインタを避けることは可能ですが、C言語(その他の言語でも)ポインタを除くことはできません。ポインタはプログラミング言語を実現する上での基本機能の1つです。C言語では配列へのアクセスがポインタの利用と同一視できるようにしていますが、概念的には配列のアクセスa[i]などは表面上だけでなく内部的にも(p+1)と認識されているという関係を持つと言えます。
・ポインタは重要かつ基本的な機能ですが、一方で危険な機能でもあります。プログラミング言語の歴史上、ポインタは避けられる傾向にあります。このような経緯から誤解が発生し「ポインタ=アドレス」という認識が生まれたようです。


ポインタとはメモリアドレスそのものではありません。ポインタとは、「間接性」という仕組みをC言語がプログラマに公開した機構です。
そして
「ポインタ=アドレス」と説明してしまった瞬間に、その間接性という本質が見えなくなります。


この記事ですが、記述するのにかなりの体力を使いましたが、本来は書くべきことの半分になります。書けるかどうか分かりませんが次回の予定は『AI時代に必要な教養としてのポインタとアセンブリ言語』がテーマになります。

2026-08-02 | コメント:0件

私のソフトウェアの開発手順とその進化(1)

 初心者の方は特に感じられることかと思いますが、さぁ、プログラミング言語を覚えた!プログラムをどうやって作成するのでしょうか?
例えば、ゲームのプログラムを作りたい!と思ったときに、手が勝手に動いて、気が付いたら思う通りのゲームを作れる人は多分、これ以降は読まなくてもよいかもしれません。


これは皮肉ではなく、私自身が本当にそう思っているということもありますし、一方で私には無い才能ということで憧れでもあります。
実は、ソフトウェアの開発環境はこのようなある種の、プロデューサーといいますか映画監督といいますが、要するに『何を作るかが分かっている人』が潜在的には求められており、AIがコーディングを助けるようになると、ますます重要なポジションになるかと思います。


これは、特にソフトウェアをある種の表現行為に使う(ゲームとかAI動画等)場合に必要な人材と言えるかもしれません。


一方で、ソフトウェア開発というのはもっと多種多様な世界ともいえます。こういうと大げさに聞こえますが、『どう作るかは分からないが、完成図が見えている人』『完成図を貰えれば、あとは作れる人』と組み合わせると上手くいくケースもあります。大半のエンジニアが関わる業務アプリやらECサイト、その他、WEBアプリや組み込みアプリがありますが、『現場で使う人』と『現場の人が使えるツールを作る人』という組み合わせもあるでしょう。


このように作る人と作ってもらいたい人が別になる、つまり組織的にプログラムを作るようになると、作るものの合意をしたり役割分担をすることになり、『誰がいつ、何をするか?』というソフトウェアの開発手順が必要になってくるでしょう。


ソフトウェアの開発手順の古典的かつメジャーな方法が、「ウォーターフォール開発」ということになります。
段取りを踏んでプログラムを作りましょうということですが、『こういう面倒なことは知らなくてもよいのでは?』と思われる方も、自分が仕事としてプロジェクトに参加して、プログラムを組む場合は、つまり組織的に開発するということは何らかの開発手順を踏むことになりますので、勉強されることをお勧めします。


ウォーターフォール開発は、色々細かいところはありますが、概ね


・要求仕様(Requirements)
・設計(Design)
・実装、開発(Implementation)
・テスト(Verification)
・運用(Maintenance)


という風に工程を分けて、開発を行うということです。『要求仕様』というのは一言でいうと『何を作るのか?』を決める作業で、『設計』『どう作るのか?』を決める作業、『実装』で実際にモノを作り、『テスト』で動作確認を行いバグをとりのぞき、『運用』では実際に使っていくということになるかと思います。


実装、テスト、運用についてはプログラマの方には分かりやすいかと思いますが、要求仕様や設計についてはピンと来ないかもしれません。いわゆる上流工程と言われている工程が要求仕様になるかと思います。具体的になにをするかは様々ですが、ここではお客さんと頻繁に打ち合わせを行うことになります。よく、オプションとして「AとBどちらにしますか?」というとお客さんから「どっちもできるように対応して」とか言われるところです。ここですかさずに「そうしますと工数が増えますね。あとで概算見積もりに入れます」と言えるかどうかが、その後の開発が修羅場になるかどうかを決めることになります。


設計作業は、名前ほどキチンとしておらず曖昧かもしれません。例えば、昔は「オブジェクト指向は大規模開発に向く」と喧伝され「UMLが・・・」とか「オブジェクト指向設計」やら「モデリング」やら言われた時代がありました。これらの声が下火になった、今、はたしてどこまで浸透しているかは謎ですが、批判を覚悟であえて言いますと、大規模開発において信頼性のあるアプローチ(設計手法)はトップダウン分割統治かと思います。つまり機能ごとに分割してアプリケーションを定義するということになります。ECサイトなら「購入サイト」や「商品・その他マスター管理」、「顧客管理」、「注文管理」とかになるかと思います。機能分割に着目することは「オブジェクトがどうのこうの」というより、より上位の、つまり抽象度が高い操作と言え、分割にも適しているでしょう。


さて、以上は、『私のウォーターフォール開発』の理解です。日本でのソフトウェア開発の現場を見てきましたが、こういうことをキチンと分かっている人や出来ている人は経験上少なかったかと思います。


『お前はどうやねん』と言われそうなので、予め答えますと、プログラミングと同様に一通りの勉強はしました。また、キャリアのごく初期は幸運にもウォーターフォール開発を行っていた経験があり、理論と実践で学ぶことができました。
一方で、繰り返しになりますが、現場現場で、関わる人達の理解に差があったり、ケーススタディーがキチンとしていない面もあり、状況は「カオス」かと思います。
 一時期、情報処理技術者試験の受験をやめたことがあるのですが、これは『あまりにもソフトウェア開発手順を無視したプロジェクトマネージャ』がいる現実に落胆して勉強するのが馬鹿らしくなって遠のいたことがあります。


以上、ざっとウォーターフォール開発を見ていきましたが、ウォーターフォール開発の批判の1つが、「各工程を厳密に区切る」ところにあり、Wikiにもあるとおり、「全工程に間違いがない」ことを前提にしている開発手順と指摘されています。
例えば、要求仕様の段階で、「そのままの要求仕様を満たすものは作れない」となると大問題ということになります。
これに近いものになりますが、私が気を付けているのは「プロジェクトメンバはどのタイミングで、作成するものを具体的にイメージ出来るか?」ということになります。言葉を変えると「この情報でプロジェクトメンバは開発が滞りなくできるか?」ということになります。
ソフトウェア開発の現場で、よくあるトラブルの1つは『プログラムを作れない』という状況になります。要求仕様の間違いもありますが、どうも一部のエンジニアは『出来ない』ということが言えなかったり、自分の能力不足を認めなかったりし、想定(報告)以上の時間を使って開発が遅延します。


次回では、ウォータフォール開発をもう少し突っ込みつつアジャイル開発についてみます。


2026-04-25 | コメント:0件

ウォーターフォール vs アジャイル論争の誤解

最近、とあるAI絡みのプロジェクトでLLMを使ったシステムの研究開発をやっておるのですが、個人的に応用が利くものができつつあると実感しているのですが、一方で、今は広める訳にはいかないので、成果の一部を小出しにします。


ちなみに、「オブジェクト指向おじさん?」の記事については、「感情的である」というAIからのご指摘を受けてそのうち修正を行いたいと思います。


以下本題、下記の記事にコメントします。


なぜ、「ウォーターフォールでソフトウェアを作れる」という嘘を信じる人が世の中にいるのか?
ウォーターフォール開発がダメなのかは、弁護士が請負契約で仕事しないことでわかる?

あくまでも個人的な感想ですが、どうもAIを活用した論調の記事だとは思うのですが、私のAI分析が良い感じだったのでその成果ということであえてコメントします。


1.はじめに:プロセス論争の空虚さと顧客の関心


 まず、結論ですが、ウォーターフォールだろうが、アジャイルだろうがその場にあった方法でどうぞとなります。
「プログラマ」ではなくある種のコンサルタント的な立場の人間(SEや営業、アーキテクトもその一人かもしれません)、つまり上司やプロジェクトリーダの元での作業ではなく、非エンジニアの顧客(情報システム部門の方でも、そうでない方でも)と直接話をする場合にわかるかと思いますが、おそらく99%の顧客の関心事項は


金をどぶに捨てることにならないか?(何らかの成果物が出てくるか?)


ということです。顧客目線に立てばウォーターフォールが良い場合もあればアジャイルが良い場合もありますし、ウォーターフォールだろうがアジャイルだろうが、最終的にモノが出来なければ「修羅場」になるということです。


2.契約形態がプロセスを決める


 まともな顧客は、RFP(Request for Proposal)を用意して提案書(見積)を求めてきます。
そうでなくても「こんなシステムを作りたいのだが・・・」からはじめに打合せを行ってから、ある程度「何を作るか?」を話し合ってからの見積となります。
この場合、ウォーターフォール開発が想定されるような「システム開発」の場合、ほぼ間違いなく


完成保証があります。


つまり請負契約になります。ここで『ウォーターフォール開発がダメなのかは、弁護士が請負契約で仕事しないことでわかる?』とありますが弁護士が請負契約をしない最大の理由は「原理的に完成保証ができない」からです。つまり裁判になったときには必ずどちらかが負けます。弁護士が仕事を受けるときには原理的に「勝ち」を保証できないことになります。


3.不確実性の正体:本質・要求・技量の3層構造


 一方でソフトウェア開発の場合は、「不確実性」があったとしても顧客と開発者が成功に向けて協力すれば、「何を作るか?」ということに関しては何らかの合意ができることが多いです。また、記事にあるような『不確実性が高い』というのは、ソフトウェア開発が持つ「本質的不確実性」の他に、開発するエンジニアの技術不足や経験不足「技術的不確実性」ということもあります。もちろん顧客の都合で仕様が変わる場合「要求不確実性」もあり、これらをきちんと見極めないと、アジャイルといって何回も工程を繰り返しても「いつまでたってもできない」ということもあります。


つまり、ウォーターフォールかアジャイルかの問題ではなく、これら3つの不確実性のうちどれをどの時点で誰が負担するかの問題になります。


「完成保証なんて出来ない」と思っている人もいるかもしれません。当然ですが、ここは現状に合わせて適宜修正を行うことになります。「出来るかどうかわからない」という状態はウォーターフォール開発的に言えば「要件定義が終わっていない」ということになります。実はウォーターフォール開発でも現状分析や要件定義などのいわゆる上流工程は原理的に「完成保証」ができません(し実務上もしません)。要件定義がまとまっても技術的にできないことが後になって発覚する場合もあり得ます。そういうのを防ぐ為にも「プロトタイプ」を要件定義の間に行うこともあります。また、テスト工程では「要件定義のバグ」も考慮して期間や費用の見積もりを行った方がよいでしょう。
私の経験では、要件定義の段階で大体プロトタイプを作成しますしそれなりの費用をいただいております。


4.ウォーターフォールの利点:仕様確定による“境界の明確化”


 ウォーターフォール開発の利点の一つですが、「仕様を確定させる」というのがあります。これはお客からの理不尽な要求を抑止する効果があります。もっとも「全ての追加要求を突っぱねる」というのは営業的に問題がありますし、私も「当初の約束でないタスク」をしたこともありますが、一定の線引きがないと収拾がつかなくなります。


仕事というのは契約なので「何を何処まで、幾らでどの期間で」というのを確定させるには工程もある程度は確定している必要があります。ウォーターフォール開発というのは、一般的なビジネス習慣と親和性が高いということになります。


5.アジャイルの曖昧性:誰にとってアジャイルなのか?


 一方でアジャイルという言葉には曖昧性が含まれています。つまり誰にとってアジャイルか?ということです。顧客企業にしてみれば「仕様変更がアジャイル」というかもしれませんが、上記の筆者の言いたいことは「アジャイルは完成保証を前提としない」というこのように読み取れます。こういう認識の違いは、ウォーターフォール以上に、後工程でほぼ確実に契約上の衝突として顕在化します。避けたいところです。アジャイルと言って何でもかんでも「不確定」としてはいけません。
『『アジャイル開発の失敗率は268%も高い』のコメント欄が面白かったので紹介するよ』という記事もあるので、基本的に開発プロセスについてはそもそも比較自体が怪しいということは知っておいた方がよいかと思う。


6.信頼関係がプロセスを決める


 また、アジャイル開発外部委託モデル契約についてを引用しますと『アジャイル開発においては、開発過程において仕様変更を柔軟に受け入れる場合や、そもそも仕様が明確でない場合等がある。しかし、こうしたアジャイル開発の特徴に対するユーザ企業側の理解が十分でない場合には、期間内に成果物が完成しない等により、ユーザ企業とベンダー企業の間でトラブルとなるケースも発生するとの指摘がある。』とあります。
つまり、ウォーターフォールでもアジャイルでも、モノができなければ揉めます逆説的になりますが、経験上、信頼されている開発者(会社)は顧客からアジャイル(準委任)を要望され、信頼されない開発者(会社)は顧客からウォーターフォール(請負)を強制される傾向があります。
つまりプロセスは技術的な選択というよりも、信頼関係の結果として決まる側面もあり、あまり声高にアジャイル(準委任)を叫ぶと逆効果ということもあります。


7.経験談1:信頼関係が育んだ自然なアジャイル


 ウォーターフォール vs アジャイルと見てきましたが、『信頼されている開発者(会社)は顧客からアジャイル(準委任)を要望され』ということで、私もアジャイル的に開発を進めたことがあります。つまり顧客と長い付き合いになり、いい意味でなし崩し的になると、開発スタイルとして「プロトタイプ1」、「プロトタイプ2」・・・「完成!」、みたいなことがありました。これはいちいち顧客の要望を細かく聞かなくても、モノを見せながら適宜(アジャイル的に)直していけばいいやという感じで成立していたこともあります。


8.経験談2:究極のアジャイル


 その他の印象に残るアジャイル的な開発プロジェクトになりますが(まぁ時効ということで告白しますと)、ある意味ぶっ飛んだプロジェクトで、ソフトウェアのリリースが直前になって差し止められるという究極のアジャイル、を経験しました。これは笑い話のようでいて、「価値判断が最後まで確定しない」という意味では、現実に最も忠実な開発プロセスだったのかもしれません。数か月の努力が無になったのですが、お金をもらった以上こちらとしても問題なく、上記の例外の1%ということで、私の経験上これ以上のアジャイルはないかと思います。

2026-04-15 | コメント:0件

東京都がAIを使って、業務アプリを開発・共有するらしい

 東京都、内製AIプラットフォーム「A1」本格運用開始 職員がノーコードで業務アプリ開発・共有


 今に始まったわけではないが、IT系の新しい技術が出てくると、まるで冷やし中華のように「○○始めました」という記事が出てきます。もちろん当人たちは真剣(?)にやっているかと思いますが、ただ流行りを追っかけているだけとうことであれば資源(この場合税金)の無駄遣いになります。


 ということで、いち東京都民として、税金の無駄遣いにならないか検証するために、覚書ということでメモします。一年後ぐらいに検証できればと思います。

2026-04-14 | コメント:0件

次世代のプログラミング言語とは?

AIによって、プログラマーが淘汰されようかという今になって「次世代のプログラミング言語とは何を言っているのか?」と返されそうですが、今とあるプロジェクトをやっておりまして(具体的なことは控えます)、AI時代に必要なプログラミング言語についてより意識するようになってきました。


■ 現在のAIプログラミングの構造


今までのプログラミングとは「人間からコンピューターへの指示」ということでした。ではAI時代になると何が変わるのでしょうか。


現状、AIを使ったプログラミングは次のような形になります。


人間 → [自然言語(プロンプト)] → AI → [プログラム] → コンピュータ



■ 次世代言語という発想


これについてまず考えられるのは、次のような形です。


人間 → [次世代言語] → AI → [プログラム] → コンピュータ

つまり、人間とAIの間に共通言語を持てないか、という発想です。


さらにいうと、


人間 → [次世代言語] → AI → [次世代言語] → コンピュータ

ここまで踏み込めれば、AIが生成した内容も(原理的に)人間が確認可能になります。
つまり、問題が発生したときに「何が意図されていたのか」を追跡できるようになります。




■ 想定される批判


ここで当然、次のような批判が出てきます。


人間 → [次世代言語] → AI
AI → [次世代言語] → コンピュータ

同じ言語を使うのであれば、AIは人間が入力した内容をそのまま返すだけではないか、というものです。


実際に、そういうことは起こり得るでしょう。




■ それでも何が便利なのか?


ポイントは、人間・AI・コンピュータの間で共通言語を持つこと自体にあります。


自然言語ではなく、このような言語を想定する理由は以下のとおりです。


  • 表現にムラがなく、曖昧性を排除できる
  • 最終的に機械で実現可能な形に落とし込める

一方で、従来のプログラミング言語との違いは、


  • 人間が理解可能な抽象度を保つこと

にあると考えています。




■ 実は新しくない発想


このアイデアは一見すると突飛に見えるかもしれませんが、実は全く新しいものではありません。


第5世代コンピュータで目指されていた、論理型言語の思想に近いものです。


論理型言語の代表格であるPrologは、当時日本でも注目されました。
また、このブログでたびたび触れているADPも、Prologをベースとしています。


また、Grokに言わせるとLLMとPrologをつなぐアイデアは既に研究対象となっているようです。AIに聞けば多数出てくるようです。下記面白そうなもの2点をピックアップします。


Arithmetic Reasoning with LLM: Prolog Generation & Execution
LLMが自然言語の算術問題からPrologの述語・ルールを生成し、Prologインタプリタで実行。CoT(Chain-of-Thought)より精度が大幅に向上した実験。


LLM and Prolog: the logical alternative to chain-of-thought reasoning (Medium, 2025)
金融領域での実践例。LLMが自然言語からPrologルールを抽出し、シンボリック推論エンジンで処理。非常に読みやすい解説。


そりゃPrologを知っている人なら絶対に考えるよなという話ですね。




■ 宣言的という考え方


Prologのような言語は「宣言的」と呼ばれます。


つまり、


  • 「何を作るのか?」にフォーカスする
  • 「どう作るのか?」は実行系に委ねる

という考え方です。


現在のAIによるコーディングもこれに近く、人間が「何を作るのか」をプロンプトで与え、「どう作るか」はAIが担っています。




■ 批判への一つの答え


この役割分担を前提とすると、


「AIは入力をそのまま返すだけではないか」


という批判にも、ある程度対応できます。


もう少し踏み込んだイメージとしては、


  • 人間は「満たすべき条件」(述語)を定義する
  • AIはその実装(述語の本体)を生成する

という形です。


ここでいう「述語」は論理型言語の用語で、「関数」に近い概念です。


処理手順ではなく「満たすべき条件」を中心に表現することで、結果の妥当性を人間が確認しやすくなります。




■ 現実的な課題


とはいえ、このような構想は「言うは簡単で実現は難しい」ものです。第5世代コンピュータプロジェクト自体が頓挫した経緯もあり、AIを使えば当時の問題は解決できるのか?という問いは依然として残ります。

具体的には、


・人間に読みやすいと言ってもPrologはコンピュータよりの言語である。
・宣言的といっても、それだけですべてが上手くいくわけではない。Prolog自体が流行っていない。
・現実案としては、コメントがAIに対する補足(プロンプト)になるが、そうすると自然言語が残ることになる。


というジレンマがあります。特に「宣言的プログラミング」とは当時一瞬流行ったパラダイムになりますが、純粋さを追求すると却ってコードを分かりにくくする側面があります。またAIの出力は手続き的にもなりえます。このあたりの折り合いをどうつけるのかというのが課題かと思います。

このような「純粋な理論」と「現実の泥臭さ」の板挟みを解決するために、私は一つのアプローチをとっています。例えば、ADPは、Prologをベースにマルチパラダイムを追求しています。つまり純粋な宣言的なパラダイムを捨てて、手続き的にも書けるようにしています。このあたりの落としどころが現実的ではあるかと思います。




■ 最後に


もっとも、私自身も30年ほど前にこうしたアイデアの断片を考えたことがあります。


さらにいうと、ADPの機能として次世代言語に必要な要件を備えることができれば、ADPそのものが次世代言語になり得るのではないか、とも思っています。


夢は広がりますが、まずは時間を見つけて少しずつ形にしていきたいところです。


2026-04-06 | コメント:0件
Previous Page | Next Page