わさっきhb

大学(教育研究)とか ,親馬鹿とか,和歌山とか,とか,とか.

定数の定義は,const intか,#defineか,それとも

メールが来ました.学内の先生からです.アルドゥイーノのスケッチのプログラミングで,定数を定義するのに,ある本ではconst intを使っており,また別の本では#defineを使っているのだけれど,夏休みのプログラミング教室で教える際には,どちらのほうが良いのか,とのこと.……

const int ledPin = 13;

#define ledPin 13

の違い,というわけですが,一般論としては,const intで書く方が,“Cらしい”と言っていいでしょう.
ただし,もしこのledPinが,配列宣言時の要素数(int array[ledPin];)や,switch〜caseのラベル(case ledPin:)として出現する場合には,変数名だと都合が悪いので,#define を選ぶ必要があります.まあ,const intを使っている本があるということなので,こんな事例はないのでしょうね.
もう少し,調べてみました.Sketchのチュートリアルhttp://arduino.cc/en/Tutorial/Sketch)には,「int ledPin = 13;」と書かれていました.その一方で,wikipedia:Arduinoおよびwikipedia:en:Arduinoでは,サンプルコードの中に「#define LED_PIN 13」とあります.Webにおいても,どちらでもよさそうです.


ArduinoやSketchから離れて,C言語として,定義の仕方を調べてみました.
まず用語として,「定数」はCの規格では特定の意味があるのでした.具体的には,JIS X 3010:2003では,定数は「整数定数」「浮動小数点定数」「列挙定数」「文字定数」のいずれかとしています.なので,上のコードのどちらであっても,ledPinを定数とみなすのは,規格から外れた解釈となります.
まあそれはそれとして,constか#defineかについては,英語の議論を見つけました.

この中で,Jonathan Leffler (answered Nov 4 '09 at 15:17)の意見が秀逸でした.

It depends on what you need the value for. You (and everyone else so far) omitted the third alternative:

  1. static const int var = 5;
  2. #define var 5
  3. enum { var = 5 };

Ignoring issues about the choice of name, then:

  • If you need to pass a pointer around, you must use (1).
  • Since (2) is apparently an option, you don't need to pass pointers around.
  • Both (1) and (3) have a symbol in the debugger's symbol table - that makes debugging easier. It is more likely that (2) will not have a symbol, leaving you wondering what it is.
  • (1) cannot be used as a dimension for arrays at global scope; both (2) and (3) can.
  • (1) cannot be used as a dimension for static arrays at function scope; both (2) and (3) can.
  • Under C99, all of these can be used for local arrays. Technically, using (1) would imply the use of a VLA (variable-length array), though the dimension referenced by 'var' would of course be fixed at size 5.
  • (1) cannot be used in places like switch statements; both (2) and (3) can.
  • (2) can change code that you didn't want changed because it is used by the preprocessor; both (1) and (3) will not have unexpected side-effects like that.
  • You can detect whether (2) has been set in the preprocessor; neither (1) nor (3) allows that.

So, in most contexts, prefer the 'enum' over the alternatives. Otherwise, the first and last bullet points are likely to be the controlling factors - and you have to think harder if you need to satisfy both at once.

If you were asking about C++, then you'd use option (1) - the static const - every time.

訳してみます.

どのコードを選ぶかは,その値を何に使いたいかによる.3番目の選択肢があることを忘れているよ.

static const int var = 5; /* (1) */
#define var 5             /* (2) */
enum { var = 5 };         /* (3) */

名前のつけ方は無視するとして,これらには次の違いがある.

  • そこからポインタを得る*1のなら,(1)を使わないといけない.
  • (2)は明らかに,そこからポインタを得ることができない.
  • (1)と(3)はデバッガのシンボルテーブル内でシンボルが作られる.ということでデバッグ作業がより容易になるデバッガに優しい.(2)ではシンボルが作られないので,デバッグ時に都合が悪い.
  • (1)はグローバルスコープの配列の次元として使うことができない.(2)と(3)ならできる.
  • (1)は関数スコープの静的配列の次元として使うことができない.(2)と(3)ならできる.
  • C99では,これらのいずれもローカルな配列(の次元)として使うことができる.技術的には,(1)を使うと可変長配列になるが,その次元はvarにより5に固定される.
  • (1)はswitch文なんかの定数として使うことができない.(2)と(3)ならできる.
  • (2)は,前処理の段階で望まないコードの変更ができてしまう.(1)と(3)ではそのような期待されない副作用は起こらない.
  • (2)では前処理の段階で値の設定がなされることを検出できる.(1)と(3)ではできない.

なので,多くの状況で,enumを使った(3)は,(1)や(2)よりも良い.別の面では,箇条書きの最初と最後の項目に注意をしたい.同時に両方を満たしたいのなら,よく考えないといけない.
C++だと,常に,static constの(1)を使えばいい.

コードを書いて動作確認してみました.ソースファイルはGistに置いています.mainのほかに2つの関数を定義していて,test_arrayは,varを要素数とする配列の定義,test_switchではvarと定数式としたswitch〜caseを書いています.「static const int var = 5;」を使った方法は,gcc -DOPTION_1 constant.c -o constantによりコンパイルできますが,「error: case label does not reduce to an integer constant」とエラーが出ます*2
デバッグ作業がより容易になるデバッガに優しい」も,確かめたいところです.ソースファイルは共通で,コンパイルは以下の3通りの方法をとりました.gdb constantでデバッガを起動し,「b 24」「r」をしたあと,varとarrayの値を見たところ,次のようになりました.

  • gcc -g -DOPTION_1 -DUSE_DEBUGGER constant.c -o constantでコンパイル(static const int var = 5;を採用)
(gdb) p var
$1 = 5
(gdb) whatis var
type = const int
(gdb) p &var
$2 = (const int *) 0x100403030 <var>
(gdb) printf "%d\n", var
5
(gdb) p array
$3 = 0x22aa10
(gdb) whatis array
type = int []
(gdb) p var
No symbol "var" in current context.
(gdb) whatis var
No symbol "var" in current context.
(gdb) p &var
No symbol "var" in current context.
(gdb) printf "%d\n", var
No symbol "var" in current context.
(gdb) p array
$1 = {0, 1, 2, 3, 4}
(gdb) whatis array
type = int [5]
(gdb) p var
$1 = var
(gdb) whatis var
type = enum {...}
(gdb) p &var
Can't take address of "var" which isn't an lvalue.
(gdb) printf "%d\n", var
5
(gdb) p array
$2 = {0, 1, 2, 3, 4}
(gdb) whatis array
type = int [5]

2番目のコンパイル方法およびgdbで,varが式に使えないのは確かに不便です.またarrayの型が,1番目と2・3番目で異なることも確認できました.

とはいえ日常,ここまで違いを理解した上で,enumの定数(列挙定数)を定義して使うことはまあないかなとも思います.C言語の厄介なところをまたひとつ,目にする機会となりました.

(最終更新:2016-10-01 朝)

*1:pass a pointer aroundの良い訳が思い浮かびませんでした.次の項目と合わせて,「&var」と書けるのは(1)だけ,と解釈しました.

*2:gcc -g -std=c89 -pedantic -DOPTION_1 constant.c -o constantとすれば,配列宣言に関して「warning: ISO C90 forbids variable length array 'array'」と警告してくれます.