トップ回答者
VC++11で、ある型変換を伴う初期化式がC2065でコンパイル不能になってしまうのは何故?

質問
-
↓ のコードがコンパイル不能で、コードに落ち度を見出せなかった為、タイトルのお尋ねをさせて頂きました。尚、全く同じコードが g++4.7.2 ではコンパイルできます。
void* f( int, const int* )
{
return nullptr;
}
int main()
{
void ( *p )()( reinterpret_cast< void (*)() >( f( 0, reinterpret_cast< const int* >( 1 ) ) ) ); //「 error C2065: 'p' : 定義されていない識別子です。」で構文エラー
}
↓ のどれかのように変えると、何故かコンパイルできるようになります。
1. 初期化式の構文を、()でなく=を使う
2. 関数fの第1引数 int をなくしたコードにする
3. 関数fの第2引数のconstをなくしたコードにする
この問題の解法になっているのは 1. ですが、↓ の理由で原因を知りたいと思った次第です。
A. 本来それを行う必要はない筈で、本件のような無用な気遣いを今後もしたくないと考えた事
B. 例え 1. で本件を対症療法的に解決できたとしても、原因療法になっておらず今後も類似問題に出くわしてしまう可能性がある事
環境
Win7 Pro. 64[bit]
VC++11 x64 Debugビルド
どうぞ宜しくお願い申し上げます。
回答
-
規格で明確にポインタ型に対する関数形式の初期化がはっきり「OK」と書かれている個所は発見はしていませんが、ポインタを特別ダメという表記を見ていない事
n3485(pdf)を見て
6.8 Ambiguity resolutinで
T(*g)(double(3));
が宣言優先ルールでTへのポインタをdouble(3)で初期化とみなすようになる…それゆえ構文的に問題はないが意味的にill-formed、という記述があることなどから、私は現時点ではポインタ型に対する関数形式の初期化子自体はOKと認識しています。(間違っていたら指摘してください。)
またVC++で
auto p( reinterpret_cast< void (*)() >( f( 0, reinterpret_cast< const int* >( 1 ) ) ) );
や
typedef void(*FuncPtr)(); FuncPtr p( reinterpret_cast<FuncPtr>( f( 0, reinterpret_cast< const int* >( 1 ) ) ) );
は通りました。なのでポインタに対する関数形式初期化への問題ではない可能性があると思います。C++11への準拠度はg++の方が進んでいたかと思いますが、この件とは関係ないのではないか、とぼんやり考えています。
ただ、これが通ってしまうと、関数ポインタなので実際は後で変化可能性があるのだと思いますが、このコードではこの瞬間は意味的に
void* vp = nullptr; auto p = (void(*)())vp;
のpと等しくなると思います。void*はオブジェクトへのポインタを受け取れるサイズが保証されますが
5.2.10 Reinterpret castで
Converting a function pointer to an object pointer type or vice versa is conditionally-supported. The meaning of such a conversion is implementation-defined, except that if an implementation supports conversions in both directions, converting a prvalue of one type to the other type and back, possibly with different cv- qualification, shall yield the original pointer value.
とあります。C++03ではillegalだったのがC++11で処理系次第でOKとなった的な感じですが
stackoverflowのCasts between pointer-to-function and pointer-to-object in C and C++のDavid Hammenさんのコメントを読むと、C/C++系統の中でも、このキャストが確実にOKとなる範囲はそれなりに狭いということが言えると思います。なので、移植可能性がある場合は、これによって劇的に楽になる状況でないならば、void*を関数ポインタへキャストすることは避けるのがおすすめです。
-
なるほど、GetProcAddressならcastは致し方ないですねw( 関数を拾うのならFARPROCな以上、関数ポインタ→関数ポインタ変換になるので、これも保証される動作ですし )
仮に規格を精査しバグだという確証がある場合、例えばMicrosoft Connectとかにバグレポートとして提出されてみるのも手かもしれません。
ただ、もしこのvoid (*)()という型が、引数戻り値ともに元の関数通りだったとしても、現状の情報の範囲では呼び出し規約の指定が書かれていないので、dll側と呼び出し側のプロジェクトの設定によってはクラッシュする可能性があります。(少なくともVC++では、書かない場合のデフォルトの呼び出し規約を変更可能)
例としてはWINAPI ( = __stdcall )なら、pの宣言は
void (WINAPI * p)()
など。仕様変更の可能性がある場合、これを確実に合わせるのはそれなりに手間かもしれません。typedefなどがないと、長ったらしくてしかも同じ関数ポインタの型を複数回書かないといけない可能性もあるなど、面倒といえば面倒です。
なので、仮にC++11の機能を使っていいのならば、dll側と関数の宣言があるヘッダなどを共有して、その関数に対してdecltypeで云々するのがおすすめです。
MessageBoxAがuser32.dllにあるとみて明示的リンクで呼び出すテスト#include <windows.h> #include <tchar.h> #include <locale.h> int main(void){ _tsetlocale( LC_ALL, _T("japanese") ); auto const hDLL = LoadLibrary( _T("User32.dll") ); if ( !hDLL ) return 0; typedef decltype(MessageBoxA)* FP_MessageBoxA; //関数名指定でOK //autoを使わない、「()」で初期化という点で一応書式を合わせて出来ることを確認します。 FP_MessageBoxA p( reinterpret_cast<FP_MessageBoxA>( GetProcAddress( hDLL, "MessageBoxA" ) ) ); if ( p ) p( NULL, "ペンギン10時なう", NULL, 0 ); FreeLibrary( hDLL ); return 0; }
もう一つ「?」なポイントなのは
void ( *p )()( なんとか );
という形式で初期化する→十中八九pはローカル変数?→autoではだめなのでしょうか?という点ですね。(昔の資産をそのまま使う場合でのこと・・・あるいはレガシーな環境への移植予定がある、とかでしょうか?)
すべての返信
-
規格で明確にポインタ型に対する関数形式の初期化がはっきり「OK」と書かれている個所は発見はしていませんが、ポインタを特別ダメという表記を見ていない事
n3485(pdf)を見て
6.8 Ambiguity resolutinで
T(*g)(double(3));
が宣言優先ルールでTへのポインタをdouble(3)で初期化とみなすようになる…それゆえ構文的に問題はないが意味的にill-formed、という記述があることなどから、私は現時点ではポインタ型に対する関数形式の初期化子自体はOKと認識しています。(間違っていたら指摘してください。)
またVC++で
auto p( reinterpret_cast< void (*)() >( f( 0, reinterpret_cast< const int* >( 1 ) ) ) );
や
typedef void(*FuncPtr)(); FuncPtr p( reinterpret_cast<FuncPtr>( f( 0, reinterpret_cast< const int* >( 1 ) ) ) );
は通りました。なのでポインタに対する関数形式初期化への問題ではない可能性があると思います。C++11への準拠度はg++の方が進んでいたかと思いますが、この件とは関係ないのではないか、とぼんやり考えています。
ただ、これが通ってしまうと、関数ポインタなので実際は後で変化可能性があるのだと思いますが、このコードではこの瞬間は意味的に
void* vp = nullptr; auto p = (void(*)())vp;
のpと等しくなると思います。void*はオブジェクトへのポインタを受け取れるサイズが保証されますが
5.2.10 Reinterpret castで
Converting a function pointer to an object pointer type or vice versa is conditionally-supported. The meaning of such a conversion is implementation-defined, except that if an implementation supports conversions in both directions, converting a prvalue of one type to the other type and back, possibly with different cv- qualification, shall yield the original pointer value.
とあります。C++03ではillegalだったのがC++11で処理系次第でOKとなった的な感じですが
stackoverflowのCasts between pointer-to-function and pointer-to-object in C and C++のDavid Hammenさんのコメントを読むと、C/C++系統の中でも、このキャストが確実にOKとなる範囲はそれなりに狭いということが言えると思います。なので、移植可能性がある場合は、これによって劇的に楽になる状況でないならば、void*を関数ポインタへキャストすることは避けるのがおすすめです。
-
皆様、ご回答 誠に有難うございました。とても勉強になりました。
その結果「↓のどれかではないか? 」と仮説を立てた為、その裏付けを集めている所です。
・特定条件を満たす構文を使った場合のみ発症してしまう、コンパイラのバグ
・C++の言語仕様上、仕方ない問題。例えば 各コンパイラに実装法を委ねている仕様の所で、今回 問題が起きてしまった
尚 本件は VC++10 Express + Win XP Pro. 32[bit]でも発症しました。
【佐祐理さん】
>規格上、ポインター型に対して、関数形式の初期化子は認められているのでしょうか?
認められるのが妥当と思います。↓の為です。
・ポインタ型だけ認めない事への、合理的理由が考え難い為
・初回投稿時のコード例のように、認められる物がある為
・少なくともVC++7.1時代から、色々なポインタ変数の初期化式を、()で書けていた事実がある為。又 MS C++だけでなく、IC++ BC++ g++ でも使えた為。
>規格上はなんであれ、Visual C++のドキュメントにはポインター型については = expression 形式についてしか言及されていません。初期化子の方にも特に何も書かれてはいませんが。
「MSDNの全域で、ポインタ変数の =で初期化式 しか言及されていない」か、実際に確かめた事はありませんが...
もし実際にそうだとしても、少なくとも「 MSDNが、ポインタ変数の =の初期化式しか言及されていない 」事は、
「 MS C++が、=の初期化式しか 正式仕様として受容しない 」事にはならないです。↓の為です。
・一貫性・可読性を上げる目的で、意図的に "=の初期化式" に統一して説明するようにしている可能性がある為
・上述の通り、昔のVC++から十分許容されていた事実がある為
【mr.setupさん】
>ポインタに対する関数形式初期化への問題ではない可能性があると思います
同感です。()形式で行えるポインタ変数の初期化式は、かなり前から分かっている為です。
>C++11への準拠度はg++の方が進んでいたかと思いますが、この件とは関係ないのではないか
はい、私も投稿前から そう思っておりました。本件はC++98の仕様範囲内の話だからです。
g++4.7.2 を引き合いに出した意図は、「最新の異なるコンパイラ間で、コンパイル可否に差があった」という事実を、参考情報として提示する事です。
>移植可能性がある場合は、これによって劇的に楽になる状況でないならば、void*を関数ポインタへキャストすることは避けるのがおすすめ
仔細な調査、誠に有難うございます。が実際の型変換対象コードは、↓の通りFARPROCです。
// 序数でDLL内の関数を得る。が、C2065エラー
void ( *p )()( reinterpret_cast< void (*)() >( GetProcAddress( hDLL, reinterpret_cast< LPCSTR >( 1 ) ) ) );
// 第2引数の型変換を、旧式で書くと避けられる。が、優先して採るべき解法でない
void ( *p )()( reinterpret_cast< void (*)() >( GetProcAddress( hDLL, (LPCSTR)1 ) ) );
// ()の代わりに=でも避けられる。が、代入文と区別する目的で意図的に()を使ってきた流儀・問題ない筈のコードを、盲目的に変えるべきでない
void ( *p )() = reinterpret_cast< void (*)() >( GetProcAddress( hDLL, (LPCSTR)1 ) );
↑をワザワザ初回投稿時のコード例のように変えた理由は、↓の通りです。
・議題の本質と無関係なGetProcAddress()を排除し 範囲を狭める事で、分かり易くしたかった事
・Win以外の環境との比較結果を提示したかった事
- 編集済み ぶた 2013年4月10日 6:35
-
なるほど、GetProcAddressならcastは致し方ないですねw( 関数を拾うのならFARPROCな以上、関数ポインタ→関数ポインタ変換になるので、これも保証される動作ですし )
仮に規格を精査しバグだという確証がある場合、例えばMicrosoft Connectとかにバグレポートとして提出されてみるのも手かもしれません。
ただ、もしこのvoid (*)()という型が、引数戻り値ともに元の関数通りだったとしても、現状の情報の範囲では呼び出し規約の指定が書かれていないので、dll側と呼び出し側のプロジェクトの設定によってはクラッシュする可能性があります。(少なくともVC++では、書かない場合のデフォルトの呼び出し規約を変更可能)
例としてはWINAPI ( = __stdcall )なら、pの宣言は
void (WINAPI * p)()
など。仕様変更の可能性がある場合、これを確実に合わせるのはそれなりに手間かもしれません。typedefなどがないと、長ったらしくてしかも同じ関数ポインタの型を複数回書かないといけない可能性もあるなど、面倒といえば面倒です。
なので、仮にC++11の機能を使っていいのならば、dll側と関数の宣言があるヘッダなどを共有して、その関数に対してdecltypeで云々するのがおすすめです。
MessageBoxAがuser32.dllにあるとみて明示的リンクで呼び出すテスト#include <windows.h> #include <tchar.h> #include <locale.h> int main(void){ _tsetlocale( LC_ALL, _T("japanese") ); auto const hDLL = LoadLibrary( _T("User32.dll") ); if ( !hDLL ) return 0; typedef decltype(MessageBoxA)* FP_MessageBoxA; //関数名指定でOK //autoを使わない、「()」で初期化という点で一応書式を合わせて出来ることを確認します。 FP_MessageBoxA p( reinterpret_cast<FP_MessageBoxA>( GetProcAddress( hDLL, "MessageBoxA" ) ) ); if ( p ) p( NULL, "ペンギン10時なう", NULL, 0 ); FreeLibrary( hDLL ); return 0; }
もう一つ「?」なポイントなのは
void ( *p )()( なんとか );
という形式で初期化する→十中八九pはローカル変数?→autoではだめなのでしょうか?という点ですね。(昔の資産をそのまま使う場合でのこと・・・あるいはレガシーな環境への移植予定がある、とかでしょうか?)
-
>仮に規格を精査しバグだという確証がある場合、例えばMicrosoft Connectとかにバグレポートとして提出されてみるのも手かも
そうですね。 確証が得られずとも、確認という形で投稿し、何とか取り合ってもらえないか 試すかもしれません。
確約できませんが、もしそういう行動をしたら、結果(URL等)を 本件で ご報告させて頂きます。
>現状の情報の範囲では呼び出し規約の指定が書かれていないので、dll側と呼び出し側のプロジェクトの設定によってはクラッシュする可能性があります
>C++11の機能を使っていいのならば、dll側と関数の宣言があるヘッダなどを共有して、その関数に対してdecltypeで云々するのがおすすめ
なるほど。自前で非生産的な作業( 呼出規約の差異を吸収するコーディング )をするより、コンパイラにやってもらったが良いですね。decltype を有効に使おうと思います。
お話が若干それましたが、副産物として とても有益な助言を頂く事ができました。
※ 尚 呼出規約に関する明示をコード例で行わなかったのは、冗長化して議論の焦点がボケてしまう事を避けたかった為です。現時点の私の文ですら、長いと思っているからです。
>「?」なポイントなのは void ( *p )()( なんとか );
>という形式で初期化する→十中八九pはローカル変数?→autoではだめなのでしょうか?という点
oops! 勿論 ↑ が最良の解法と思います。 前回 ウッカリ解法として書き添えるのを忘れてしまいました。
折角コードを例示して頂いたにも関わらず、誠に失礼致しました。
貴重な時と労を投じて下さり、誠に有難うございました。
- 編集済み ぶた 2013年3月21日 12:46
-
先方から↓の回答( 独自の邦訳 )を頂けた事から、「 問題をMS社に認識して頂けたが、瑣末な事なので今回は対応しない。将来の版で対応するカモ 」と、解釈するに至りました。 皆様、どうも有難うございました。
皆様から弊社にお寄せ頂いた全ご報告も含め、本件の対応可否を検討させて頂いた結果、今回は対応を見合わせる事に決定致しました。
が 将来の出荷においては、この決定を再検討して参ります。
もし本件が お客様の業務遂行上の致命的原因 又は 製品開発や配布の阻害要因 になりえる厳しい物でしたら、
http://support.microsoft.com にご訪問頂くか、1-800-MICROSOFT までお電話を頂きたく存じます。
又 もしお客様がマイクロソフト プレミア カスタマーに加入されているようでしたら、
御社の管理者・技術アカウント管理者・マイクロソフト プレミア アカウント代表者 にご連絡下さいませ。