4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

C++Advent Calendar 2022

Day 13

bind(BSD socket/WinSock) vs. std::bind(C++)

Last updated at Posted at 2022-12-12

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

つまり、この関数呼出しには intunsigned __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));

類似のトラブル事例

std::bind and winsock.h bind confusion

4
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?