この記事は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));