読み取り専用の配列型仮引数

[戻る]
シャノン 2006/06/18(日) 06:38:47
日頃お世話になっております。宜しくお願いいたします。

突然ですが俺は、関数の仮引数を書き換えるのが好きではありません。
そのため、最近は仮引数を const にするように心がけています。
こんなふう↓に。

 void func1( int const arg );

#ちなみに、const int ではなく int const なのは、些細なこだわりです。

もちろん、const にせずに、関数内で仮引数を書き換えても、呼び出し元の実引数が書
き換わらないことくらいは承知しています。
それでも書き換えたくないのです。

というわけで、文字列を引数に取る関数なんかだと、こう↓なります。

 void func2( char const * const arg );

ところで、関数の仮引数に配列型を置くと、これはポインタ型に読み替えられます。
つまり、以下の2つが等価です。

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

ですから、こんなこと↓ができてしまいます。

 int main( int argc, char * argv[] )
 {
  argv = 0; // 配列に代入しているみたいで気持ち悪い…
 }

この argv を読み取り専用にしたいのです。
要するに、↓の3行がすべてコンパイルエラーになるようにしたいのです。

 int main( int const argc, char const * const * const argv )
 {
  argv = 0; // コンパイルエラー
  argv[ 0 ] = 0; // コンパイルエラー
  *argv[ 0 ] = 0; // コンパイルエラー
 }

仮引数をポインタ風に書けば、↑のコードで事足りるのですが、argv を配列風に書いた
ときに const にすることはできるのでしょうか?
これ↓で、下2行はエラーに出来ますが、一番上のは通ってしまいます。

 int main( int const argc, char const * const argv[] )
 {
  argv = 0; // これはコンパイルできてしまう
  argv[ 0 ] = 0; // コンパイルエラー
  *argv[ 0 ] = 0; // コンパイルエラー
 }

この一番上の行もエラーにすることはできるのでしょうか? できるとしたら、どう書
けばよいのでしょうか?

επιστημη 2006/06/18(日) 11:54:00
--- Digital Mars C++ では:

argv = 0;
        ^
foo.cpp(3) : Error: can't assign to const variable main::argv
argv[ 0 ] = 0;
             ^
foo.cpp(4) : Error: can't assign to const variable
*argv[ 0 ] = 0;
              ^
foo.cpp(5) : Error: can't assign to const variable

--- Borland C++ では:
エラー E2024 foo.cpp 3: 
 const オブジェクトは変更できない(関数 main(const int,const char * const * 
const) )
エラー E2024 foo.cpp 4:
 const オブジェクトは変更できない(関数 main(const int,const char * const * 
const) )
エラー E2024 foo.cpp 5:
 const オブジェクトは変更できない(関数 main(const int,const char * const * 
const) )

となります。(g++3.4.4 では line-3をエラーとはしませんでした@cygwin)

なんかコンパイラの癖みたいなもんがありそうです。

シャノン 2006/06/18(日) 20:01:27
試していただきまして、ありがとうございます。
DMC で試していただいたのは、以下のどちらのコードでしょう?

> int main( int const argc, char const * const * const argv );
> int main( int const argc, char const * const argv[] );

BC++ では、エラーメッセージを見る限り、上の行のものでしょうか。
上の行のは、VC++ でも argv = 0; はちゃんとエラーになりますし、規格的にもならな
ければいけないはずです(GCC ともあろうものがエラーにしてくれない…?)。

下の行のような、配列風の書き方で、上の行と等価な(argv = 0; がコンパイルエラー
になる)宣言はどうなるのでしょう?

そもそも、関数の仮引数でなければ、配列に直接代入なんてできないんだから、そうい
う構文は用意されていない可能性もあるかもしれませんが…。

επιστημη 2006/06/18(日) 21:11:59
dmc, bcc どっちも arv[] なやつです。

tetrapod 2006/06/19(月) 11:22:51
main() に関しては argv 関連が non-const であることが規格書に明言されているので
例題を void testfunc1(const int a[]) { a=0; } に変更します。
この挙動を手持ちのコンパイラで調査したところ以下のようになりました。
vc++6, gcc-3.4.4, 某社組み込み用コンパイラ : 通る
bcc-5.5.1 : エラー

規格書で探せた文言は
仮引数中の cv T a[] は cv T* a に読み替えられる であって
仮引数中の cv T a[] は cv T* const a または cv T* cv a に読み替えられる
ではなかったので bcc の挙動は親切設計、ないしは、余計なことをしている、です。

ちなみにちょっと気になったのでこんな例題を組んでみたところ
int testfunc2(const volatile int a[]) { return a[0]+a[0]; }
bcc は const volatile int * const volatile a とみなしているようです。
a[0] に対するアクセス2回は当然ながら a に対するアクセスも2回となりました。
vc++6/gcc/某コンパイラでは a[0] アクセス2回、a アクセス1回でした。
... bcc の挙動はやはり余計なことと言えそうです ...

仮引数そのものの変更を禁じたいならポインタなど使わず参照をどうぞ、と言いたい。
ご希望の動作は構文上用意されていません、と言えるでしょう。

シャノン 2006/06/19(月) 22:58:20
> main() に関しては argv 関連が non-const であることが規格書に明言されている

なんと。そうでしたか…
ということは、勝手に const をつけると、規格違反になる可能性が…?
コンパイルエラーも警告も出ませんでしたけど。
参考までに、規格書のどこに書かれているのか教えて頂けますでしょうか。

> 仮引数そのものの変更を禁じたいならポインタなど使わず参照をどうぞ、と言いたい。

別にポインタだっていいじゃないですか。
「ポインタと参照を比較して、C++ なら参照を使え」っていうならわかるけれど、「仮引
数そのものの変更を禁じたいなら参照を使え」っていうのは、どのような理由があって?

あとは、参照だと、引数に配列を取れませんしね(テンプレート関数にして、配列の要素
数を推測でもさせない限り)。
vector の参照にしろってことになるのかしら…

STL は C++ の範囲内で完結する分には便利だけれど、DLL 境界を超えられないので、あ
まり使う気にならんのです。
参照も C++ に固有のものだから DLL 境界は越えられんと思っていたけど、ポインタと同
じ実装になっているようで、問題ありませんでした。もちろん、処理系に互換性がある限
りは…の話ですけど、互換性がない処理系で DLL 作ると、参照ではなくポインタを使おう
が何をしようが問題が出るので、それはまた別の話。

シャノン 2006/06/19(月) 23:31:52
解決忘れてました。

> ご希望の動作は構文上用意されていません、と言えるでしょう。

了解です。ありがとうございました。

シャノン 2006/06/19(月) 23:33:45
[[解決]]
あれれ。チェックされてなかったorz

Ban 2006/06/20(火) 11:10:36
> 参考までに、規格書のどこに書かれているのか教えて頂けますでしょうか。

C++ (ISO/IEC14882:2003)であれば、3.6.1 Main function を指していると思われます。

> ということは、勝手に const をつけると、規格違反になる可能性が…?

書き方的には、int main() と int main(int argc, char* argv[]) が全ての実装で
許可(shall)程度なので、違反というよりは、通らなくてもしらないぞに近そうですが、
あまりお勧めはできないかと。


> コンパイルエラーも警告も出ませんでしたけど。

コンパイラは多くの場合、main だからといって規格に基づく特別な警告を
したりしないと思いますので、通ったからといって規格準拠の是非にはつながらないか
と。
# main の return 省略を特別視するような例外もあるにはありますが。

tetrapod 2006/06/20(火) 12:32:14
配列の参照はありです(参照の配列は無いけど)
int testfunc1(int& array[10]) {...} // ×これは参照の配列
int testfunc2(int (&array)[10]) {...} // ○これは配列の参照
任意個の配列を渡すことはできませんが。

C++ のほうはBanさんの指摘どおりで char *argv[] が通るということが根拠ですし
C99 なら 5.1.2.2.1 にもっと厳しく明記されています。JISX3010:2003 を引用。
[仮引数 argc, argv および argv 配列が指す文字列は、プログラムによって
変更可能でなければならない。さらに、プログラム開始処理から終了処理までの間、
最後に格納された値を保持しなければならない。]

ポインタだっていいぢゃんには御意。
っていうか実用上「配列先頭+個数」にせざるを得ないことはしばしばですし。

# 仮引数を書き換えたくないというrationaleがちょっぴり知りたい鴨
# 俺的には何も気にせず書き換えますが...
# サイズ/速度が気になる組み込み系がメインの仕事だから?

シャノン 2006/06/20(火) 16:01:21
> 書き方的には、int main() と int main(int argc, char* argv[]) が全ての実装で
> 許可(shall)程度なので

ですよね。その記述は見つけたんですけど。

> 配列の参照はありです(参照の配列は無いけど)
> 任意個の配列を渡すことはできませんが。

厳密には、任意個の配列を渡していることにはなりませんが、こういう芸当なら。

template< size_t num >
void func( int ( & array )[ num ] );

ところで、ふと考えてみたらば、「呼び出し側が const だと思っていないものを、呼び
出され側で勝手に const にする」ということが許容されるのはある程度までで、

caller : void func( int ** );
callee : void func( int const ** );

はエラーになる(JIS X 3014:2003 4.4.4)。

というわけなので、main は

int main( int argc, char ** argv );

と考えられるとすれば、

int main( int const argc, char const ** argc );

まではやっちゃいけないのかもしれない(この制限は引数に適用されると、双方の関数か
ら可視なグローバル定数がないと悪用できないけれど)。
ただ、他のコンパイラはどうだか知らないけれど、VC++ は main の型チェックが甘いの
か何なのか、

int wmain( int argc, char * argv[] );

なんてのでもエラーを吐かないくらいだから、たまたま通っちゃってただけかもしれない。

しかし、

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

までは合法であるような。

シャノン 2006/06/20(火) 16:03:25
> # 仮引数を書き換えたくないというrationaleがちょっぴり知りたい鴨

いやなんとなく。書き換えたからといって害があるわけでもないんですが、気分的に。
個人的に、

void func( int i )
{
 i = 3;
}

は許容しませんが、

void func( int i )
{
 int j = i;
 j = 3;
}

なら全く問題ありません。

tetrapod 2006/06/21(水) 08:43:01
char* stpcpy(char* d, const char* s) {
  while (*d++=*s++);
  return d-1;
}
とか書きませんか、そうですか。

シャノン 2006/06/21(水) 20:12:08
> とか書きませんか、そうですか。

そう書かねばならない何らかの理由がある場合は別として、個人的には書きません。
書くとしたら

char * stpcpy( char * const d, char const * const s )
{
 for( char const * temp = s; *temp != '\0'; ++s, ++d )
 {
  *d = *temp;
 }

 return d;
}

…書いてみてから d 変更していることに気がついた。これじゃコンパイル通らないね。
でも、ここは d も一時変数を使うべきかと考えて、どうにも素直にそうできない気分。
でもでも、s は一時変数を使うことが、考えなくても浮かんだから不思議。

ちなみに、一時変数を使うにせよ使わないにせよ、

> while (*d++=*s++);

これは絶対に書きません。
・++ と -- は常に前置で、(for 文で上記のように使う場合を除き)単独の行として書
く
・条件は必ず比較演算子を用いて書く
というマイルールなので。

古いスレッドには追加発言できません。