この記事は2019年1月ごろの Teratailサイトで行われた質問と回答 を転記・微調整したものです。
質問(変形)
Micorsoft Visual C++ 2013 では動作していた下記C++コードを、VC++2017 でコンパイルしたところ bind
関数 の呼び出しでエラーになります。何が変わったのでしょうか?
#include <function>
#include <winsock2.h>
using namespace std;
int main()
{
// (WinSock固有の初期化)
int socket = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in sockaddr;
// (sockaddr構造体の各種メンバ設定)
int status = bind(socket, (LPSOCKADDR)&sockaddr, (int)sizeof(sockaddr));
}
(※上記コードは Takumi_sさん によるオリジナル質問内容から簡略化したものを引用しています。)
回答
エラーの直接原因、および推奨事項は Chironianさん 回答の通りです。
(Chironianさん回答転記)
C++11でstd::bind
が追加されています。VC++ 2013はC++11にはほとんど対応していませんが、VC++ 2017はほぼ対応しています。
結果、std::bind
の方とマッチしたのだろうと思います。
using namespace std;
を止めて、1つ1つstd::
を付けた方が良いと思います。
- WinSockの
bind
関数 ではなく、C++標準ライブラリのstd::bind
関数 が意図せず呼び出されている。 -
using namespace std;
は利用せずに、std::
のように名前空間を明示指定する方が好ましい。
詳細解説
今回のエラーは、C言語向けAPI(BSD socket互換)を提供する WinSockのレガシーな関数設計 と、C++言語による 関数オーバーロード解決規則 が組み合わさって生じたと考えられます。
WinSockのbind
関数は次のプロトタイプ宣言になっています。
int bind(
SOCKET s,
const struct sockaddr *addr,
int namelen
);
一方、C++標準ライブラリのstd::bind
関数テンプレートは(およそ)下記のようなプロトタイプ宣言になっています。可変長テンプレートやForwarding Referenceが使われており少々難解ですが、ここでは “どんな実引数も受け付ける関数テンプレート” になっていると解釈ください。この “どんな実引数でも受付ける” 性質が、今回のトラブル要因の一端となります。
template <class F, class... BoundArgs>
ResultType bind(F&& f, BoundArgs&&... bound_args);
説明のため、bind
関数呼出し元コードのエッセンスだけ抽出します。ここでは、実引数に指定する第1引数型(int
)および第2引数型(struct sockaddr *
)が、WinSock bind
関数プロトタイプ宣言の第1引数型(SOCKET
==unsigned __int64
)および第2引数型(const struct sockaddr *
)とは 厳密一致していない ことに着目してください。
// Windows/WinSock標準ヘッダによる型名定義
typedef unsigned __int64 UINT_PTR;
typedef UINT_PTR SOCKET;
typedef struct sockaddr *LPSOCKADDR;
// 呼出しコード
int socket;
struct sockaddr_in sockaddr;
status = bind(socket, (LPSOCKADDR)&sockaddr, (int)sizeof(sockaddr));
// 第1引数の型=int
// 第2引数の型=struct sockaddr *
// 第3引数の型=int
つまり、この関数呼出しには int
→ unsigned __int64
型変換(整数拡張)と、struct sockaddr *
→ const struct sockaddr *
型変換(const付与)が必要となります。通常であればこれらの型変換は暗黙に行われるため、プログラマが意識する必要はありません。WinSockはC言語時代に設計された古風なAPI仕様ですから、このコードのような型キャストは自然に行われるものと思います。
一方、C++標準ライブラリ std::bind
関数テンプレートでは実引数から全てのパラメータ型を推論するため、第1引数型F
==int
, 第2引数型以降BoundArgs
=={struct sockaddr *
, int
}と推論されます[※]。つまり、全ての実引数型に関数パラメータ型が完全一致する関数が(意図せず)生み出されます。
ソースコード中で using namesapce std;
を行った場合、この std::bind
関数テンプレートが、元々グローバル名前空間にあるWinSockの bind
関数と同列に並びます。C++言語の関数オーバーロード解決規則は非常に複雑ですが、ここでは「型変換が必要となる候補:WinSock bind
関数」と「型変換が不要な候補:C++標準ライブラリstd::bind
型推論結果」が天秤にかけられた結果、プログラマの意図しない後者が選択されています。
※注:厳密にはForwarding Referenceのため、テンプレートパラメータF
==int&
、第1引数型はint&
に型推論されます。また可変テンプレートパラメータBoundArgs
=={struct sockaddr *
, int
}となり、第2,3引数型はそれぞれstruct sockaddr *&&
, int&&
に型推論されます。
おまけ
実は質問のケースでは、下記コードのようにプロトタイプ宣言型と実引数の型を厳密一致させると、期待通りWinSockの bind
が呼び出されるようになります。しかし、この解決方法はあまりにC++言語の難解なルールに依存しているため、素直に名前空間を分けて扱うことをおススメします。
// OK: 明示的なキャストにより全ての引数型を厳密一致させる
status = bind((SOCKET) socket,
(const struct sockaddr *) &sockaddr,
(int) sizeof(sockaddr));