間違ったコードが間違って見えるコーディング規則を探すこと。プログラミングにおける変数や関数、クラス設計の命名というのは、そのプログラムの可読性を左右する最も重要な要素のひとつです。変数名や関数名が適切であれば、プログラムの流れも読みやすいですし、バグがあっても見つけやすいものです。
Joel Spolsky
さて、このような命名規則(命名規約)といったものは、大抵それ読む人間のために設定されます。コンパイラにとっては、変数名がoldUserNameだろうがounだろうがxだろうが、関係ありません。変数名は単なる記号です。
しかし、考えて見るとこれは非常にもったいない。人間が、変数名からエラーを発見できるということは、このルールを(広い意味での)コンパイラに教えてあげれば、同じようにエラーを発見してもらえるのではないでしょうか。
例えば以前に次のようなバグコードを見たことがあります。
.... len = strlen(name); strncpy(buf, name, sizeof(len)); ....ひとめで何がおかしいか分かるでしょうか。答えは、strncpyにsizeof(len)を渡しているのが間違い。sizeof(buf)のような与え方をすることも多いので、うっかり間違えたのでしょう。このようなコードは人間がよく見ればわかりますが、コンパイラにとっては何が間違いかはわかりません。C言語上の型チェックとしては、どちらもsize_tが渡るので問題ないと解釈されてしまいます。よって、この問題をコンパイラがコンパイル時に(静的に)発見することはできません。
ということで対策案。上の例でいえば、いたって単純な方法が考えられます。まず、lengthのようなものをsizeofでサイズ取得するというのは、あまりなさそうです。よって、sizeof(len.*)というパターンをソースでgrepしてみると。ものすごくピンポイントな対応ですが、このパターンでみつけられバグは結構ありそうに思います。
ようは、命名規則がきまっていれば、単なる記号論理だけでなく、意味論までふみこんだ静的解析ができるんじゃなかろうかと。アプリケーションハンガリアンと呼ばれる変数の意味的な側面に注目してプレフィックス等の命名ルール呼ばれる命名規則を決める手法(参考:間違ったコードは間違って見えるようにする - Joel on Software)がありますが、これも人間に臭うコードが見えるようにするというだけではもったいない。ある程度ルールを決めれば、コンパイル時点でCPUに検出させることもできるのではないかと思うのです。
例えば、次のようなものもうまくやれば検出できそうです。
/* database中のoldをnewに置き換え */ void replace(DB* db, const char* old, const char* new); ... replace(db, new, old); ....プロトタイプでは、newもoldも同じ型ですが、プロトタイプの変数名を見ればold->newの順番だとわかります。ところが、通常のコンパイラでは、上のようにnewとoldがいれかわっていても検出することはできません。おそらく人間でも直感的には間違いに気が付きにくいと思いますが、これもせっかくの仮引数名を利用してやれば、何とか検出できそうです。
意味論の解釈というのは難しいとはいえ、発想自体は単純な話ではあるので、すでにいろいろツールありそうに思って探したのですが、見つかりません。CheckStyleのような命名規則をチェックしてくれるものがあるので、これをカスタマイズすればできるのでしょうか。単なる規約以上は無理なのか・・
ご存知の方おられたらぜひ教えてください m(_ _)m。
【関連記事】
・変数の命名規則
・テキストとしてのコーディング規約
・コンパイル時の静的チェック(STATIC_ASSERT)
【関連書籍】
・Joel on Software Joel Spolsky 青木 靖
・組込みソフトウェア開発向けコーディング作法ガイド[C言語版] SEC
・C++ Coding Standards―101のルール、ガイドライン、ベストプラクティス ハーブ・サッター他
・リファクタリング―プログラムの体質改善テクニック
・プログラミング作法
【関連リンク】
・間違ったコードは間違って見えるようにする - Joel on Software
・ハンガリアン記法 - 悪態のプログラマ
デザインはわがまま→思いやり
川崎和男 『ドリームデザイナー』
最近はユニバーサルデザイン(UD)という言葉を良く耳にするようになりました。はてなキーワードの説明では、『ユニバーサル=普遍的な、全体の、という言葉が示しているように、「すべての人のためのデザイン」を意味し、年齢や障害の有無などにかかわらず、最初からできるだけ多くの人が利用可能であるようにデザインすること。』となってます。つまり、全ての人にやさしいデザインということになるでしょうか。
この「ユニバーサル」つまり「全ての人に」という部分は非常に重要な考え方だと思いますが、やはり人によって得手不得手がありますので、ものによっては、どうしてもこっちを立てればこっちがたたずといった状態になります。こうなると、どこを立てるか、つまり、どんな評価関数でもって最適化をかけるかということが重要になります。
さて、こんなことを書いているのは最近Gコードの構成についてなるほどーっと思ったのがきっかけです。
Gコードといえばテレビ番組の予約等で使われる数桁の数字。新聞のラテ欄やTV雑誌でおなじみですね。といっても、最近は、EPG搭載のデジタルTVやDVD/HDDレコーダが普及して、あまり使われなくなった感がありますが。さて、Gコード、原理的には、数文字の数字に、番組の開始・終了日時、チャンネル情報を符号化したものになっています。ですので、この数字を対応のVHSビデオやDVDレコーダに入力すると、これら予約録画に必要な情報に変換して、予約録画ができるという仕組みです。
はてさて、このあたりまでは使われたことがある方ならご存知かもしれないのですが、今回私がなるほどと思ったのは、この数字の符号化方法にあります。Gコードの数字列は可変長コードになっているのですが、この数字列、ゴールデンタイムの番組、つまり視聴率の高そうな番組程短くなるように符号化されているのです。いわゆる、エントロピー符号化というやつです。時には、一文字のこともあるそうです(※1)。知ってました?
つまり、Gコードの符号化設計における評価関数はユーザーの予約頻度なんですね。
で、ここでふと思ったのは、「では、今のGコードの符号化って本当に使われ方に対して最適化されているの?」ということ。実際の仕様は公開されていませんが、個人で解析されている方の情報等をみると、結構こったことになっているようですが、そもそもいろんな趣向を持った人がいるのに、最適化ってどうするのだろうという疑問。まず単純に考えると、時間帯や番組長、チャンネル等で予約頻度順に符号長も短くなるようにするという方法。つまり、世の中の全予約の平均長を短くするという方針。単純に考えるとこうなるのでしょう。多分Gコードもそういう考え方がもとになっているのだと思います。
しかし、よく考えれば、Gコードを使う人って機会慣れした若者から不慣れなおじいちゃん、おばあちゃんまでいろいろおられるはず。となると、「各番組の予約頻度」×「その番組の視聴者層の機械慣れ具合」といった重み付け後の値でもって最適化を考えないといけないのかとかも考えられます。
いやいや、そもそも機械慣れしているなら少々数字が多くても大丈夫だから、この層は一旦おいておいて、おじいちゃん・おばあちゃん層の番組の最適化を進めるというのもひとつの考え方としてあるだろうとか。。
考え出すとこれがなかなか奥が深い。
プログラミング時には、アルゴリズムの性能指針として、平均オーダーと最悪オーダーが使われます。平均と最悪、どちらが特に重要かは、ユースケースによってかわってきます。この考え方は、ユーザビリティを考える時もこれは同じ。先の例では、全番組予約頻度で符号化長を調整するのは平均値の最適化。おじいちゃん・おばあちゃん優先で最適化するのが最悪値の最適化といえなくもないでしょう。
自分自身ふりかえると、大抵の設計時には、ついつい平均値優先になっていたなぁという気がします。きっと、その方がわかりやすいからでしょう。
冒頭の「デザインはわがまま→思いやり」というのは、デザインディレクター 川崎和男氏の言葉。『考具―考えるための道具、持っていますか?』という本の中で紹介されていたので知ったのですが、とても大好きな言葉です。ユーザビリティは、単純に数学的に最適化できるものではありません。最後のまごころ忘れちゃいけませんね。
※1 2005年6月1日放送のNHKきょうの料理では、一部新聞でGコードが「1」と掲載されたため、同番組の出演者が珍しく「当番組をぜひ見てほしい」と宣伝した事もあるんだそう。
【関連書籍】
・誰のためのデザイン?―認知科学者のデザイン原論 (新曜社認知科学選書) ドナルド・A. ノーマン
・ユーザビリティエンジニアリング原論―ユーザーのためのインタフェースデザイン ヤコブ・ニールセン
・アフォーダンス-新しい認知の理論 佐々木正人
・暗号解読 上巻 (1) (新潮文庫) サイモン・シン 青木 薫
【関連記事】
・できることが増えると、できないことが増える
・プログラム+インターフェース=モジュール
・似て非なるインターフェース
・選択できない選択肢
【関連リンク】
・Gコード解析(analyzing VCR-plus code)
・ユニバーサルデザイン - コクヨ
・Webデザイン・Webデザイナーのまとめサイト | Webデザインに優れたサイトのリンク集
・ヤコブ・ニールセンの考えをまとめたユーザビリティガイドライン
・Jakob Nielsen博士のAlertbox
・楽天はデジタル家電の新製品を随時ご紹介
良い結果をもたらす嘘は、不幸をもたらす真実よりいい。すっかり暑くなってエアコンが恋しい季節になってきました。この季節になると、毎年話題にあがってくるのが省エネや環境問題、エコといった話。去年はクールビズがずいぶんとはやりました(私はもともとスーツ着る機会が少ないので、伝聞状態でしたが)。今年はどうなんでしょう。
ペルシアの諺
さて、環境問題というと、少し前に「環境問題はなぜウソがまかり通るのか」という本が話題になっていました。関西のTV番組 たかじんのそこまで言って委員会でとりあげられたのがきっかけで、かなり売れたようです。その内容は、官主導のリサイクル運動が隠してきた非効率性と利益誘導の実態をあばくというもの。例えば、- 地球が温暖化しても、海の水位は上昇しない
- 猛毒に仕立て上げられたダイオキシン
- 回収されたペットボトルはほとんどがリサイクルされていない
・『環境問題はなぜウソがまかり通るのか』のウソ
常識をくつがえすような内容というのは読んでいて面白いのですが、手放しで信じるのは、結局危険かなと思います。でもまぁ、今の環境政策には何かしら問題はあるのは間違いなさそうですので、いいきっかけにはなったんじゃないかなとは思います。
ちなみに、この本の内容がどうであれ、環境について何も考えなくていいということはありません。日本が欧米に比べてましであっても、環境が確実に壊されているのはまぎれもない事実でしょう。そんな中で、ちょっとした意識をもって行動するだけでも、それが積み重なれば大きな効果になります。リサイクルなんていわなくても、ちょっと水を節約する、エアコンの温度をしぼるなんてことでも十分です。というのは、自分に言っているのですけどね ^^)
さて、前ふりだけで終わりそうですが、もともとはソフトウェア開発と環境問題について書こうと思ってました (^^)。ものづくりで環境というと、例えば、省電力だとか材料の選択だとかハードウェア的な話が中心で、ソフトウェアはあまり関係ないのかなと思われることもあります。しかし、組込み機器等では、ソフトウェアのつくりによって、省エネになったり、材料がへったり、あるいは製品が長持ちしたりということにはつながります。例えば、次のようなもの。
- メモリ効率のよいプログラム設計により搭載メモリが減る
- ハードウェアアクセスを最小限にして、必要最低限の電源供給におさえる
- ソフトの起動速度を向上させて、電源をこまめにきれるようにする
- 開発プロセスを改善して開発効率をあげる。これで作業時間が短くなり、開発にかかるエネルギーを削減する
- あるいは、それを実現するようなソフトや製品をつくって、ユーザの活動エネルギーをおさえる
嫌々仕事にせず、まずはいいものをつくる。これこそ一番のエコ活動なのかもしれませんね。
最後にひとつHPを紹介。去年も紹介したEcotonoha。今年もやっています。素敵です。
【関連記事】
・ペーパーレス - 電子化からもう一歩
【関連書籍】
・省メモリプログラミング―メモリ制限のあるシステムのためのソフトウェアパターン集 James Noble
・組込みソフトウェア開発向けコーディング作法ガイド[C言語版] SEC BOOK
・組込み現場の「C」プログラミング―基礎からわかる徹底入門 SESSAME
・ハッカーのたのしみ―本物のプログラマはいかにして問題を解くか Henry S. Warren
・環境問題はなぜウソがまかり通るのか 武田 邦彦
・不都合な真実 アル・ゴア 枝廣淳子
【関連リンク】
・Ecotonoha (エコトノハ)
急なれば標を治し、緩なれば本を治す実に久々の記事です (^^;)。今回はバグ修正のリスクについて書いてみます。
医療用語には「対症療法」と「原因療法」という言葉があります。表面的な症状の消失あるいは緩和を主目的とするのが「対症療法」、症状の原因となる疾患そのものを制御する治療が「原因療法」です。 完全回復や再発防止のためには原因療法が必要になりますが、事情によりそれがとれない時に対症療法がとられます。
対症療法を選択する理由はいくつかパターンがあります。そのひとつは、症状の原因が不明な場合。この場合、分かっているのは症状のみですから、対症療法以外すべがありません。原因はわかっていても有効な対策がわからない場合も同じです。風邪薬なんていうのはそうですね。
もうひとつのパターンは、原因療法に伴うコストやリスクが大きいた場合。手術をするにもお金がかかりすぎるとか、体力がもたないとかいう場合がそうです。その他、原因療法を施す前の応急処置として対症療法がとられることもあります。原因療法と対症療法は、排他的なものではなく相互に補完し合う関係にあります。
さて、この話は、ソフトウェア開発におけるバグ修正にも当てはまります。ソフトウェア開発における実装フェーズでは、バグの検出とその修正というサイクルが繰り返されます。このバグつぶし、デバッグという作業は病気の治療に似ています。そして、それは基本的には、根本原因をさぐってそれを直す原因治療として行われます。しかし、これが製品リリース直前、あるいは、既にシステムの運用開始後、パッケージや組込みソフトウェアであれば製品発売してしまった後だったらどうでしょう。この場合は、不具合の原因がわかっていても、原因療法ではなく対症療法がとられることがあります。これは、修正のリスクを考慮してのことです。
このような運用直前あるいは運用中のケースでは、システムの品質は、一度は確認されているはずです。そして、バグの修正というのはそのシステムに変更を入れるわけですから、その部分の再確認が必要になってしまいます。しかし、大規模なシステムの検証コストは莫大です。全ての検証をやり直すなんてことはもちろんできません。よって、修正の影響範囲をしぼり、検証の範囲もしぼるということが必要になってきます。そこで、広い範囲に影響が及ぶような原因療法ではなく、見つかったバグの発生条件の場合のみに影響するような対症療法を選択するようなケースが出てくるのです。
別の医療用語に「トリアージ」という言葉があります。災害医療において、多数の傷病者を重症度と緊急性によって分別し、その対応方法を決める手順を指す用語です。このような緊急の場面では、重篤であっても救命不可能な患者は切り捨てられます。非常につらい選択ですが、できるだけ多くの命を救うためには必要な選択です。
運用直前や運用中のバグ修正も同じです。修正の時間は限られています。複数のバグがあったとしても、全てを修正して検証する時間はないかもしれません。つまり、バグの取捨選択が必要になります。先に書いたように対症療法でしのぐことによって検証時間を節約するというだけでなく、そもそも直さずにそのままにするという選択も時には必要です。検証コストを削減する一番の方法は、修正しないことですから。
分かっているバグを直さない、あるいは、原因がわかっていて根本修正もできるのに、表面的な回避しかしないというのは、プログラマにとっては非常に嫌な選択です。バグが見つかったら直したいというのが大半の意見でしょう。しかし、ソフトウェアの修正には、常にリスクが伴うということを忘れてはいけません。例え修正が正規のものであっても、隠れたバグをエンバグさせる可能性だってあります。マイナス×マイナスでプラスになっていたものが、マイナスになるかもしれないのです。修正したい気持ちは非常に非常に良く分かりますし、つらいのですが、ここはぐっと我慢が必要です。エレガントさよりも品質確保が重要なのです。
ソフトウェア開発の現場では、「動いているコードはさわるな」ということがよく言われます。これは上のような話が背景にあります。しかし、これも絶対ではありません。時と場合によります。運用直前や運用中は、上のような対症療法が有効な場合が多いですが、開発に余裕がある場合は、原因療法でしっかり直しておくべき場合がほとんどです。所詮、対症療法は一時凌ぎにすぎません。安易な解熱剤や鎮痛剤の服用と同じで、中途半端な回避コードは、不具合を潜伏させ、自体をより悪化させることすらあります。ですので、対症療法をとった箇所はきちんと管理し、適切なフォローをすることを忘れてはいけません。
不具合のない品質の高い成果物を作ることがプログラマ・エンジニアの役目です。この時、理想論に走るのではなく、限られたリソースの中でいかにそれを最大化するかを意識することが、プロのエンジニアとしては必要です。
# もちろん理想を追うのもわすれちゃいけません ^^)
【関連記事】
・バグを潜伏させない工夫
・デバッグと不確定性原理
・デバッグ指向のススメ
【関連書籍】
・ソフトウェアアーキテクチャ―ソフトウェア開発のためのパターン体系 F. ブッシュマン他
・ソフトウェア開発の持つべき文化 IT Architects' Archive ソフトウェア開発の課題1
・開発の現場 vol.007 - 運用・保守
・ソフトウェアパターン再考―ソフトウェア品質学 各論 パターン発祥から今後の展望まで
・達人プログラマー―システム開発の職人から名匠への道 Andrew Hunt
・リファクタリング―プログラムの体質改善テクニック
愚者が賢者から学ぶことよりも、賢者が愚者から学ぶことのほうが多い。今回の記事のテーマは「コーディング規約」です。コーディング規約とに含まれる、変数や関数の命名規則やら、コメントの書き方等の話題はここでも何度か取り上げましたが、今回は、コーディング規約の「テキスト」としての側面に焦点を当てたいと思います。
モンテーニュ
「コーディング規約」や「コーディングルール」というと、何だか型にはめられるという気がして、拒否反応を示す方もおられるでしょう。確かに「規約」とか「ルール」とかいう単語が入っているので、そのようなイメージは強いでしょう。しかし、中には「制約」というよりは「ガイドライン」と呼ぶべきものも多く含まれています。
ここで、コーディング規約導入の必要性について改めて整理してみます。細かく分類することも可能でしょうが、大きくは次の二つに大別できるかと思います。
- スタイルを統一することで可読性を改善する。
- 危険なコーディングを禁止し、品質を確保する。
さて、この後者の方ですが、内容的には規約というよりはガイドラインといった方が良いかもしれません。この点を積極的に利用すると、コーディング規約をプログラミングのテキスト・教科書としてとらえることができます。
以下の表に組込みソフトウェア開発力強化推進委員会による「コーディング作法ガイド」の一部を抜粋してあげさせて頂いています。それぞれの項目は、入門書でもよく言われるものから、意外と見落とされがちなもの、複数人の開発において重要となるルール的なもの、はたまた「実行時の利用スタックサイズが予測できないため再帰呼び出し関数は使用しない」といったような組込み特有のものまで、広範囲にわたってまとめられています。
|
構造体や共用体の比較にmemcmp を使用しない。 構造体や共用体のメモリには,未使用の領域が含まれる可能性があり,その領域に何が入っているか分からないので,memcmp は使用すべきでない。比較をするなら,メンバ同士で行う必要がある。また,構造体のメンバの詰め物,境界調整は処理系定義である。 |
<適合例>
struct {
char c;
long l;
} var1, var2;
void func() {
if (s1.a == s2.a && s1.b == s2.b) {
/* …*/
<不適合例>
struct {
char c;
long l;
} var1, var2;
void func() {
if (memcmp(var1, var2, sizeof(var1)) == 0) {
/* … */
|
|
浮動小数点型変数は厳密な等式,非等式の比較はしない。【MISRA 13.3】 浮動小数点型は,見た目の値と実装された値は完全に一致していないので,比較は許容誤差を考慮して判定する必要がある。 |
<適合例>
#define LIMIT 1.0e-4
void func(double d1, double d2) {
double diff = d1-d2;
if (-LIMIT <= diff && diff <= LIMIT) {
/* … */
<不適合例>
void func(double d1, double d2) {
if (d1 == d2) {
/* … */
|
|
下線で始まる名前(変数)は定義しない。 言語規格では,次の名前を予約済みとしている。 (1) 下線に続き英大文字1 字または下線に続き,もう1 つの下線で始まる全ての識別子は,いかなる使用に対しても常に予約済みである。 例: _Abc, __abc (2) 1 つの下線で始まるすべての識別子は,通常の識別子及びタグ名前空間予約済みの名前を再定義した場合の動作は未定義である。 1 つの下線で始まり,小文字が続く名前は,ファイル有効範囲以外の部分では予約されていないが,覚えやすい規則とするため,下線で始まる名前すべてを使用しないという規則としている。 |
<不適合例>
int _Max1; /* 予約されている */
int __max2; /* 予約されている */
int _max3; /* 予約されている */
struct S {
int _mem1; /* 予約されていないが使用しない */
};
|
| 実行時の利用スタックサイズが予測できな いため再帰呼び出し関数は使用しない。 【MISRA 16.2】 |
内容は非常に充実しており、C言語プログラミングの初心者の方はもちろん、中級者以上の方にとっても、実践的な注意点が体系にまとめられており、非常に参考になると思います。例を見て頂けると分かるかと思いますが、危険なコーディングを禁止というよりは、「こんなことをするとこういうことになります。知ってますか?」というような失敗例・アンチパターンのカタログのようなものになっています。実際これらの多くは、言語仕様の勉強やサンプルコードだけではなかなか見えてこないもので、バグの定番のようなものが多いです。とはいえ、実際にバグを生めて苦い経験をしなくとも、事前にこのような失敗例を知っていることで、バグは回避できます。また、なぜそのようなコーディングがNGなのかを考えることで、言語仕様やプログラミングについての理解は深まります。
上の表のオリジナルはIPAのHPでPDFが無料配布(コーディング作法ガイド 0.8)されており、書籍化されたもの(組込みソフトウェア開発向けコーディング作法ガイド[C言語版])もあります。
無料で手に入るものですので、初心者以外の方も一度目を通してみられることをおすすめします。
(初出「組込みプレス Vol.3」を元に改稿)
【関連記事】
・変数の命名規則
・ビットシフトの落とし穴 - 算術シフトと論理シフト
【関連書籍】
・組込みソフトウェア開発向けコーディング作法ガイド[C言語版] SEC
・C++ Coding Standards―101のルール、ガイドライン、ベストプラクティス ハーブ・サッター他
・リファクタリング―プログラムの体質改善テクニック
・プログラミング作法
・組込みプレス Vol.3
失敗はつまずくことではない。つまずいたままでいることだ。またまた更新間隔が開いてしまったので、久々の更新です(^^;)。今回は「エラー処理」を取り上げてみます。「エラー処理」という単語もあいまいなのですが、今回とりあげるのは「あるメソッド・関数が本来の処理を達成できなかった場合の処理」といったところです。
■エラーを伝える
さて、処理が失敗した時の最も基本的なパターンは、戻り値としてエラー値を返すことです。このような関数・メソッドは数多くあります。例えば、POSIX系のclose()関数をみてみましょう。int close(int fd);これはエラーで0以外の値が返ります。このように、エラーか否かを戻り値で返すものの他に、本来の戻り値がとり得ない値をエラーの意味として返すものもあります。例えば、POSIX系のopen()関数です。
int open(const char *pathname, int flags, mode_t mode);この関数は戻り値としてfd(file descriptor)を返しますが、エラー時は-1を返す仕様です。
どちらにせよ、この関数を使用する側としては、戻り値を確認することによって、エラーが起こったか否かを確認することができます。
■エラーを処理する
さて、大半の関数がこのようにエラーを戻り値で通知してくれるわけですが、そのようなエラー値を見つけた場合、一体どうすればいいのでしょうか。大抵の場合、次の3種類に分けられるでしょう。- 1. エラー処理としてユーザーへの通知や、代替処理、後始末などを行う。
- 2. エラーを無視する。
- 3. 処理をそこで止める。
しかし、実際にこのようなエラー処理が行われるのはごく一部です。大抵の場合は、2の「無視」に落ちることが多いと思います。
大抵の関数はエラーを返してくれるのですが、実際全ての関数ごとにエラー処理をするのは難しい、あるいは無理がある場合がほとんどです。例えば、バグでしかエラーになりえないものに対して、適切なエラー処理を与えるというのは不可能でしょう(全てのprintfのエラー見てる人なんていませんよね)。それに、全ての関数毎にエラーを処理しようとすると、次のようなすさまじい入れ子ができあがりかねません。
/* 処理A, B, C, Dを行う */
if (func_A(x, y) == 0) {
...
if (func_B(x) == 0) {
...
if (func_C(y) == 0) {
...
if (func_D(x, y) == 0) {
...
open()のようなI/Oのための関数は、途中でエラーが発生する場合もあると思いますが、論理的な関数の場合は、最初の関数が成功すれば、あとは絶対成功するはずという場合もあります。このような場合であれば、確かにエラー処理は必要ありませんし、しようもありません。しかし、エラー処理が必要ないというのと、エラーをチェックしなくて良いというのは違います。つまり、単純に無視していいとは限りません。そこで、3の「処理を止める」というのが登場します。■見てみぬふりをしない
次の例を見て下さい。... pthread_mutex_lock(&mutex) shared_variable ++; pthread_mutex_unlock(&mutex) ...いたって単純なmutexでの排他制御コードです。ここでmutex_lockはエラーを返す可能性がありますが、エラーは確認されていません。mutex_lockがエラーになるのは、初期化忘れや、同じスレッドでの二重取得等ですが、自身での本来、このような例でエラーが確認されない理由は、それは外的要因ではなく、単なるバグです(trylockと違い、busyならまたされるだけ)。よって、ユーザーに通知するという必要もありませんし、エラー処理で復帰できる望みも薄そうです。では、どうするか。このコード例では特にエラーを見ずに無視していますが、これでいいのでしょうか。
プログラムにバグがなければ、自スレッドから再度mutexをとってみたり、初期化されていない構造体を渡したりということは発生しません。しかし、開発中にはこのような不正規な処理のために、よくデッドロックを起こしてしまうことがあります。しかも、このようなバグは、タイミング依存になったりして、なかなか発見されません。
しかし、この例の場合、関数(pthread_mutex_lock())のエラーを見ていれば、すぐに分かったはずなのです。
よって、ここは以下のようにすべきでしょう。
... ret = pthread_mutex_lock(&mutex) assert(ret == 0); shared_variable ++; ret = pthread_mutex_unlock(&mutex) assert(ret == 0); ...assertにひっかけてやることで、おかしな動作をした場合、すぐに 検出できます。
バグというのは、発生地点から離れれば離れるほど解析が難しくなりますので、このように発生時点で捕まえるというのは非常に重要です。
なお、標準のassertマクロなら、リリース時に消すこともできまし、検出時に行数も出してくれますので、リリース物の効率にも影響を与えません。
■ライブラリの中心でエラーを叫ぶ
さて、上記の例では、エラー値を見て処理を止めていました。しかし、エラーを起こしてはいけないのはここ部分のコードだけではないでしょう。mutex_lockを使用しているコード全般に言えることです。であれば、mutex_lockしている方ではなく、mutex_lockの方でassertにひっかけてやる方がいいのではないかということも考えられます。その通りです。pthread_mutex_lockの例で言えば、エラーを返す前に、abort()するようなコードを入れておけば、不正な処理が行われた段階でプログラムは停止します。デバッガを繋ぐなりしてあれば、バックトレースでどこが問題かもすぐ解析できます。
しかし、この方法はライブラリのソースコードがない場合はそうはいきません。また、自分がライブラリを作成する側の場合、ライブラリ内で勝手にabortするのは、常に期待値とはいえません。自分がライブラリ作成者である場合は、ライブラリ使用者のために、エラー時の挙動を選択できるようにしておいてやるというのもいいでしょう。例えば、
- コンパイル時にエラーを返す前の処理(ログを出力、abort、HOOK関数を呼ぶ等)を選択できるようにする
- 環境変数やAPIでエラーを返す前の処理を選択できるようにする
エラー通知の重要な役割の一つは、上位モジュールの間違いを指摘することです。そして、その目的を達成するためには、単なるエラー通知だけでなく、ログやabort,フック関数の呼び出し等とうまく組み合わせることによって、その効果は何倍も高まります。
■例外スローの場合
以上では、関数リターンとしてエラー値を返すというパターンで説明しましたが、C++やJavaの例外スローも同じようなことが言えます。例外の場合、
try {
doAction();
} catch (...) {
/* 無視 */
}
として、とりあえずterminateで落ちるのだけを防ぐといったコードを時々見かけます。しかし、これはエラーを無視して突き進み、事態をどんどん悪化させることにつながりかねません。例外もエラー同様に、どこでどのように処理すべきかということを明確にした上で、設計しないと行き場のない例外が氾濫することになります。
せっかくのエラー通知。しっかりと耳を傾けてあげましょう。
【関連書籍】
・デバッグルール - 9つの原則、54のヒント David J.Agans
・実践マルチスレッドプログラミング スティーブ・クライマン バート・スマールダー ディバン シャー
・組込み開発者におくるMISRA‐C―組込みプログラミングの高信頼化ガイド MISRA‐C研究会
・C++プログラミングの処方箋―ひと味違うコードを書くための99の鉄則 Stepehn C. Dewhurst
・Cプログラミングの落とし穴 A.コーニグ
・組込みソフトウェア開発における品質向上の勧め―コーディング編 情報処理推進機構 SEC
・Effective C++ 【改訂第2版】 Scott Meyers
・組込みプレス Vol.3
アルゴリズム + データ構造 = プログラム最近しばらく関数やライブラリのインターフェースについての雑感を書いてきましたが、プログラムにおけるインターフェースの位置付けに関して面白い記事があったのでその紹介と感想を。
Algorithms + Data Structures = Programs
N.ヴィルト
その記事と言うのは、Rogue Engineer's Diary/やさぐれ日記さんの「「アルゴリズム+データ構造=プログラム」? 本当に?」という記事。その内容は、前回の記事でも紹介したN.Wirth氏著の古典的名著のタイトルでもあり、プログラミングにおける格言とも言える「アルゴリズム+データ構造=プログラム」という考え方には、「インターフェース」の観点が抜けているんじゃないか、「アルゴリズム+データ構造+インターフェース=プログラム」とするべきじゃないか、といった内容。
なるほど。うまい表現です。でも、個人的には、もうちょっと表現を変えて次のような表現にした方がしっくりきます。
- アルゴリズム+データ構造=プログラム
- プログラム+インターフェース=モジュール
考え方をの単純な言葉で表現してみるというのは、考え方が整理されて気持ちがいいですね。
【関連記事】
・似て非なるインターフェース
・関数のユーザビリティ
・選択できない選択肢
【関連リンク】
・「アルゴリズム+データ構造=プログラム」? 本当に?(Rogue Engineer's Diary/やさぐれ日記)
【関連書籍】
・オブジェクト指向における再利用のためのデザインパターン
・Javaの格言―より良いオブジェクト設計のためのパターンと定石 Nigel Warren
・Algorithms + Data Structures = Programs Niklaus Wirth
・珠玉のプログラミング―本質を見抜いたアルゴリズムとデータ構造 - Jon Bentley
外見というものは一番ひどい偽りであるかもしれない。引き続き、プログラミングにおける関数(ライブラリ)インターフェースの話を。
シェークスピア
今までに生み出されてきたプログラムの数は数知れず。さらにこうしている今も世の中では無数のソースコードが生み出されています。とはいえ、そのうちのほとんどはそれほど新規性のあるものではないでしょう。細かい違いはあれど、基本的なロジックは既にどこかで作られたものものを焼き直し、組み合わせているだけです。
「アルゴリズム+データ構造=プログラム」と言えば、Pascalの設計者であるNiklauth Wirth氏によって書かれた古典的名著ですが、まさに言い得て妙。「アルゴリズム」と「データ構造」はまさにプログラムの基本構成要素であり、定石とも言えるパターンがあります。クイックソートや二分探索のアルゴリズムや、リスト構造や配列、ハッシュ等のデータ構造。それらの計算オーダー、メモリ効率。。。プログラミングのテキストには必ず出てきます。テキスト上で眺めている間は退屈なものですが、基本的なパターンを知っておくことは実際にプログラムを書く上では重要です。このようなテキストにあるような基本パターン以外にも、既存のソースコードからは、パターン化されたデータ構造やアルゴリズムを数多く見ることが出来ます。
さて、アルゴリズムやデータ構造に加えて、プログラミング、特にモジュール設計において重要なのがインターフェース設計。このインターフェースについても全てを一から考えるようなケースはほとんどなく、大抵の場合、リファレンスとなる前例があるでしょう。これも一種の定石のようなものです。
しかし、このインターフェースについては、アルゴリズムやデータ構造にはない注意点があります。それは、アルゴリズムやデータ構造がコンピュータを対象としたものであるのに対し、インターフェースはそれを使う人間を対象にしているという点です。使う対象の差は、変化に対する追従性に表れます。
アルゴリズムやデータ構造を変えたとしてもコンピュータならすぐ追従してくれます。しかし、例えば関数のインターフェースをちょっと変えても、それを使う人間はすぐに適応してはくれません。慣れ親しんだインターフェースであるほどそうです。人間の思い込みというものはそうすぐには変わりません。
ちょっと例を出しましょう。今、ファイルアクセスを行うライブラリの設計を行っているとします。POSIXのopen/close/read/writeは標準的なインターフェースと言えるので知っている人も多い。リファレンスとしては良いでしょう。 でもここで一つ問題が。このopenのflagsには、ファイルが存在しないときは作成するというO_CREATというフラグがあります。O_CREATEではなくO_CREATです。E一文字くらい省略するなよと何人の人が思ったことでしょう。ということで今回はO_CREATはO_CREATEと変えたとします。
さて、ここで何が起こるでしょうか。大抵のユーザはI/Fを一瞥して、「あぁ、POSIXと同じだな」と思い込んで、詳細な仕様をわざわざ見ずに使うでしょう。となると、ファイルを作成したいときは思わずO_CREATと指定してしまいます。この場合は、コンパイルでエラーになるだけなので対した被害はありませんが、実行時でしか気がつかないような機能的な違いだと非常にやっかいです。
これがPOSIXのopen()と似ても似つかないインターフェースならユーザーはCREATEと指定してくれる可能性が高いでしょう。「似て非なる」という点が混乱のもとになっているのです。
アルゴリズムやデータ構造といったコンピュータを相手にする部分は、小さな工夫を積み重ねることで効果は上がるでしょう。しかしインターフェースのような人間が使うものは小さな変化を積み重ねていては、かえって混乱を招くだけです。「似て非なる」はコンピュータには別々できても人間には分かり難いものです。キーボードのQWERTY配列は人間工学的に見て優れているとは言えませんが、そうは分かっていてもなかなか変更されないのも良く似た理由と考えられるでしょう。
一から新たなインターフェースを設計する際には、単純に最も良いと思われるものを目指せば良いでしょう。しかし、それが何かしら基になるものを改善するようなものである場合は、改善後のインターフェースに対する利便性等だけでなく、変化に対する移行性、時には互換性といったものに対する配慮も必要でしょう。
【関連記事】
・関数のユーザビリティ
・選択できない選択肢
【関連リンク】
・入力法および表記法のヒューマン・インタフェース学入門(QWERTY成立の解説等)
【関連書籍】
・Algorithms + Data Structures = Programs Niklaus Wirth
・アルゴリズムとデータ構造 - N.Wirth
・珠玉のプログラミング―本質を見抜いたアルゴリズムとデータ構造 - Jon Bentley
・プログラミング作法 - Brian Kernighan
あなたの実力以上に有徳であろうとするな。プログラミングにおいて最も重要なもののひとつ。「美しさ」。 「美しい」ソースコードと「読みやすい」ソースコードとは必ずしも一致するのものではありませんが、美しいソースコードは可読性や拡張性が高く、保守性にも優れています。美しいコードを書こうという努力は、決して自己満足に終わるものではありません。
できそうもないことを己に要求するな。
ニーチェ - 『ツァラトゥストラはこう言った』
さて、「美しい」といってしまうとあまりにも漠然としているのですが、今回とりあげるのはその中の「統一性」です。
インターフェース設計においては、複数の機能をひとつのインターフェースで実現しようとすることがあります。しかし、もともと複数の機能ですから、機能によって必要とするパラメータが微妙に異なったり、操作対象によって使えたり使えなかたったりする場合がでてくることがあります。
よく言われるオブジェクト指向のアンチパターンにfat interfaceと呼ばれるものがあります。一部の子クラスでのみ有効な機能も全てスーパークラスのインターフェースに詰め込んでしまい、スーパークラスのインターフェースが膨れ上がる。おまけに、実オブジェクトのクラスによって使えるインターフェースと使えないインターフェースまで出てきてしまう。fat interfaceの名の通り、インターフェースの数が膨大になるもの勿論問題なのですが、それ以上に、使えるか使えないか一見して分からないというのは、使う側にとっては非常に問題です。
他の例としては、いろんな機能要求に答えるために、複数の引数を持ったり、フラグ型引数を持つもの。例としては、POSIXのopen()関数。この関数では、flagsにはこれとこれが同時指定可能とか、このflagsにはこのフラグとこのフラグは同時指定可能とか、modeはflagsがこの値のときだけ有効とか、関数の形や型定義からは判断できない仕様ができてきしまっています。
誤解のないように書いておくと、ひとつの関数やインターフェースに複数の機能をバインドしてしまうのはいけないことだと言っているわけではありません。インターフェースの統一は非常に重要な観点です。上記の例で問題なのでは、インターフェース中に「選択できない選択肢」があることです。仕様を確認すれば分かるというのはなしです。そもそも、選べないものは見せるべきではありません。
インターフェースを統一・共通化しようとした際に、上記のような「選択できない選択肢」が登場しそうな場合には、本当に共通化するべきか再考するべきです。そもそも共通項のない機能であれば無理して共通化する必要はありません。共通項が多い場合や、相乗効果が得られそうな場合には統一する 価値があるかもしれません。先のopen関数の場合、多少不要な引数が現れることもありますが、readとwriteのオープン処理が共通化されているために、read/writeモードでファイルをオープンするといったこともできていますので、それほど悪い例とは思いません。
本当に「美しい」コードは、見た目のエレガントさだけではなく、機能性・理解性も優れているものです。
【関連記事】
・関数のユーザビリティ
【関連書籍】
・アンチパターン―ソフトウェア危篤患者の救出
・J2EEアンチパターン
・サーバーサイドJavaアンチパターン
・Code Complete第2版―完全なプログラミングを目指して
しかし,単純なものでも説明がいるような道具があるとするなら,これは,よいインターフェイスを持った道具ではないのだ。改めて述べるまでもなく、IT関連技術の発展は目覚しいものがあります。PCを例にとれば、処理速度や容量・解像度のような定量的な尺度で計れる性能については、大抵のユーザーの需要はすでに満たしているのではないでしょうか。このような性能面での機能が満たされれば、次にユーザーが求めるのは「機能性」、そして「使い勝手」です。
J・J・ギブソン 『アフォーダンス理論』
最近はこの「使い勝手」を決定付けるものとして、ユーザーインターフェース(User Interfase:UI)に関する議論が活発になっています。GUIを始めとするUI設計に関する記事や書籍もよく目にしますし、日用品や家電製品でもユニバーサルデザインを前面に押し出した製品をよく見かけるようにもなってきました。
ところでこのUIに関する議論は、プログラミングにおける関数やクラスの設計も同じことです。関数やクラスが含まれるライブラリの使用者によっては、インターフェースのみが重要であって、実装には興味がないのです。ここでいうインターフェースとは、ヘッダファイルに直接現れるような関数やクラスの宣言のみではなく、処理内容の外部仕様も含みます。
一般的にUIというと機械と人間を媒介するものを指すので、関数やクラスのインターフェースはUIとは少し違うように思えるかもしれません。しかし、実際にそのライブラリを使うのが別のあるプログラムであるとしても、それを使わせようとプログラミングするのはプログラマたる人間です。そういう観点で考えれば、ライブラリ設計にもUI設計の考え方が当てはまるのではないでしょうか。
例えば、最近よく耳にする「アフォーダンス(affordance)」。物体の持つ属性(形、色、材質、etc.)が、物体自身をどう取り扱ったら良いかについてのメッセージをユーザに対して発している、とする考え方です。Webアプリケーションであれば、リンクをボタンの形にすることで、これは押すものだとユーザーに訴える。
プログラミングの例にあてはめると、関数の名前や引数を見ただけで、その仕様がわかるようにする。例えば、is_A()という名前の関数名のようにis_xxx()という名前によって、関数がbool値を返すということがわかるといった感じです。
その他、昨今のUIの議論からプログラミングの設計について考えさせられるところは多いです。詳しく書き出すと長くなりそうなので、改めて追々書いていきたいと思います。
【関連記事】
・変数の命名規則
【関連書籍】
・誰のためのデザイン?―認知科学者のデザイン原論
・インターフェイスの街角―本当に使いやすいユーザー・インターフェイスの極意
・アフォーダンス-新しい認知の理論
多くを知るものは少なく語る仮想関数やテンプレート関数等、同じシンタックスで複数の使われ方をするような関数では、使用しない仮引数というものが登場することがあります。例えば、
Who knows most says least.
class ClassBase {
virtual func(int hint);
};
class ClassA : pulic ClassBase {
virtual func(int hint);
};
void
ClassA::func(int hint)
{
// このClassのfuncでは仮引数hintは不要なので使用しない
...
}
仮想関数やテンプレート以外でも、関数仕様として拡張性を持たせるために用意されているものの、現状では使用しない場合等も考えられるでしょう。さて、このような使用しない仮引数が使用しないだけで、動作上問題はないのですが、コンパイル時に未使用変数(unused variable)として警告目セージが出力されてしまいます。このような警告を出力されたままで放置すると、本来注意するべき警告が埋もれてしまう(見逃されてしまう)可能性もありますので、できれば避けたいところです。
ここでは、このような未使用仮引数の警告を抑制するいくつかの方法を紹介します。
■コンパイラのunused属性を使用する
コンパイラによっては、変数や構造体に属性をしているための方法が用意されています。例えば、gccでは次のような指定が可能です。
void
func(int val __attribute__ ((unused)) )
{
...
}
(参考)GCC - 変数属性の指定unused以外では、構造体のパック(pack)やアライメント(alignment)調整等、セクション配置等が指定できます。
この方法の問題は、指定方法がコンパイラ依存のため、移植性に問題がでる点です。
■仮引数名を省略する
Cでもプロトタイプ宣言中では仮引数名を省略できましたが、C++では関数定義でも省略できます。使用しない仮引数は、変数名を省略することで、その仮引数を使用しないということをコンパイラに伝えることができます。
残念ながらこの方法はCでは使えません。
■voidキャストを使う
この方法が個人的には最もおすすめです。使用しない変数を関数定義中でvoidキャストしてやる方法です。例をあげます。
#define UNUSED_VARIABLE(x) (void)(x)
void
func(int val)
{
UNUSED_VARIABLE(x);
...
}
この方法のメリットは、C,C++共通で使える、移植性の問題もない、といったこともありますが、何よりあとから未使用変数をgrepし易いこと点があります。上記の例では、未使用変数宣言用のマクロを用意して使用していますので、ソースコード中からUNUSED_VARIABLEをgrepすることで、未使用変数を洗い出して見直すことができます。【関連記事】
・warningに気を配る
【関連リンク】
・仮引数名の省略 (C++と組み込み環境)
・GCC - 変数属性の指定
【参考書籍】
・GCC GNU C Compiler―Manual & Reference
・Introduction To Gcc (Richard M. Stallman)







