オーバーロード解決の制御 #1:暗黙的な変換の防止

オーバーロードの解決は C++ で最も複雑なことの 1 つですが、ほとんどの場合、それについて考える必要はありません。あなたのコントロール。

最初の投稿では、候補を削除する方法と、それを使用して暗黙の変換を防ぐ方法を示します。

C++11 の =delete

ほとんどの人は、C++11 以降で 01 を指定できることを知っています。 コピー コンストラクターやムーブ コンストラクターなどの特別なメンバー関数の生成を禁止します。 関数と 15

標準では、§8.4.3[dcl.fct.def.delete] の冒頭で単に次のように指定しています:

これは、次のプログラムを作成できることを意味します:

void func() = delete;
 
int main()
{
 func();
}

コンパイルしようとすると、同様のエラー メッセージが表示されます:

それ 機能はあまり役に立ちません。関数が必要ない場合は、まったく宣言しないでください!

しかし、関数がオーバーロードされた場合に何が起こるかを考えてみてください:

#include <iostream>

void func(int)
{
 std::cout << "int\n";
}

void func(double) = delete;

int main()
{
 func(5);
}

22 には 2 つのバージョンがあります 、 33 を取るもの 48 を取る削除されたもの .一見すると、以前よりも便利に見えません.オーバーロードが必要ない場合は、宣言しないでください!

しかし、もう一度見て、§8.4.3:A function with 56 の結果を考えてみてください。 最後に、宣言だけではありません 、それは定義でもあります !そして、名前検索は一致する宣言のみを検索するため 、削除された関数は、オーバーロードの解決に参加できる通常の候補です。

65 と書くと 、 74 で呼び出すようになりました .コンパイラは 87 のオーバーロードを選択します 、削除された関数がオーバーロードの解決に参加し、関数が削除されたと不平を言うためです。

これは 97 を渡すことを禁止します 101 まで 、たとえそれが暗黙的に変換される可能性があっても.

暗黙的な変換の禁止

上記のように、 115 できます オーバーロード解決で特定の暗黙的な変換を回避する候補。

特定の型のセットを受け入れる関数のオーバーロードが 1 つ以上ある場合は、受け入れられる型に暗黙的に変換可能な型で呼び出すこともできます。多くの場合、これは素晴らしく簡潔で、冗長なボイラープレートを回避します。

ただし、これらの暗黙的な変換には、損失やコストがかかる場合があります。ユーザー定義の変換は、120 を使用して制御できます。 、しかし 131 のような言語に組み込まれている暗黙の変換 141 へ ?153 は書けません

しかし、禁止したい型と 163 を取る別のオーバーロードを書くことができます

double だけでなくすべての浮動小数点を禁止して、上記の例を拡張しましょう:

void func(int)
{
 std::cout << "int\n";
}

void func(float) = delete;
void func(double) = delete;
void func(long double) = delete;

現在、浮動小数点で呼び出すことはできません。

テンプレートを使用して 3 つのオーバーロードを生成し、SFINAE を使用して浮動小数点のみを有効にすることもできます。

template <typename T,
 typename = std::enable_if_t<std::is_floating_point<T>::value>>
void func(T) = delete;

暗黙的な変換の禁止:一時変数

ある種の暗黙的な変換は特に悪い場合があります:一時的なものを作成するユーザー定義の変換。

たとえば、176 を受け取る関数に文字列リテラルを渡すと、 一時的な 180 を作成します 引数を初期化します。これは、次の場合に特に驚くべきことです:

void func(const std::string &str);
...
func("Hello, this creates a temporary!");

191の作者はこちら 202 を取りました によって (214 ) 参照は、コストのかかるヒープ割り当てが必要になる可能性があるため、文字列をコピーしたくないためです。しかし、文字列リテラルを渡すと、 一時的なものによるヒープ割り当てを伴います。また、一時的なもの (右辺値、つまり) が 223 にバインドされるためです。 (左辺値) 参照、これは機能します。

多くの場合、これは許容される動作ですが、コストが高すぎて一時的な (偶発的な) 作成を許可できない場合があります。この場合、239 を使用する新しいオーバーロードを導入できます。 、削除されます:

void func(const std::string &str);
void func(const char*) = delete;
...
func("this won't compile");
func(std::string("you have to be explicit"));

関連するメモとして、242 を受け取る関数がある場合があります。 何かへの参照であり、関数はそれへのポインターをどこかに保存します。一時的なものでそれを呼び出すと、コストがかかるだけでなく、致命的になります。一時的なものは - まあ - 一時的であり、ポインターはすぐに破棄されたオブジェクトを指します:

void func(const T &obj)
{
 // store address somewhere outside the function
}
...
func(T()); // dangerous!

ここでは、any を許可しない、より一般的な形式が必要です。 つまり、右辺値参照を取るオーバーロードです:

void func(const T &obj) {...}
void func(T &&) = delete;
...
func(T()); // does not compile

これは機能しますが、完全ではありません。関数 256 があるとします。 269 を返す (何らかの理由で):

const T foo();
void func(const T &obj) {...}
void func(T &&) = delete;
...
func(foo()); // does compile!

273 であるため、これはコンパイルされます 右辺値は 282 以外にバインドしません 右辺値参照など、左辺値のオーバーロードが選択されていますが、これもまた危険です。

ソリューション?シンプルです。290 を使用するだけです 右辺値参照:

const T foo();
void func(const T &obj) {...}
void func(const T &&) = delete;
...
func(foo()); // does not compile

削除されたオーバーロードは any を受け入れます 右辺値、306 または 318 以外 .これは、328 の数少ない適切な使用例の 1 つです。 右辺値参照。

結論

関数のオーバーロードで特定の種類の暗黙的な変換を禁止すると便利な場合があります。それらは高価であったり、損失につながる可能性があるためです。

これは、338 にバインドする一時変数に特に当てはまります。 左辺値参照も危険です。参照されるオブジェクトのアドレスを取得して保存する場合、一時値を引数として許可したくないでしょう。

このようなことを防ぐには、暗黙的に変換される型を取る新しいオーバーロードを定義し、削除済みとしてマークするだけです。一時的なものを防ぐ場合、新しいオーバーロードは 349 を取る必要があります。 適切な型への右辺値参照。

オーバーロードの解決では、完全一致が優先され、削除されたオーバーロードが選択されます。これにより、コンパイル時エラーが発生します。

このミニ シリーズの次の投稿では、この手法をさらに使用して、失敗したオーバーロードの解決に関するエラー メッセージを改善し、削除された関数が選択されたときのエラー メッセージを完全にカスタマイズする方法を紹介します。