トップページ / 覚え書き / プログラミング


プログラミング


プログラミング - C++

インライン関数

インライン関数には普通の関数にinlineキーワードを付けるだけではうまくいかない面倒なところがあります。
というのはインライン関数と関数の外部リンケージ指定を組み合わせることは認められておらず(コンパイラ開発者が楽をするためだそうな)、インライン関数は使われるすべての翻訳単位で同じ内容で定義されなければならない(「プログラミング言語C++ 第3版」の9.2)ためです。
なお「同じ内容」という言葉の定義は"定義は1度の規則:the one-definition rule(ODR)"に従います。

"定義は1度の規則:the one-definition rule(ODR)"とは
[1]2つの定義が異なる翻訳単位に含まれており、
[2]トークンレベルで同一であり、
[3]両翻訳単位におけるトークンの意味が同じである。
(「プログラミング言語C++ 第3版」の9.2.3)

<余談>この"the one-definition rule"の和訳「定義は1度の規則」ってどうも日本語として奇妙に感じるのは私だけでしょうか?</余談>
まぁ、とにかく、というわけで以下のようなコードはエラーになります。

//test1.cpp
extern inline int func();
・・・funcを使う・・・

//test2.cpp
extern inline int func() { return 1; }

従って、普通インライン関数はヘッダに記述することになるわけです。
さて、少し考えてみるとこの制限は仕方がないことではある気がします。
ある翻訳単位をコンパイル中に別な翻訳単位に記述されている関数をインライン展開する、ちょっと考えただけでかなり厳しい作業になりそうではないですか?

実は以前Visual C++でこれにはまったことがありまして(^^;。
そのときdebugビルドでは問題なかったにもかかわらず、releaseビルドではリンクできなかったのでした。debugではコードの最適化はしないので、インライン展開もされず問題が発生しなかったのではないかと思ってます。(2001/1/16)


標準C++とVisual C++とC++ Builder

実は標準C++を使う上で、Visual C++の現行バージョン(6.0)は標準C++の準拠し切れておらず時々問題が起こります。単純に古いからなんですけど。
修復不能なエラーでコンパイルを放棄されてしまうこともしばしば。
C++ Builder 5.0は新しいだけあってVisual C++が放り出したコードでもきちんとコンパイルしてくれます。ただ、カタログで「最新のANSI C++の言語仕様をサポート」という表現を使っていることからも分かるように完全ではないようです(実際Inpriseの方からもそのように聞いています)。
実際C++ Builderではコンパイルできないけど、Visual C++ではコンパイルできるというケースも極めて少数ですけどありました(^^。
最後に、Visual C++でSTL(Standard Template Library)を使用するときはソースに
#pragma warning(disable: 4786)
を入れておきましょう。STLを使うとデバッグ様識別子があっという間にVisual C++の制限の255文字を超えるので、これをやっておかないとWarningがじゃんじゃん出てきてしまうのです。(2000/12/31)


C++とマクロ

Cでは多用されているマクロですが、C++ではあまり好まれません。というのはコンパイルの前処理の段階でプリプロセッサが機械的に置き換えるだけなので、コンパイラの型チェックなどを行うことができないためです。Cでは有名なNULLマクロですらあまり好まれません。
以下のようなマクロはCではよく用いられますが、
#define MAX 1024
これはソース中のMAXが機械的に1024というマジックナンバーに置き換えられることになり、何の1024だか分からなくなってしまい、デバッガで追いかけるのには不適です。 C++ではむしろ
const int MAX = 1024;
としたほうがよいでしょう。この場合MAXというシンボルが埋め込まれるのでデバッガで追いかけるのも楽です。また、コンパイラが定数と評価してくれるため、配列のサイズに使用することも可能で、型チェックの支援も十分に受けることができます。

なお、const、typedefはデフォルトで内部リンケージを持っているため(「プログラミング言語C++ 第3版」の9.2)、複数の翻訳単位で同じconst定数を定義して問題ありません。当然ヘッダファイルに記述して複数のファイルに#includeすることも可能です。(2001/1/14)


NULLと0

NULLは整数の0ではありません。整数の0とビットが一致していることすら保証されてしません。
ポインタと比較できるのは定数0だけです。
従って整数とポインタの間のキャストは潜在的に危険であり移植性は保証されません。
C++はこのようなキャストはreinterpret_castを使ってキャストすることが推奨されています。従来のC型のキャストでもキャストできますが、危険なキャストであるということが一目でわからないのでreinterpret_castを使うべきでしょう。(2001/1/16)


オブジェクトのコピーをさせたくない場合

クラスのユーザーにオブジェクトのコピーをさせたくない場合、意外と簡単なテクニックでこれを実現できます。
クラス定義の中でoperator=を宣言しておいて関数本体を用意しない、それだけです。
この場合、ユーザーがオブジェクトのコピーをするようなコードを書いても、operator=のリンクに失敗し、ビルドが成功することはありません。

唯一のオブジェクトの存在しか許可したくない場合はデザインパターンの「Singletonパターン」の使用を検討するとよいでしょう。(2001/1/16)


staticメンバ関数

スレッド関数など決められた型の関数が要求される場合、非staticのメンバ関数では型をそろえたつもりでもなぜか型が一致せずコンパイルできません。
実は非staticのメンバ関数は暗黙の引数、thisポインタが渡されているため、見た目よりも実際の引数が多くなっているためなのです。
このような場合はstaticメンバ関数を利用します。staticメンバ関数はthisポインタが渡されないため、見た目通りの宣言の型の関数として使うこともできます。
ただし、thisポインタが渡されないと言うことはインスタンスのメンバにアクセスできないと言うことを意味するため、この点は注意が必要です。

なお、staticメンバ関数はインスタンスではなくクラスに所属するため、インスタンスの存在の有無に関わらず呼び出すことができます。
この特徴をうまく使えば、まだ、クラスのインスタンスが1つも存在しないときでさえ、staticメンバ関数の中でインスタンスをnewすることも可能なことが分かると思います。
デザインパターンの「Singletonパターン」はこれを利用して、インスタンスが一つしか存在しないことを保証します。
言語が違いますが、Javaでもmainメソッドはstatic宣言されたクラスメソッドであるため、インスタンスの有無に関わらず実行することができるわけです。(2001/1/16)


自動変数と例外処理とオブジェクト指向

C++では自動変数はスタック上に確保する実装が多いため、自動変数はスタック上に確保されるものとして話を進めます。
念のために書いておくと、自動変数がスタック上に確保されること自体は自体は保証されていませんが、仮にスタックを使用しない実装であったとしても、動作は同じ結果になるので(でなければ標準C++ではない)問題はないと思います。

C++では自動変数はスコープから外れると自動的に解体されます。
当然この変数がデストラクタを持つ型であった場合、デストラクタが呼び出されます。
初期化と解放は対となる動作であるため、どちらか片方だけが実行されるようでは困るわけです。
解放するならするで、すでに解放済みのものをさらに解放されたりしてもやっぱり嬉しくない事態になりそうです。

さて、C++ではエラー検出部と処理部の橋渡しのために、例外処理が導入されました。
例外は関数を飛び越えられるマルチレベルなreturnのように働き、try{}で囲まれた部分でthrowされると、catchハンドラに制御を移すまでの間、スコープから外れた自動変数のデストラクタを呼び出し続けます。
この動作は「スタックの巻き戻し」と呼ばれます(こういう言葉が生まれること自体が、いかにスタックを使用した実装が多いかということの証明ではないでしょうか)。

Cでよくエラー処理に使われたsetjmpとlongjmpという、関数を超えたジャンプを行える関数があります。
しかし、これらはC++では使用すべきではないとされています。
というのはlongjmpは、「スタックの巻き戻し」ができないのです。longjmpを呼び出すとそれまでに構築された自動変数のデストラクタが呼び出されることなく、setjmp呼び出し時点まで復帰してしまい(「スタックがはぎ取られる」と呼ぶ)、メモリリークを始めとするあまり嬉しくない現象を呼び起こします。

注意しなければならないのは、「スタックの巻き戻し」が対象とするのはあくまでも自動変数だけで、動的に生成されたオブジェクトの解体は対象外だということです。当然ながら、OSから取得したハンドルなどの解放も対象外。
Javaでは使われなくなったオブジェクトはガベージコレクションによって勝手に破棄してくれるんですけど、実行時の効率を最優先するC++ではそのようなことはありません。
従って、例外がthrowされたとき、動的に確保されたオブジェクトは自分で解体しなければなりません。

実は、標準C++にはauto_ptrという便利なものが用意されており、これを利用すると例外処理中に自動的にdeleteを呼び出してくれます。
使い方は簡単で、自動変数として確保されたauto_ptrインスタンスに自動的にdeleteして欲しいポインタを渡すだけです。
auto_ptrインスタンスがスコープから外れる場合、auto_ptrのデストラクタが保持するポインタに対してdeleteを呼び出します。
この動作によって動的に確保されたオブジェクトの解体を、自動変数並に簡単確実に行うことができるわけです。
やはり、目標は最小の労力、最大の成果でしょう(^^)。

便利なauto_ptrですが、いくつか注意事項もあります。auto_ptrが呼び出すのはdeleteであり、delete[]ではないので組み込み配列の解体には利用することができません。
また、auto_ptrは破壊的セマンティクスを持つため、auto_ptrのインスタンスa、bがあった場合、b=a;を実行した時点で、ポインタの所有はbに移行します。
このため、auto_ptrは標準コンテナの要素としての要件を満たしていません。
参照カウンタで実装されたauto_ptrのようなものがあれば、後者の制限はなくなるのですが、参照カウンタは標準制定の作業の遅れより、標準C++ライブラリから削除されたそうです(残念)。
<余談>削除されたといえばhash_mapをはじめとするハッシュ関連コンテナも標準コンテナから削除されちゃってます。これまた残念。</余談>

さて、さて、長々と書いてきましたが、実は書きたかったのはここから。
便利な自動変数なんですが、自動変数にオブジェクトの構築を許すオブジェクト指向プログラミング言語は少数派だそうです。
私なりに理由を考えてみたんですが、「自動変数を乱用するとオブジェクト指向でなくなってしまう」ということではないかな。
オブジェクト指向ではオブジェクトを参照、ポインタによって実現されるインターフェース経由で操作することで、ポリモーフィックな、多様化した動作を実現します。
でも、自動変数上として確保されたオブジェクトばかり使っていてはちっとも動作がポリモーフィックにならず、従来型のプログラミングとあまり変わらなくなってしまう、そういうことではないでしょうか?

自動変数にオブジェクトの構築を許さないプログラミング言語では、たいてい例外発生時のオブジェクトの解体はfinally(標準C++にはないです)部などを使用して行います。
でも、私には自動変数のデストラクタで解放させた方がスマートに見えるんです。それがオブジェクト指向でない要素を持つとしても。
<余談>C++ Builderには言語拡張として__finallyが存在します。私はどうしても避けられない場合を除いて言語拡張のお世話になりたくないので、まず使わないですけど。</余談>
C++がオブジェクト指向を目的として設計されたわけではなく、結果としてオブジェクト指向っぽくなっただけ、という話は有名です。C++Javaなどと異なり、純粋なオブジェクト指向言語として設計されたわけではないC++ならではなのかも。私はそんなところが好きなんですけど(^^。(2001/1/17)


Untitled Web Page
http://www.tk.airnet.ne.jp/seiitiro/
mailto: seiitiro@tk.airnet.ne.jp