記事一覧

第27問の答え

答え

このelseは「1ではないとき」に実行されるので、「2のとき」というコメントはおかしい!
  if (n == 1) {
    /* 1のとき */
    puts( "one" );
  } else {
    /* 2のとき */
    puts( "two" );
  }
ここは単純なif文なのでコメントを削除してスッキリさせつつ……
  if (n == 1) {
    puts( "one" );
  } else {
    puts( "two" );
  }
さらに、関数の定義域を明確にできるとベター!
/*
 * 引数が1のときは"one"、2のときは"two"と表示する。
 * それ以外のときの動作は未定義。
 */
void PrintOneOrTwo(int n) {
  assert((n == 1) || (n == 2));

  if (n == 1) {
    puts( "one" );
  } else {
    puts( "two" );
  }
}

解説

これは、コメントの書き方に関する出題でした。問題の関数では、if文のコメントに少しおかしなところがあります。

void PrintOneOrTwo(int n) {
  if (n == 1) {
    /* 1のとき */
    puts( "one" );
  } else {
    /* 2のとき */
    puts( "two" );
  }
}

「2のとき」というコメントがありますが、これが実際の動作と一致していないのが分かるでしょうか。

ここは(n == 1)という条件に対するelseだから……
「1ではないとき」っていうコメントじゃないとおかしいですね!
ふふふ。それじゃあ、やってみましょうか。
さすがに簡単ですよっ!
あれ?なんだか、すごく当たり前のことを書いてしまったような……
void PrintOneOrTwo(int n) {
  if (n == 1) {
    /* 1のとき */
    puts( "one" );
  } else {
    /* 1ではないとき */
    puts( "two" );
  }
}
そうね。まるでif文の説明みたいになってしまったわね。ここは思い切って、コメント自体をなくしてみるのはどう?
えっと。コメントを削除……と。
void PrintOneOrTwo(int n) {
  if (n == 1) {
    puts( "one" );
  } else {
    puts( "two" );
  }
}
あ、なんだかスッキリしましたよ!
ね。これくらいシンプルなif文なら、むしろコメントを書かないほうが読みやすいんじゃないかしら。
たしかに!ところで、僕はちょっと別の答えを予想してましたよ。
あら、そうなの?
はい。もともとの「2のとき」っていうコメントに合わせた動作にしたらいいのかなって。
こんな感じで、elseのところにifを追加すれば……
/* 引数が1のときは"one"、2のときは"two"と表示する */
void PrintOneOrTwo(int n) {
  if (n == 1) {
    puts( "one" );
  } else if (n == 2) {
    puts( "two" );
  }
}
仕様をストレートに実装したのね。とても良い答えよ!
やったー!
これなら、いつか1や2以外の値に対応したくなった場合にも、処理を追加しやすいわね。

さて。いつか1や2以外の値を追加したくなるかもしれないというのは、この関数が今のところ「1または2」しか受け付けないからですね。これは、関数に「定義域」が定められていることを意味します。定義域とは、引数として与えられる値の範囲のことです。

定義域の考え方は、実は数学で習う関数と大きく変わりません。

例えばね、三角関数の「tan」ってあるでしょ?
「サイン(sin)・コサイン(cos)・タンジェント(tan)」のタンジェントのことですか?
そうそう。そのタンジェントには「π/2」を与えられないじゃない?
いやいや、ユキ先輩!僕が文系出身だってことをお忘れですか?
あー、ごめんごめん。つまりね、関数というのは、意外に引数が制限されているものなのよ。
なるほど。そういう話なら分かります!
でね、それは数学でもプログラミングでも同じことなの。

では、問題の関数について確認してみましょう。

/* 引数が1のときは"one"、2のときは"two"と表示する */
void PrintOneOrTwo(int n) {
  ……
}

コメントされている仕様から、この関数の定義域は「1または2」だと分かります。そして、main()からは、次のように呼び出されています。

  PrintOneOrTwo(1);
  PrintOneOrTwo(2);

引数の値が定義域に収まっているので、期待どおりに動作しているということです。

もし、これが数学の関数なら、「1または2」以外の値を与えることはできません。でも実際には、「1または2」以外の値を引数にして関数を呼び出すプログラムを書くこともできますね。では、この関数の引数に3という値を与えたらどうなるでしょうか?

どうなると思う?
はい、分かりません!
正解!
ええっ?
だって、決められていないもの。「分からない」で正しいのよ。

今回の関数では、「1または2」以外の値を受け取ったときの動作が決められていませんでした。そのため、引数に3と書いた場合にどのような動作をするかは、「不明である」というのが仕様なのです。このような動作を「未定義(undefined)」といいます。

ふむふむ。つまり、「1または2以外の値を受け取った場合の動作は未定義」ということですかね。
そういうこと!
えっと……。そうしたら、引数に3を指定するのは無理ってことになりませんか?
そのとおりよ!「未定義」というのは、実質的には「使用禁止」のことね。
そっか!なんでも引数に指定していいわけじゃないんですね!
分かったみたいね。それじゃあ、何が未定義なのかが明確になるように、コメントを直してみましょうか。
やってみます!
未定義な動作を明確に……と。こんな感じかな?
/*
 * 引数が1のときは"one"、2のときは"two"と表示する。
 * それ以外のときの動作は未定義。
 */
void PrintOneOrTwo(int n) {
  ……
}
うん、いい感じ。これなら引数に3を指定してはいけないっていうことも分かるわね。
やったー。それにしても、C言語のクイズの答えで日本語を書くっていうのは、ちょっと意外だったかも。
ふふふ。そうかもね。
ここがポイント!
コメントには大事なことを書こう!関数に未定義の動作があるときは、コメントしておけば分かりやすい!

ちなみに。

関数の使い方をしっかりコメントに書いていたとしても、間違いを100%防げるわけではありません。今回の関数を、うっかり引数を3にして呼び出してしまうこともあるでしょう。

そこで、次のようにassertを入れておくのがおすすめです。こうすれば「1または2」以外の値が明確に禁止されるので、早い段階で間違いに気付けるようになるでしょう。

#include <assert.h>

void PrintOneOrTwo(int n) {
  assert((n == 1) || (n == 2)); /* ← 「1または2」以外の値を明確に禁止! */

  ……
}
レオ君が考えた答えと合わせて、こういう風に書くのもいいわね。
#include <assert.h>
#include <stdbool.h>

void PrintOneOrTwo(int n) {
  if (n == 1) {
    puts( "one" );
  } else if (n == 2) {
    puts( "two" );
  } else {
    assert(false); /* ← 「1または2」以外の値を明確に禁止! */
  }
}
採用された〜!
または、switch文で書き換えてもOKよ!
#include <assert.h>
#include <stdbool.h>

void PrintOneOrTwo(int n) {
  switch (n) {
    case 1:
      puts( "one" );
      break;
    case 2:
      puts( "two" );
      break;
    default:
      assert(false); /* ← 「1または2」以外の値を明確に禁止! */
  }
}

修正後のプログラム

main.c
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

/*
 * 引数が1のときは"one"、2のときは"two"と表示する。
 * それ以外のときの動作は未定義。
 */
void PrintOneOrTwo(int n) {
  assert((n == 1) || (n == 2));

  if (n == 1) {
    puts( "one" );
  } else {
    puts( "two" );
  }
}

int main(void) {
  PrintOneOrTwo(1);
  PrintOneOrTwo(2);

  return EXIT_SUCCESS;
}
実行結果
one
two

第27問

引数で数値を受け取ると、対応するテキストを表示する関数を作りました。その関数を次のように呼び出してみたところ、どうやら期待どおりに動作しているように見えます。でも、できれば早いうちに直しておきたい部分があります。

どこを直すのがいいか分かりますか?
main.c
#include <stdio.h>
#include <stdlib.h>

/* 引数が1のときは"one"、2のときは"two"と表示する */
void PrintOneOrTwo(int n) {
  if (n == 1) {
    /* 1のとき */
    puts( "one" );
  } else {
    /* 2のとき */
    puts( "two" );
  }
}

int main(void) {
  PrintOneOrTwo(1);
  PrintOneOrTwo(2);

  return EXIT_SUCCESS;
}
実行結果
one
two
今回のプログラムは、コメントが入ってて読みやすいですね。
そうね。最初のコメントが関数の仕様になってるのが分かるかしら?
最初のコメントはこれか……
/* 引数が1のときは"one"、2のときは"two"と表示する */
void PrintOneOrTwo(int n) {
  ……
}
引数によって表示するテキストが変わるっていう仕様ですね。
そうそう。関数の中身はどうなってるかしら?
関数の中身にもコメントが入ってる。ふむふむ……ん?
  if (n == 1) {
    /* 1のとき */
    puts( "one" );
  } else {
    /* 2のとき */
    puts( "two" );
  }
んんー。なんだろう、これ……
ね。なんだか正しいような正しくないようなプログラムでしょ?
はい。最初は読みやすいと思ったんですけど、よく見たらモヤモヤしてきました……
そのモヤモヤの理由について考えるのが、今回の問題よ。
な、なるほど。
問題の関数に3を入力したらどうなるでしょうか?

コメントからはじめよう

ソフトウェアエンジニアには「設計力と実装力のバランスをとることが大切」と教えてもらいましたけど、実装力(細部までキッチリと作り上げる力)を伸ばすには、どうしたらいいですか?
そうねぇ。まずは一つ一つの関数をキッチリ仕上げられるように気を付けることかしら。
一つ一つの関数をキッチリ……ですか。
そうよ。……あら、なんか「納得いかない」って顔ね。
いえいえ、そういうわけじゃないんですけど……その、キッチリっていうのがいまいちピンとこないんですよ。
ああ、そういうことね。……それじゃあ、まずは「キッチリした関数」とはどんなものかという定義を考えてみましょう。
はい!

関数がキッチリしていると言える条件は、以下のような仕様が細部までしっかりと明確になっていることです。

キッチリした関数とは

  • 使用目的が明確
  • 定義域(受け付けられる入力の範囲)が明確
  • 値域(得られる出力やエラー値の範囲)が明確
  • 制限事項が明確

なるほど。関数を実装するときには、こういう細かい仕様をきちんと考えておく必要があるってことですね!
そういうこと。
だけど、最初は意識していたつもりでも、実装を考えているうちに忘れてしまうこともありそうですね……
そうなのよ。だから、関数のコーディングを開始する前に、仕様について「コメント」を書いておくのがおすすめよ。
え、コメントですか!?しかもコーディングの前に!?

細かいところまで漏れなく仕様を決めて、「キッチリした関数」を実装するためのガイドラインは、「コメントを書く」ことです。関数のコーディングを開始する前に、その仕様についてコメントしましょう。

コーディングしながら書くのではなく、コーディングしてから書くのでもなく、コーディングする前に書くのです!

どんなコメントを書くのがおすすめですか?
それじゃあ、具体的にどのようなコメントを書けばいいのか、一つずつ考えていきましょう。

使用目的

関数の使用目的は、言葉で表現するのが一番よ。普通に日本語で書けばいいわね。

関数の使用目的を書くのは、何を作ったのかを忘れないようにするためだけではありません。

明確に言葉で表現することによって、これから作ろうとしているものを自分自身に言い聞かせるためでもあります。実際に言葉で書いてみると、何を作るか分かっていたつもりでも、実は考えがまとまっていないことに気付くかもしれません。

なるほど。すごく身に覚えがあります。

そういう意味でも、関数を一つ書く前に、その目的についてコメントするのはおすすめの習慣です。

頭の中がモヤモヤしたままでコーディングを始めるよりも、欲しいものをハッキリさせてから作ったほうが品質も期待できるわね。

ちなみに、関数の使用目的は「作る人」ではなく、「使う人」の視点で書けるとベターです。どの関数も作るのは1回きりなので、使うためにコメントを読む回数のほうが多くなるからです。

定義域

関数が入力できる値の範囲は、明確に記述する必要があるわね。

例えば次の関数(文字列の長さを求める標準関数ですね)は、引数に0(NULL)を指定することができません。

size_t strlen(const char *s);

このように、関数には入力できる値とできない値があるのが普通です。想定外の値を入力すれば、プログラムはクラッシュしてしまうかもしれません。

では、このことがコメントされていなかったら何が起こるでしょうか。

例えば、この関数のある利用者が「0を入力するとクラッシュするぞ」という「発見」をしたとしましょう。このとき、コメントに何も書かれていなければ、「これは関数のバグだろう」と判断するかもしれません。

その結果、クラッシュしないように関数の実装を手直ししたとしたら、これは「デバッグ」でしょうか?いいえ、この行為は紛れもなく仕様変更です!

この例の問題点は、クラッシュした理由が関数の実装のせいなのか、使い方のせいなのかが不明なことよ。

もしも関数の定義域がコメントされていれば、どちらが間違っているのかすぐに分かったでしょう。このようなコメントには、安易な仕様変更を抑止する効果があるのです。

なるほど〜。

ちなみに、実際に関数をコーディングするときは、範囲外の入力を受け取ったらassertすればいいですね。

assert()については別のトピックで詳しく説明します。

値域

値を返す関数では、その範囲を明記しておくことも大切ね。

以下の2点について、しっかり書いておきましょう。

  • 正常な値:関数が正常に終了した場合に返却する値の範囲
  • エラー値:関数がエラーになった場合に返却する値の範囲

エラー値は、関数を呼び出したあとの判定に必要な情報です。ファイル操作やメモリー確保など、ある程度の確率で発生するエラーの確実なチェックを促すためにも、ぜひコメントしておきましょう。

制限事項

その関数でできないことがあれば、それも明記しておきましょう。

例えば、文字列の文字数をカウントする関数には、次のような制約があるかもしれません。

  • 「この関数は、文字列がUTF-8でエンコードされているものとして扱います」

こうした記述があれば、関数が不可解な動作をしたときに、頭を悩ませる時間を節約できるでしょう。


ふむふむ。あらかじめコメントを書いておくと、いろいろな場面で役立つんですね。
そういうこと!

関数を作る前に仕様をコメントするのは、正直に言えば面倒臭い作業です。でも、それ以上に効果が期待できます。コメントを書くのに時間がかかっても、トータルでは時間の節約になるのではないでしょうか。

ここがポイント!
関数のコーディングを開始するときは、コメントを書くことからはじめましょう。細かい仕様まで明確にしておけば、キッチリ仕上げられます。

次のトピックでは、あとで関数を使う人(または使うとき)のために何をやっておくべきか検討しましょう。

第26問の答え

答え

C++の予約語にあたるdeleteが関数名になっている!
bool delete(char *pText) {
  ……
}
C++の予約語はC言語でも使わないほうがベター!
bool DeleteLastLetter(char *pText) {
  ……
}

解説

C言語では、いくつかの単語が「予約語」に指定されています。予約語は、変数名や関数名などの「識別子」に使うことができません。

例えば、次のようにif()という名前の関数を書くとエラーになります。

void if(bool condition) { /* エラー:「if」は関数名にできない! */
  ……
}

次のように、変数の名前をswitchとするのもエラーです。

  bool switch = true; /* エラー:「switch」は変数名にできない! */
さすがにこういう名前は付けないと思いますけど。
そうよね。そもそもコンパイルエラーになるから、すぐに気付くしね。
ですよねぇ。
でも、エラーにはならないけれど、使わないほうがいい名前もあるの。

C言語のものに加えて、C++にはさらにいくつかの予約語があります。例を挙げると、newdeleteclassthisなどが予約されています。

問題のプログラムには、delete()という関数が使われていましたね。

bool delete(char *pText) { /* ← deleteはC++の予約語! */
  ……
}

これは、C言語のプログラムとしては問題ありません。でも、先々のことまで考えるなら、早めに別の名前に変えておいたほうがいいでしょう。C言語のプログラムを、そのままC++に流用(いわゆる「移植」)したくなるケースも少なくないためです。

bool DeleteLastLetter(char *pText) { /* ← 別の名前に変えておこう! */
  ……
}
それって、実際にC++に移植するときにやればいいんじゃないんですか?
それでもいいわよ。でもね、「これC++の予約語じゃん!」って気付いていたとしたらどう?
あー、そのまま放置するのは、ちょっと気持ち悪いかも……
あと、そもそもdelete()っていう関数名はどうなのかしらね?
うーん、ちょっと何をする関数なのか分かりづらいし、予約語かどうか以前にかぶりやすい名前かも……
でしょう?だからね、こういうのは早めに直しちゃったほうがいいのよ。
直さなかったら、どうなるんですか?
その関数を呼び出す部分が増えていって、だんだん直すのがメンドウになっていくわね。
そっか。だから「早めに直す」わけですね!
ここがポイント!
変数や関数には、予約語を避けつつ、分かりやすい名前を早めに付けておこう!

ちなみに。

アンダースコア(_)で始まる名前も、特別な理由がない限り使わないほうが無難です。具体的には、次のようなものです。

int _MyVariable = 123; /* _で始まる変数名は避ける */

void _MyFunction(void) { /* _で始まる関数名は避ける */
  ……
}

こうした名前は、ライブラリなどが独自に使用できるようにするために予約されています。C言語の文法としては問題ないのですが、自分で付けた名前とライブラリ内の名前が衝突するとエラーになってしまいます。

また、アンダースコアを2つ以上つなげた名前も避けたほうがいいでしょう。この書き方も、開発環境によっては予約されている場合があります。

#define MY__MACRO /* __を含む名前も避けたほうがいい */
こういう名前も、普通はあんまり付けないんじゃないですかね。
だといいのだけど、そうでもないのよ。
例えば、typedefと一緒に_が使われるケースは多いわね。
typedef enum _MyEnum {
  ……
} MyEnum;

typedef struct _MyStruct {
  ……
} MyStruct;
あ、見たことあるかも!
でしょ?そういうルールでやってる開発チームもあるもの。
でも、こう書くルールにしたほうが無難ね。
typedef enum {
  ……
} MyEnum;

typedef struct {
  ……
} MyStruct;
あ、はい。前に別の問題で練習したやつですね!
そうそう、よく覚えてたわね。えらい!
えへへ(褒められた〜!)

修正後のプログラム

main.c
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>

bool DeleteLastLetter(char *pText) {
  size_t length = strlen(pText);
  if (length > 0) {
    pText[length - 1] = '\0';

    return true;
  }

  return false;
}

int main(void) {
  char text[] = "Hello!!";
  while (DeleteLastLetter(text)) {
    printf("deleted: %s\n", text);
  }

  return EXIT_SUCCESS;
}
実行結果
deleted: Hello!
deleted: Hello
deleted: Hell
deleted: Hel
deleted: He
deleted: H
deleted: 

第26問

文字列を後方から1文字ずつ消去していくプログラムを作りました。正常に動作しているのですが、先々のことまで考えると直しておきたい部分があります。

どこを直すのがいいか分かりますか?
main.c
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>

bool delete(char *pText) {
  size_t length = strlen(pText);
  if (length > 0) {
    pText[length - 1] = '\0';

    return true;
  }

  return false;
}

int main(void) {
  char text[] = "Hello!!";
  while (delete(text)) {
    printf("deleted: %s\n", text);
  }

  return EXIT_SUCCESS;
}
実行結果
deleted: Hello!
deleted: Hello
deleted: Hell
deleted: Hel
deleted: He
deleted: H
deleted: 
今回のプログラムは、1つ前の問題より分かりやすいんじゃないかしら。
そうですね。解読してみます!
えっと、main()の内側にwhileがあって……
  char text[] = "Hello!!";
  while (delete(text)) {
    printf("deleted: %s\n", text);
  }
このdelete()っていう関数の処理が成功している間だけループして、テキストを表示してるんですかね。
そうそう。実行結果を見ると、テキストの変化が分かるわね。
そうですね。うしろから1文字ずつ消えていってます。
じゃあ、delete()関数の中身は……
bool delete(char *pText) {
  size_t length = strlen(pText);
  if (length > 0) {
    pText[length - 1] = '\0';

    return true;
  }

  return false;
}
これは、文字列の最後の1文字を'\0'、つまりヌル文字に置き換える処理で消してるんですね!
そうね。戻り値はどうなってるかしら?
はい。文字が消せたらtrue、消す文字がなければfalseになります!
これは、bool処理の成功・失敗を返しているということよ。
ふむふむ。だからwhileでループできるんですね。
そういうこと!それじゃあ、問題文に戻りましょうか。
そうでした。先々のことを考えて、どこを直すか?でしたよね。
今回のプログラムは、「C言語としては」正常に動作しますが……

第25問の答え

答え

uint32_tと宣言されたバッファにuint16_t *でアクセスするのは危険!
uint32_t ReversedHighLow32(uint32_t n) {
  uint32_t buffer;

  return ReversedHighLow32Helper(n, &buffer, (uint16_t *)&buffer);
}
無理なキャストを使わない方法を考えるのが正解!
uint32_t ReversedHighLow32(uint32_t n) {
  uint16_t high = n >> 16;
  uint16_t low = n & 0xffff;

  return (low << 16) | high;
}

解説

これは、コンパイラによる「最適化」に関する問題でした。最適化とは、プログラムをコンパイルする際にニーズに合わせてチューニングすることです。おもに、より高速に動作するようにしたり、メモリーの使用量を減らしたりすることが目的です。

開発している段階では、通常は最適化を実施しません。そのため、リリース後の動作が開発中と変わってしまうことも考えられるのです。

それって、おかしいですよね。開発中も最適化すればいいじゃないですか。
そうでもないのよ。開発中に最適化を有効にすると、デバッグが難しくなるの。
えー、むしろ効率良くなりそうな気がするんですけど……
例えばね、ステップ実行が難しくなるのよ。無駄な処理が削除されたりするから。

コンパイラは最適化のために、さまざまな方法でコードを変更します。といっても、動作の結果が変わってしまうような変更を加えようとするわけではありません。チューニングを施しながらも、同じ結果が得られるようにしてくれます。

ところが、プログラムの書き方によっては、最適化すると結果が変わってしまう場合があるのです。今回のプログラムでは、次の関数の結果(戻り値)が変わってしまう恐れがありました。

uint32_t ReversedHighLow32Helper(uint32_t n, uint32_t *pAs32, uint16_t *pAs16) {
  ……
}

この問題の原因は、関数の呼び出し元にあります。

  uint32_t buffer;

  return ReversedHighLow32Helper(n, &buffer, (uint16_t *)&buffer);

uint32_tと宣言されたバッファのアドレスを、uint32_t *uint16_t *の2つの型で引き渡している様子が分かるでしょうか。これらは互換性のない型なので、(uint16_t *)とキャストして型を読み替えています。

こういうキャストって、C言語ではよく見かけますよね。
そうね。でも、「データには型がある」っていう発想を、根本から破壊する手法ともいえるわね。
うむむ。たしかに……
ちなみに、こういう手法は「型のパニング(type punning)」って呼ばれているわ。
「punning」は「語呂合わせ」っていう意味ですね!
あとね、2つのポインタが同じバッファを指しているでしょ?
はい。
そういう状況のことを「エイリアス(alias)」というの。
なるほど、「別名」っていう意味ですね。

エイリアスを用いること自体は、何も問題ありません。しかし、今回のプログラムでは型のパニングによって、uint32_t *uint16_t *の2つのポインタが同じバッファを指していました。実は、コンパイラはこのような状況を想定していません。

最適化を実施するにあたって、コンパイラは「型が違う2つのポインタが同じ領域を指すことはないだろう」と考えていいルールになっています。そのため、このルールを無視して作られたプログラムは、最適化後に思わぬ動作をする恐れがあるのです。

では、ルールを無視すると、具体的に何が起こるのでしょうか?あらためて、問題の関数を確認してみましょう。引数nには0x11112222が与えられ、戻り値は0x22221111になるというのが期待される結果です。

uint32_t ReversedHighLow32Helper(uint32_t n, uint32_t *pAs32, uint16_t *pAs16) {
  /* 処理1:1つ目のポインタが指す領域にnの値を書き込む */
  *pAs32 = n;

  /* 処理2:2つ目のポインタで値を入れ替える */
  uint16_t temp = pAs16[0];
  pAs16[0] = pAs16[1];
  pAs16[1] = temp;

  /* 処理3:1つ目のポインタが指す領域から値を読み出す */
  return *pAs32;
}

ここでは処理内容を3段階に分けて、コメントを付けました。2つのポインタpAs32pAs16は型が異なりますね。そのため、コンパイラは2つのポインタが別々の領域を指していると想定しています。

すると、1つ目のポインタを使う「処理1」と「処理3」は、2つ目のポインタを使う「処理2」の影響をまったく受けないことになってしまいます。

そこで、「処理1」と「処理3」だけに注目してみましょう。

uint32_t ReversedHighLow32Helper(uint32_t n, uint32_t *pAs32, uint16_t *pAs16) {
  /* 処理1:1つ目のポインタが指す領域にnの値を書き込む */
  *pAs32 = n;

  ……

  /* 処理3:1つ目のポインタが指す領域から値を読み出す */
  return *pAs32;
}

「処理1」では、ポインタでnの値を書き込んできます。そして、その値を「処理3」で読み出していますね。「処理2」が影響しないなら、ここで読み出される値はnと同じに違いありません。

そこでコンパイラは、この関数を次のように解釈しても問題ないと判断します。nと同じに決まっている値をわざわざポインタから読み出すのは無駄なので、その処理を削除して、代わりにnを返すという最適化を行うのです。

uint32_t ReversedHighLow32Helper(uint32_t n, uint32_t *pAs32, uint16_t *pAs16) {
  *pAs32 = n;

  uint16_t temp = pAs16[0];
  pAs16[0] = pAs16[1];
  pAs16[1] = temp;

  return n; /* ← 1つ目のポインタからの読み出しを削除! */
}

こうした変更が随所で行われることにより、プログラムは最適に動作するようになります。ただし、そのためには「型が違うポインタは同じ領域を指さない」というルールにしたがうことが前提です。今回のプログラムはルールを破ってしまったため、問題が発生したのでした。

ちなみにね、このルールのことを「Strict Aliasing Rule」というわ。直訳すると「厳密なエイリアスのルール」という意味ね。
うー、今回は難しい用語が多いですね。
まぁ、用語は暗記しなくてもいいのだけど。
そうなんですか?
その代わり、もっとシンプルな2つのポイントを押さえておくといいわね。
わくわく。
1つ目は、最適化された状態でプログラムの動作確認をすること。
そっか、それでいいんですね!正直、難しくてどうしようかと思ってましたよ。
ふふふ。結局はテストが大切ってことね。
ですねー。それじゃあ、2つ目は何です?
できる限り、ポインタのキャストを使わないことね。

コンパイラの最適化オプションを有効にした状態でテストを実施すれば、最適化に起因する問題は未然に防げます。でも、コンパイラ自体もバージョンアップすることを覚えておきましょう。今は期待どおりに動作しているプログラムでも、新しいコンパイラでコンパイルすると想定外の動作を引き起こすかもしれません。

先々のことまで考えるなら、可能な限りポインタのキャストを使わなくて済む方法を考えるのがおすすめです。今回のプログラムは「32ビットの値の上位16ビット、下位16ビットを入れ替える」というものでした。この操作は、次のようにビット演算で実現できますね。

uint32_t ReversedHighLow32(uint32_t n) {
  uint16_t high = n >> 16; /* ← 上位16ビットを取り出す */
  uint16_t low = n & 0xffff; /* ← 下位16ビットを取り出す */

  return (low << 16) | high; /* ← 上位と下位を反対にしてくっつける */
}

または、もっと単純に割り算や掛け算を使ってもよいでしょう。

uint32_t ReversedHighLow32(uint32_t n) {
  uint16_t high = n / 0x10000; /* ← 上位16ビットを取り出す */
  uint16_t low = n % 0x10000; /* ← 下位16ビットを取り出す */

  return low * 0x10000 + high; /* ← 上位と下位を反対にしてくっつける */
}
同じことをするプログラムでも、いろんな書き方があるんですねぇ。
でしょ。どうせならシンプルに考えたほうが、難しい問題も減らせるんじゃないかしら。
そうですねー!
ここがポイント!
プログラムのテストはコンパイラの最適化を有効にした状態で実施しよう!

修正後のプログラム

main.c
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>

uint32_t ReversedHighLow32(uint32_t n) {
  uint16_t high = n >> 16;
  uint16_t low = n & 0xffff;

  return (low << 16) | high;
}

int main(void) {
  uint32_t x = 0x11112222;
  uint32_t y = ReversedHighLow32(x);

  printf("x = 0x%08x\n", x);
  printf("y = 0x%08x\n", y);

  return EXIT_SUCCESS;
}
実行結果
x = 0x11112222
y = 0x22221111

第25問

以下は、32ビットの値の上位16ビット、下位16ビットを入れ替えるプログラムです。どうやら期待どおりに動作しているようなのですが、このままリリースしてしまうと問題が発生するかもしれません。

何が問題なのか分かりますか?
main.c
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>

uint32_t ReversedHighLow32Helper(uint32_t n, uint32_t *pAs32, uint16_t *pAs16) {
  *pAs32 = n;

  uint16_t temp = pAs16[0];
  pAs16[0] = pAs16[1];
  pAs16[1] = temp;

  return *pAs32;
}

uint32_t ReversedHighLow32(uint32_t n) {
  uint32_t buffer;

  return ReversedHighLow32Helper(n, &buffer, (uint16_t *)&buffer);
}

int main(void) {
  uint32_t x = 0x11112222;
  uint32_t y = ReversedHighLow32(x);

  printf("x = 0x%08x\n", x);
  printf("y = 0x%08x\n", y);

  return EXIT_SUCCESS;
}
期待される実行結果
x = 0x11112222
y = 0x22221111
ちょっと複雑なプログラムだけど、解読できるかしら?
やってみます!
えっと、32ビットの値というのは、main()関数にあるxのことかな。
  uint32_t x = 0x11112222;
  uint32_t y = ReversedHighLow32(x);
このxの値の上位16ビットと下位16ビットを入れ替えて、yに代入してるんですね。
そうね。xの値は16進数で0x11112222だから……
y0x22221111になります!
正解!それじゃあ、実際の処理をしている部分はどうなってるかしら?
関数ReversedHighLow32()の中身ですね。
あれ?また別の関数を呼び出してる。この32ビットの変数bufferは……バッファ?
uint32_t ReversedHighLow32(uint32_t n) {
  uint32_t buffer;

  return ReversedHighLow32Helper(n, &buffer, (uint16_t *)&buffer);
}
で、この関数が呼ばれるのか。
uint32_t ReversedHighLow32Helper(uint32_t n, uint32_t *pAs32, uint16_t *pAs16) {
  *pAs32 = n;

  uint16_t temp = pAs16[0];
  pAs16[0] = pAs16[1];
  pAs16[1] = temp;

  return *pAs32;
}
えっと、ここの引数はnが入れ替え前の値で、それにバッファへのポインタが2つありますね。
うんうん。pAs32pAs16の2つのポインタが、同じ32ビットのバッファを指しているわね。
あ、2つ目のポインタはuint16_t *ですね。え?これは16ビットでは……?
1つ目のポインタが指している32ビットの値を、2つ目のポインタでは16ビットの値が2つ格納された配列とみなしているわけ。
なるほど!配列だと思えば値を入れ替えられると。
そういうことよ。
そんなテクニックがあるんですねー!
そのテクニックがどうなの?っていうのが今回の問題よ。
えっと、いまいち問題の意味が理解できないんですけど……
そうね。まず今回のプログラムは、ひとまず期待どおりの動作をしているの。
あ、そうか。リリースすると問題が発生するっていう話でしたね。同じプログラムなのに?
そう。開発中とリリース時とでは、コンパイラの設定が違うでしょう?
そっか!ソースファイルが同じでも、コンパイル結果が変わるんですね。
そういうこと!
でも、だからって動作が変わったりします?
変わってしまうかもしれない、危険な書き方があるってことなのよ。
バッファへのアクセス方法に問題がないか考えてみましょう!

空間軸でつくる!

「ソースファイルを分割するときは、スコープを意識するとメンテナンスがしやすくなる」と教えてもらいましたけど、それ以外にも意識すると良いポイントってありますか?
もちろんあるわよ。……それを説明するには、まずは「モジュール」の基本的な考え方について整理しておきましょうか。
「モジュール」ですか!?

ソフトウェアを設計するとき、関連性の高い部分をひとまとめにしたものをモジュールと呼びます。どのようにモジュールに分割するかは、設計における重要な論点です。

一般的に、モジュール内の各機能の関連性が高く(=モジュール強度が強く)、ほかのモジュールへの依存度が低い(=モジュール結合度が低い)ほど良い設計とされています。

各機能の関連性と、モジュール同士の依存度かぁ。……それって、「情報隠蔽」を保つようにスコープを意識しておいたら、自然と良い設計になるってことですよね?
正解!……でもね、モジュールが実装上のどの概念に相当するかは、採用するプログラミング言語の種類によって変わってくるのよ。

オプジェクト指向をサポートする言語では、オブジェクトがモジュールに相当します。一方、C言語が主流だった時代は「どの機能をどのソースファイルに収めるか」というのがモジュール分割の論点でした。

オブジェクト指向については、別のシリーズで詳しく説明します。

いずれにしても、ソフトウェア開発は要求される処理を洗い出すことから始める場合が多いのではないでしょうか。実現すべき処理がリストアップできたら、それらの詳細を明らかにして、モジュールに割り当てていく流れです。

このとき、「まずああして、次にこうして……」と処理の順番を考えて、「時間軸」に沿った設計をしがちです。でも、ある程度大きなソフトウェアを設計するときは、この手法がうまくいく見込みは少ないでしょう。

え、そうなんですか!?
必要な処理の洗い出しを上流工程で完了させられるだけの実力と、かなりの幸運が重ならなければうまくいかないんじゃないかしら。

なぜなら……

  • 要求は増えたり変わったりする
  • 事前に予測できるのは、それらの変更の一部に過ぎない
  • そうして生じた設計の「ひずみ」を、根本的に解決するための時間的な余裕はない
  • その結果、徐々にソフトウェア全体が複雑化していく

こうして、はじめのうちはモジュールが適切に分割されていたとしても、分割すべき場所がいつの間にか変わってしまうことが少なくないのです。でも、分割の境界線を変えるには大きなコストがかかるので、やり直しがきくことは多くはありません。

そうこうしているうちに、どのモジュールが何の処理をするためのものだったのかが分からなくなり、ソフトウェア全体が一つの巨大なモジュールのようになっていきます。複雑すぎて手に負えない迷路のような状態です。

うわぁ、なんだか身に覚えがあるような。
こうした問題を克服するには、「時間軸」で考えるのをやめて、「空間軸」で設計するのがおすすめよ。
……空間軸、ですか?
処理の順番(時間の流れ)ではなく、求められる役割の違い(空間的なつながり)に注目してモジュールの境界線を決定するの。

「オブジェクト指向」について調べたことがある人は、「アクターモデル」という用語を知っているのではないでしょうか。これは、ソフトウェア全体を「アクター」で構成する設計手法です。アクターは、それぞれが自分の役割をもっていて、互いにメッセージを送り合って協調的に動作します。

アクターは、「役者」という意味ですね。
そう。そこで、ソフトウェアを演劇だと思って考えてみましょう。

もし、演劇を「時間軸」で設計するとしたら、「第1幕」や「第2幕」などのモジュールに分割することになります。でも、完成するまでにストーリーは変わるかもしれません。「第1幕のあのセリフは第2幕にもっていこう」などとやっているうちに、モジュールの境界線は曖昧になっていきます。

これに対し、演劇を「空間軸」で設計するのは、「役者(アクター)」でモジュールを分割するということです。舞台というソフトウェアを構成する主要なオブジェクトは、役者たちだからです。それぞれがメッセージを送り合い、自身のキャラクターを演じることによって舞台が進行していきます。

このときポイントとなるのは、役者は舞台上に間違いなく存在する「実体」で、それぞれが「役割」を与えられているという点よ。
なるほど〜。
あと、各自が自律的に振る舞うことによって、ほかの役者とも立派に共演を果たしているわね。
ふむふむ。

ソフトウェアを空間軸で設計するのは、演劇の登場人物をデザインするのに似ています。全体をアクターに分割し、それぞれに役割を与えるのです。これは、ソフトウェアを実在の「もの(=オブジェクト)」の集まりとしてとらえるということです。

オブジェクトごとの役割が明確であれば、要求を満たすために必要な処理を割り振るのも難しくはありません。あとから要求が増えたり変わったりしても、どのオブジェクトに対応させるべきかは明らかでしょう。

演劇で少しくらいストーリーが変更になっても、役者のキャラクターがガラリと変わるわけではないのと同じですね。
そういうこと。その結果、モジュールの境界線が曖昧になっていく問題を避けやすくなるの。

また、このような役割に応じた分割手法には、関連する実装が自然と近くに集まってくるというメリットもあります。必要以上の労力をかけずに、モジュール強度が強く、モジュール結合度が低い状態を保てるのです。

コーディングをする際も、1つのオブジェクトだけに注目すれば済む場合が増えるでしょう。

ソフトウェアをオブジェクトの集まりとしてとらえる、っていう考え方は分かったんですけど……
疑問があるのね?
はい。それって、C言語だとできないってことになりませんか?
なるほど。C言語はオブジェクト指向言語ではないものね。
そうですよ!だからC++にしないと無理なのかなって……
そこは大丈夫。C言語でも同じような考え方はできるのよ。

開発に使う言語をC言語からC++に移行すれば、ソフトウェアをオブジェクトの集まりとして設計しやすくなるのはたしかでしょう。とはいえ現実には、簡単に移行できない場合も少なくないはずです。

それでも、C言語で同様の設計をすることは可能です。「オブジェクトで考える」というのは、「オブジェクト指向言語を使う」こととイコールではないからです。

C++では、オブジェクトの種類をクラス(class)で定義します。クラスは、そのオブジェクトの内部状態を示す変数と、外から見たときの動作を決めるものです。これにより、それぞれのオブジェクトは自律的に振る舞いながら、自分の役割を果たします。

C言語でこれと近いことができるのは構造体(struct)です。オブジェクトの内部状態を示す変数は、構造体に直接含められます。オブジェクトの動作は、構造体を扱う関数として定義すればよいでしょう。

このとき、構造体ごとに別々のソースファイルを作るようにします。そうすれば、構造体を扱う関数も対応するソースファイルに自然と集めやすくなり、モジュールの境界がハッキリします。

C言語でもC++でも、オブジェクトに注目するのは自然な考え方だといえるわね。
そうなんですね。でも、僕にもできるかなぁ……
そうねぇ。プログラム中の要素を収まるべきところに収められたぞ!っていう感覚を大切にしていけば、だんだん慣れてくるんじゃないかしら。
そっか。モジュール分割がうまくできているときは、そういう感覚になりそうですね!
ここがポイント!
時間軸ではなく、空間軸で考えて設計しよう!モジュールの境界線が曖昧になりにくくなって、ソフトウェア全体が複雑化するのを防げます。

第24問の答え

答え

関数を指すポインタを引数にしたいので、こう書くのが正解!
void GreetTo(const char *pName, void (*pGreeter)(const char *)) {
  pGreeter(pName);
}

解説

これは、関数ポインタについての問題でした。

関数ポインタって、C言語のとくに難しいところですよね……
まぁ、「関数ポインタ」って聞いただけで拒否反応を示す人もいるわね。でも大丈夫よ!
ほんとですか?
関数だって、メモリーに格納されている点は変数と一緒じゃない?
はい、一緒ですね。
だとしたら、当然アドレスもあるわよね。
ありますね。
アドレスがあるんだったら、ポインタに格納できるでしょ?
できますね。あ、それが関数ポインタってことですか?
そういうこと!

関数ポインタも、「アドレスを格納した変数である」という点では変数のポインタと変わりません。ただ、そのアドレスに何が格納されているかが違うだけなのです。

今回のプログラムの穴埋め部分は、次のようになっていました。

void GreetTo(const char *pName, /* ここに入るコードは? */) {
  pGreeter(pName);
}

この関数の第2引数にどうやって関数ポインタを書けばいいのか考えてみましょう。

関数を呼び出している部分では、次のようにMorningGreeter()EveningGreeter()を渡しています。

  GreetTo("Leo", MorningGreeter);
  GreetTo("Yuki", EveningGreeter);

これら2つの関数は、次のような形をしていました。

void MorningGreeter(const char *pName) {
  ……
}

void EveningGreeter(const char *pName) {
  ……
}

2つの関数は名前が違いますが、引数や戻り値の型は同じですね。そのため、どちらも同じ型の関数ポインタに代入できます。

また、関数ポインタに代入した関数は、通常の関数のように括弧を付けて呼び出すことも可能です。この括弧は「関数呼び出し演算子」と呼ばれます。関数呼び出しは、実は演算の一種なんですね。

  void (*pGreeter1)(const char *) = MorningGreeter;
  pGreeter1("Leo");

  void (*pGreeter2)(const char *) = EveningGreeter;
  pGreeter2("Yuki");
あぁ、これですよ。関数ポインタって、この見た目が怖くないですか?
そうねぇ、たしかに文法がややこしいところはあるのよね。でも、それだけのことなのよ。

第23問で、typedefを使ってポインタの配列を分かりやすくしたのを覚えているのでしょうか。関数ポインタにも、同じテクニックが使えます。同じ型の関数ポインタを繰り返し使うときは、このように書けば分かりやすいでしょう。

  typedef void (*Greeter)(const char *); /* この関数ポインタの型を「Greeter」と呼ぶことにする */

  Greeter pGreeter1 = MorningGreeter;
  pGreeter1("Leo");

  Greeter pGreeter2 = EveningGreeter;
  pGreeter2("Yuki");
あ、pGreeter1pGreeter2が関数ポインタの変数名だったんですね!
ふふふ。分かったみたいね。
「見えた!」って感じですかね。だいぶ怖くなくなったかも。
関数ポインタはね、見慣れない文法のせいで敬遠されがちだけど、もともと怖いものではないのよ。
そっかぁ。僕も今まで見た目で誤解してたかもなぁ……

さて、問題の穴埋め部分の答えは、もう書けるのではないでしょうか。正解は次のようになります。

void GreetTo(const char *pName, void (*pGreeter)(const char *)) {
  pGreeter(pName);
}

または、typedefを使って次のようにしてもいいですね。

typedef void (*Greeter)(const char *);

void GreetTo(const char *pName, Greeter pGreeter) {
  pGreeter(pName);
}
ここがポイント!
関数ポインタは、実は見た目ほど怖くない!
あのー、関数ポインタを怖がる必要はないってことは分かったんですけど……
うんうん。
そもそも関数ポインタの使い道って、そんなにあります?
あるのよ。コールバックが欲しいときなんかに、よく使うわね。
やっぱり、よく使うんですね。で、コールバックってなんですか?
先に関数を渡しておいて、あとで呼び出してもらう手法のことよ。
あとで……?呼び出してもらう……?
そうねぇ。C言語では、標準関数のatexit()qsort()が有名かしら。

関数ポインタを使ったコールバックについて、ここではatexit()の使用例を紹介しておきましょう。次のように事前に関数を渡しておくと、それをプログラム終了時(at exit)に呼び出してくれます。後片付けが必要な場合などに便利ですね。

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

void ShowFinalMessage(void) {
  puts("Bye!");
}

int main(void) {
  atexit(ShowFinalMessage); /* あとで呼び出して欲しい関数を渡しておく */

  puts("Hello!");

  return EXIT_SUCCESS;
}
実行結果
Hello!
Bye!
あとで呼び出してもらうって、こういうことだったんですね!
だいぶイメージがわいたみたいね。
はい。もう関数ポインタは怖くないので、チャンスがあったら使ってみたいです!

修正後のプログラム

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

void GreetTo(const char *pName, void (*pGreeter)(const char *)) {
  pGreeter(pName);
}

void MorningGreeter(const char *pName) {
  printf("Good morning, %s!\n", pName);
}

void EveningGreeter(const char *pName) {
  printf("Good evening, %s!\n", pName);
}

int main(void) {
  GreetTo("Leo", MorningGreeter);
  GreetTo("Yuki", EveningGreeter);

  return EXIT_SUCCESS;
}
実行結果
Good morning, Leo!
Good evening, Yuki!

第24問

以下は、名前を指定しながら種類の異なるあいさつを表示するプログラムです。ただし、コメントの部分に入れるべきコードが隠れています。

どんなコードが入るか分かりますか?
main.c
#include <stdio.h>
#include <stdlib.h>

void GreetTo(const char *pName, /* ここに入るコードは? */) {
  pGreeter(pName);
}

void MorningGreeter(const char *pName) {
  printf("Good morning, %s!\n", pName);
}

void EveningGreeter(const char *pName) {
  printf("Good evening, %s!\n", pName);
}

int main(void) {
  GreetTo("Leo", MorningGreeter);
  GreetTo("Yuki", EveningGreeter);

  return EXIT_SUCCESS;
}
実行結果
Good morning, Leo!
Good evening, Yuki!
もう1問、穴埋め問題をどうぞ。
はい。第23問と似てますね。
そうね。でも、もっと柔軟なあいさつを表示できるプログラムになってるのよ。
えっと、あいさつを表示する関数はこれか……
void GreetTo(const char *pName, /* ここに入るコードは? */) {
  pGreeter(pName);
}
この関数の第2引数をどう書くか、っていう問題ですね。
そうそう。
あいさつする関数を呼び出している箇所は……
  GreetTo("Leo", MorningGreeter);
  GreetTo("Yuki", EveningGreeter);
このMorningGreeterとかEveningGreeterって、少し上のほうで定義されてる関数と同じ名前になってますね。
同じというか、それは関数名よ。
なんと!
関数の引数に関数を渡すには?