- 2010年3月15日 21:30
- C++
最近、些細なことに気付く程度には余裕が出てきたのだろう。ふと自分のコードを見直してみると、static_cast を単なる「警告除け」や「コンパイルエラー除け」として、あやふやな理解のまま使っていることに気付いた。どうやら私は、static_cast をなんとなく「それ以外の cast」といったような感覚で使っているようだ。「それ」とはもちろん、標準で用意されている static_cast 以外の cast、dynamic_cast, const_cast, reinterpret_cast の3つのこと。これらの cast は役割が結構はっきりしているので、それぞれ、安全なダウンキャスト用、const 外し用、強制置き換え用として(もちろん必要に迫られて)迷うことなく使っている。ところが static_cast だけは、それ以外の状況で警告(あるいはコンパイルエラー)が出た時に仕方なく付けているというわけだ。そこに明確な理由も意思もない。単なるコンパイラのご機嫌取りだ。情けない。
このままでは、新人君に「先輩、ここの static_cast にはどのような効果があるのですか?」と聞かれても、「あ、それ? なんとなく、コンパイラが文句言ってたし」としか答えられないし、先輩に「ここで static_cast はないだろう」と言われても「あ、はい、そうですね。コンパイルエラー出ちゃったんでなんとなく付けちゃいました」となんとも間抜けな回答しかできないことになる。このままではマズイ。
前置きが長くなってしまったが、要は「static_cast について自分なりにまとめましたよ」という話だ。
こういう話は一時ソースから辿るのがベスト。ISO IEC 14882-2003, 5.2.9 Static cast [expr.static.cast] に、static_cast について「これでもか」とばかりに1ページほど記述されているので、この際だから順番に追っていくことにしよう。
The result of the expression static_cast<T>(v) is the result of converting the expression v to type T. If T is a reference type, the result is an lvalue; otherwise, the result is an rvalue. Types shall not be defined in a static_cast. The static_cast operator shall not cast away constness (5.2.11).
static_cast<T>(v) と書くと、v を T 型に変換した結果が得られる。T が参照型なら結果は lvalue, そうじゃないなら rvalue だ。static_cast 内で型を定義するのはダメ。あと、const は外せないよ。
ISO IEC 14882-2003, 5.2.9 Static cast - 1
ここでは static_cast が型を変換するという事と、const を外せない事を言っている。まだまだ序の口。
An expression e can be explicitly converted to a type T using a static_cast of the form static_cast<T>(e) if the declaration "T t(e);" is well-formed, for some invented temporary variable t (8.5). The effect of such an explicit conversion is the same as performing the declaration and initialization and then using the temporary variable as the result of the conversion. The result is an lvalue if T is a reference type (8.3.2), and an rvalue otherwise. The expression e is used as an lvalue if and only if the initialization uses it as an lvalue.
もし、T t(e) と書けるなら、static_cast<T>(e) は合法だ。static_cast の結果、t に相当する一時オブジェクト(T が参照型なら lvalue, それ以外なら rvalue)が作られるからね。一時オブジェクトの生成時に e が lvalue として扱われることもあるけど、まぁそれはコンストラクタ次第かな。
ISO IEC 14882-2003, 5.2.9 Static cast - 2
ちょっと端折りすぎかも知れないが、ここでは static_cast がコンストラクタを呼ぶことと等価であると言っている......と思う。コンストラクタで構築できるものは static_cast でも変換可能。これもまぁ、当たり前っちゃぁ当たり前。
Otherwise, the static_cast shall perform one of the conversions listed below. No other conversion shall be performed explicitly using a static_cast.
それ以外の場合、static_cast は以下の変換を行うよ。static_cast の機能はこれだけだからね。
ISO IEC 14882-2003, 5.2.9 Static cast - 3
よし、ココからが本番だ。
Any expression can be explicitly converted to type "cv void." The expression value is discarded. [Note: however, if the value is in a temporary variable (12.2), the destructor for that variable is not executed until the usual time, and the value of the variable is preserved for the purpose of executing the destructor. ] The lvalue-to-rvalue (4.1), array-to-pointer (4.2), and function-to-pointer (4.3) standard conversions are not applied to the expression.
どんな型でも void にできる。もちろん、値は無くなっちゃうけどね。(あ、無くなっちゃうとは言っても、一時オブジェクトのデストラクタが呼ばれるのは通常のタイミングと同じ。つまり一時オブジェクトの寿命が尽きた時なので注意。)
あと、ここでは lvalue-to-rvalue, array-to-pointer, function-to-pointer の標準変換は適用されないよ。
ISO IEC 14882-2003, 5.2.9 Static cast - 4
つまりは、static_cast<void>(何でもOK) は合法ということ。これ自体の使い道は思いつかないが(template がらみで役立つかも?)、こういう重箱の隅までキッチリ書かれているところなどは、まさに規格書といったところか。
An lvalue of type "cv1 B", where B is a class type, can be cast to type "reference to cv2 D", where D is a class derived (clause 10) from B, if a valid standard conversion from "pointer to D" to "pointer to B" exists (4.10), cv2 is the same cv-qualification as, or greater cv-qualification than, cv1, and B is not a virtual base class of D. The result is an lvalue of type "cv2 D." If the lvalue of type "cv1 B" is actually a sub-object of an object of type D, the lvalue refers to the enclosing object of type D. Otherwise, the result of the cast is undefined.
[Example:
struct B {};
struct D : public B {};
D d;
B &br = d;
static_cast<D&>(br); // produces lvalue to the original d object
-end example]
クラス B から派生したクラス D があった時、D* を B* に変換できるなら、static_cast は B 型の lvalue を D への参照型に変換できる。ただし、B が仮想基底クラスでない場合に限るよ。もちろん、B が本当に D を指していないなら結果は未定義だからね。
(cv については自明なのであえて省略。)
ISO IEC 14882-2003, 5.2.9 Static cast - 5
ここは1つのポイント。親子関係にあるクラス間の変換を説明しているが、アップキャストではなくダウンキャストについて述べている点に注意しよう。つまり、ここでは static_cast によって安全でないダウンキャストが可能であると言っている。
これで、先ほどの「ここで static_cast はないだろう」という先輩に対して、「いや、ここは確実に D を指していることが分かっているので、dynamic_cast よりも効率の良い static_cast を使っているわけですよ」と反論できるようになった。(その後、そもそも D と分かっているという事は云々......といった感じで議論が続くと思われるが、static_cast の範疇を越えるのでこれ以上は言及しない。)
The inverse of any standard conversion sequence (clause 4), other than the lvalue-to-rvalue (4.1), array-to-pointer
(4.2), function-to-pointer (4.3), and boolean (4.12) conversions, can be performed explicitly using static_cast. The lvalue-to-rvalue (4.1), array-to-pointer (4.2), and function-to-pointer (4.3) conversions are applied to the operand. Such a static_cast is subject to the restriction that the explicit conversion does not cast away constness (5.2.11), and the following additional rules for specific cases:
lvalue-to-rvalue, array-to-pointer, function-to-pointer, boolean conversion 以外の標準変換は、static_cast で逆変換できる。この時の static_cast は const を外せないこと以外に以下の特別ルールが適用される。
ISO IEC 14882-2003, 5.2.9 Static cast - 6
標準変換については4章でがっつり述べられているが、整数の格上げやら浮動小数と整数の変換やら、警告が出そうな変換の類がこれに相当する。ここで重要なのは「逆変換」という部分。私が多用するのは「暗黙の変換によって値が範囲外になってしまうかもよ」とか「signed と unsigned が食い違っているよ」といった警告に対して「はい、分かってやってるんで」と意思表示をするための static_cast だが、ここで述べられているのは、4章の標準変換に含まれていない逆変換も static_cast によって可能になるという話なので、警告除けとは別次元の話。
つまり、逆向きが許されていない標準変換(つまり逆変換がエラーとなる変換)であっても、static_cast を使えば逆変換が可能になるという話だ。
どうやらこの辺りを理解すれば「警告除け」と「必然的な static_cast」の違いについて後輩に説明できるようになりそうな気がしてきた。はやる気持ちを抑え、とりあえず、以降の特別ルールについて見ていくことにしよう。
A value of integral or enumeration type can be explicitly converted to an enumeration type. The value is unchanged if the original value is within the range of the enumeration values (7.2). Otherwise, the resulting enumeration value is unspecified.
整数や enum は enum に変換できる。当然だけど enum の範囲外の値を enum に cast すると、変換後の値は不定になるよ。
ISO IEC 14882-2003, 5.2.9 Static cast - 7
これは逆向きが許されていない標準変換、enum → int に関する特別ルール。static_cast を使えば enum → int の逆向きである int → enum が可能になるが、enum の範囲外の値を enum に変換すると不定になるという(当たり前の)特別ルールを設けている。
An rvalue of type "pointer to cv1 B", where B is a class type, can be converted to an rvalue of type "pointer to cv2 D", where D is a class derived (clause 10) from B, if a valid standard conversion from "pointer to D"to "pointer to B" exists (4.10), cv2 is the same cv-qualification as, or greater cv-qualification than, cv1, and B is not a virtual base class of D. The null pointer value (4.10) is converted to the null pointer value of the destination type. If the rvalue of type "pointer to cv1 B" points to a B that is actually a sub-object of an object of type D, the resulting pointer points to the enclosing object of type D. Otherwise, the result of the cast is undefined.
クラス B から派生したクラス D があって、D* を B* に変換できる(アップキャストが許されている)なら、static_cast で B* を D* に変換できる(ただし、B が仮想基底クラスの場合を除く)。この時、null ポインタは null ポインタに変換されることが保証されているからね。あと、何度も言うようだけど、B へのポインタが実は D を指していないのに static_cast で D* に変換してしまうと、何が起こってもおかしくないから注意してね。
ISO IEC 14882-2003, 5.2.9 Static cast - 8
これは逆向きが許されていない標準変換、アップキャストに関する特別ルール。アップキャストの逆変換であるダウンキャストもまた static_cast で可能となるが、仮想基底クラスからのダウンキャストは無理な点と、dynamic_cast で失敗するようなケースで使うと未定義となることが述べられている。
ところで、「D* を B* に変換できる(アップキャストが許されている)なら」とわざわざ述べているが、そうならない場合とはどんな場合なのだろう。これは public でない継承を考えれば納得できる。例えば以下の通り。
class B {
};
class D: private B {
};
void Test()
{
D d;
B* pb = &d; // エラー! D* から B* への変換は許されていない
}
アップキャストできないという事は、ダウンキャストの対象となるポインタそのものが得られないという事なので、この場合にダウンキャストできないというのは容易に理解できるだろう。
An rvalue of type "pointer to member of D of type cv1 T" can be converted to an rvalue of type "pointer to member of B of type cv2 T", where B is a base class (clause 10) of D, if a valid standard conversion from "pointer to member of B of type T" to "pointer to member of D of type T" exists (4.11), and cv2 is the same cv-qualification as, or greater cv-qualification than, cv1.63) The null member pointer value (4.11) is converted to the null member pointer value of the destination type. If class B contains the original member, or is a base or derived class of the class containing the original member, the resulting pointer to member points to the original member. Otherwise, the result of the cast is undefined. [Note: although class B need not contain the original member, the dynamic type of the object on which the pointer to member is dereferenced must contain the original member; see 5.5. ]
static_cast を使えば D のメンバへのポインタを B のメンバへのポインタに変換できる。クラスの関係や逆変換が存在することといった条件や、null ポインタ云々、変換先が存在しない場合に未定義となる点は全てクラスの場合と同じ。
ISO IEC 14882-2003, 5.2.9 Static cast - 9
メンバポインタ! これは幸いなことに(決して短くはないプログラマ人生の中で)まだ1回しか使った事がない機能だったりするのだが、言われてみれば確かにこいつもクラスへのポインタと同じような関係が成り立つはず。練習がてらにサンプルを作ってみたが、如何せん経験が浅いのでここで述べられている事を説明し切れているかどうかは自信なし。
class B {
public:
virtual void Func() { std::cout << "B::Func() called" << std::endl; }
};
class D: public B {
public:
virtual void Func() { std::cout << "D::Func() called" << std::endl; }
};
void Test()
{
B b;
D d;
void (B::* mpb)() = &B::Func; // B のメンバ関数へのポインタ
void (D::* mpd)() = &D::Func; // D のメンバ関数へのポインタ
(b.*mpb)(); // メンバポインタを介した関数呼び出し(B::Func が呼ばれる)
//(b.*mpd)(); // エラー! B のオブジェクトに D::* を使おうとしている
(d.*mpb)(); // メンバポインタを介した関数呼び出し(B::Func は仮想関数なので D::Func が呼ばれる)
(d.*mpd)(); // メンバポインタを介した関数呼び出し(D::Func が呼ばれる)
//void (B::* dtob)() = mpd; // エラー! D::* から B::* への暗黙の変換は許されていない
void (B::* dtob)() = static_cast<void (B::*)()>(mpd); // でも static_cast を使えば変換可能
void (D::* btod)() = mpb; // B::* から D::* への変換は OK
(b.*dtob)(); // B::Func が呼ばれる(これが original pointer 云々の部分で言いたいことだと思う)
//(b.*btod)(); // エラー! B のオブジェクトに D::* を使おうとしている
(d.*dtob)(); // D::Func が呼ばれる
(d.*btod)(); // D::Func が呼ばれる
}
メンバポインタの場合はクラスへのポインタとは逆で、子クラスから親クラスへの変換が許されていない。これは親クラスが子クラスのメンバを持っているとは限らないからと考えると理解しやすいと思う。
An rvalue of type "pointer to cv1 void" can be converted to an rvalue of type "pointer to cv2 T," where T is an object type and cv2 is the same cv-qualification as, or greater cv-qualification than, cv1. A value of type pointer to object converted to "pointer to cv void" and back to the original pointer type will have its original value.
static_cast を使えば void* を T* に変換できる。当然だけど、その void* が元々は T* であった場合に限るよ。
ISO IEC 14882-2003, 5.2.9 Static cast - 10
これも T* → void* の逆バージョン。標準変換では許されていないが、static_cast によって変換できるようになる。
static_cast に関する C++ の仕様は以上だ。私の中で理解があいまいだったのは、標準変換の逆変換と警告除けがごっちゃになっていた部分だった。標準変換自体は static_cast は不要。コンパイラは cast によって値が失われるかも知れないよと警告しているに過ぎない。当たり前と言えば当たり前だが、コンパイラが文句を言ったら static_cast を付けるという情けない対応で何とかなっていたため、完全に思考停止状態だった。本当にエラーになるような状況でも警告と同じように static_cast を付けるだけだったので、頭の中でこれらを区別しきれていなかったというわけだ。
以上を踏まえ、これからは static_cast を「それ以外の cast」ではなく、以下のような場合に使う cast として、意識的に区別しながら使ってみようと思う。
- 安全でないダウンキャスト用の cast
- int → enum 用の cast
- void* → T* 用の cast
- 警告除けの意思表示
1は元々区別して扱っていた(し、そもそも滅多に使わない)ので、結局のところ 2, 3 と 4 を区別するようになったところが進歩したところか。まぁ、コンパイラが文句を言った後で static_cast を付けるという流れはしばらく変わらないと思うが、その時に機械的に付けるのか、それとも意味を考えながら付けるのかには大きな差がある、と信じたい。
- Older: 法則に違反したっていいじゃない
コメントを表示する前に、このブログのオーナーによる承認が必要になることがあります。コメントが表示されない場合は、承認されるまでしばらくお待ちください。