オーバーロード解決の制御 #2:失敗したオーバーロード解決のエラー メッセージの改善

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

2 番目の投稿では、オーバーロードの解決に失敗したときのエラー メッセージを改善する簡単な方法と、それを完全にカスタマイズする方法を示します。

モチベーション

最初の例には、おそらく最も長いエラー メッセージが表示されます。

#include <iostream>
#include <string>

struct foo
{
 // ...
};

std::ostream& operator>>(std::ostream &os, const foo &f)
{
 // print f
 return os;
}

int main()
{
 foo f;
 std::cout << f;
}

プログラマーは、出力演算子 (または必要に応じてストリーム挿入演算子) と思われるものを使用してユーザー定義型を定義しましたが、 04 をオーバーロードする代わりに 、彼はタイプミスをして 19 をオーバーロードしました .

GCC は次で始まるエラー メッセージを生成します:

main.cpp: In function ‘int main()’:
 main.cpp:18:15: error: no match for ‘operator<<’ (operand types are ‘std::ostream {aka std::basic_ostream<char>}’ and ‘foo’)
 std::cout << f;
 ^
 In file included from /usr/include/c++/5.2.0/iostream:39:0,
 from main.cpp:1:
 /usr/include/c++/5.2.0/ostream:628:5: note: candidate: std::basic_ostream<_CharT, _Traits>& std::operator<<(std::basic_ostream<_CharT, _Traits>&&, const _Tp&) [with _CharT = char; _Traits = std::char_traits<char>; _Tp = foo] <near match>
 operator<<(basic_ostream<_CharT, _Traits>&& __os, const _Tp& __x)
 ^
 /usr/include/c++/5.2.0/ostream:628:5: note: conversion of argument 1 would be ill-formed:
 main.cpp:18:18: error: cannot bind ‘std::ostream {aka std::basic_ostream<char>}’ lvalue to ‘std::basic_ostream<char>&&’
 std::cout << f;
 ^
 In file included from /usr/include/c++/5.2.0/iostream:39:0,
 from main.cpp:1:
> /usr/include/c++/5.2.0/ostream:108:7: note: candidate: std::basic_ostream<_CharT, _Traits>::__ostream_type& std::basic_ostream<_CharT, _Traits>::operator<<(std::basic_ostream<_CharT, _Traits>::__ostream_type& (*)(std::basic_ostream<_CharT, _Traits>::__ostream_type&)) [with _CharT = char; _Traits = std::char_traits<char>; std::basic_ostream<_CharT, _Traits>::__ostream_type = std::basic_ostream<char>]
 operator<<(__ostream_type& (*__pf)(__ostream_type&))
 ^
 ....

エラー メッセージの後に、他のすべての候補のリストが続きます。216 行、合計 17,686 文字です!すべて単純なタイプミスが原因です。

別の例

あまり問題なく拡張できる、より単純で短い例を考えてみましょう。

あなたはおそらく知っています - そしてうまくいけば使用しないでください! - 古い C のトリック - またはハック - 配列のサイズを計算する:21 .ただし、問題があります:ほとんどすべてのインスタンスで配列がポインターに減衰し、関数パラメーターを配列として宣言しても、実際にはポインターです!この動作は C から継承されました.

したがって、素朴なプログラマーが関数内で配列のトリックを使用すると、問題が発生します:

void func(int array[]) // actually a pointer, not an array!
{
 auto size = sizeof(array) / sizeof(array[0]); // actually: sizeof(int*) / sizeof(int)!
 ....
}

int main()
{
 int array[4];
 func(array); // array to pointer decay here
}

コードは配列のサイズを計算しません。ポインターのサイズを 34 のサイズで割ります。 .非常に特殊なシステムでない限り、これはおそらく 40 ではありません。 .

では、C++ プログラマーは何をするのでしょうか?

C++ プログラマーは関数を書きます。名前を 54 としましょう。 、サイズを計算します。C++ にはテンプレートがあるため、古い 61 を使用する必要はありません。 「トリック」:

template <typename T, std::size_t N>
constexpr std::size_t array_size(T(&)[N])
{
 return N:
}

この関数は、参照によって配列を受け取り、テンプレート引数の推測で配列の大きさを把握します。

ここで、プログラマーが 72 を使用する場合 87 の代わりに 、彼はエラーを受け取ります:

prog.cpp: In function 'void func(int*)':
prog.cpp:17:18: error: no matching function for call to 'array_size(int*&)'
 auto size = array_size(array);
 ^
prog.cpp:4:23: note: candidate: template<class T, unsigned int N> constexpr std::size_t array_size(T (&)[N])
 constexpr std::size_t array_size(T(&)[N])
 ^
prog.cpp:4:23: note: template argument deduction/substitution failed:
prog.cpp:17:18: note: mismatched types 'T [N]' and 'int*'
 auto size = array_size(array);
 ^

ランタイム バグをコンパイル時エラーに変換しました。これははるかに優れていますが、この投稿の要点はエラー メッセージを改善することなので、それを実行しましょう。

削除されたフォールバック

前回の投稿で 94 の使い方を紹介しました 関数がオーバーロードされている場合、これにより、削除された候補の引数の型で呼び出すことができなくなります。

これこそまさに私たちが望んでいることです!

104 に配列以外を渡した場合 ,これはベース候補をリストするべきではありません.したがって、常に有効なフォールバック関数が必要です.しかし、このフォールバックは存在しないはずなので、113

しかし、フォールバック関数の引数は何ですか?それは何でも取ることができなければならず、決して有効な関数よりも良く一致してはなりません.そうでなければ正しい型がフォールバックに行きます.

この場合、単一の引数を値で受け取るテンプレート関数を書くだけで十分です。配列型は、より特殊化されているため、常に最初のオーバーロードにバインドされ、それ以外はすべて値によるオーバーロードにバインドされます。したがって、このフォールバック オーバーロードを宣言します。 126 としてマークします :

template <typename T, std::size_t N>
constexpr std::size_t array_size(T(&)[N])
{
 return N:
}

// deleted fallback overload
template <typename T>
constexpr std::size_t array_size(T) = delete;

同じ呼び出しの結果:

 prog.cpp: In function 'void func(int*)':
 prog.cpp:20:30: error: use of deleted function 'constexpr std::size_t array_size(T) [with T = int*; std::size_t = unsigned int]'
 auto size = array_size(array);
 ^
 prog.cpp:10:23: note: declared here
 constexpr std::size_t array_size(T) = delete;
 ^

これは、以前の配列に対する大きな改善とは思えないかもしれませんが、多くの有効なオーバーロード (136 など) を持つ関数にとっては )、コンパイラは他のすべての候補をリストしないため、これは大きくなる可能性があります。

ここで投稿を終了することもできますが、あまり満足できません.エラー メッセージには、過負荷の解決に失敗した理由が示されていません.代わりに、完全にカスタマイズされたエラー メッセージを提供するのが良いと思いませんか?

カスタマイズされたエラー メッセージ

私が欲しいのは、フォールバックが選択されたときのカスタム エラー メッセージです。カスタム エラー メッセージは 140 のように聞こえます 、それで試してみましょう:

template <typename T>
constexpr std::size_t array_size(T)
{
 static_assert(false, "array-to-pointer decay has occured, cannot give you the size");
 return 0; // to silence warnings
}

154 を挿入しました これは、オーバーロードの解決によって選択されたときにエラー メッセージをトリガーする必要があります。

169 を除いて コードを不正な形式としてマークすることを熱望しています。 §14.6[temp.res]/8:

これは基本的に、「テンプレートに無効なコードが含まれていることがわかったら、すぐにそう言うことができる」という意味です。そして、clang と GCC はインスタンス化の前にそれを行い、170 を許可します。 MSVC がインスタンス化まで待機している間、すぐにトリガーします。

したがって、コンパイラに 189 を評価させる必要があります。 テンプレートが実際にインスタンス化されたときのみ.これは、ブール式をテンプレートパラメータに依存させることによって行うことができます.その場合、コンパイラはインスタンス化の前に式を評価できません.最も一般的な方法は次のとおりです:

template <typename T>
constexpr std::size_t array_size(T)
{
 static_assert(sizeof(T) != sizeof(T), "array-to-pointer decay has occured, cannot give you the size");
 return 0; // to silence warnings
}

192 のサイズ 実際にインスタンス化された 202 に依存します 、したがって、インスタンス化後にのみ使用可能になります。これは機能しますが、解決策が非常に読みやすいとは思いません。 いつも 229 に等しい したがって、 239 をトリガーします

そこで、次のことをお勧めします:

template <typename T>
struct not_an_array
{
 static constexpr bool error = false;
};

template <typename T>
constexpr std::size_t array_size(T)
{
 static_assert(not_an_array<T>::error, "array-to-pointer decay has occured, cannot give you the size");
 return 0; // to silence warnings
}

241 のため、これは機能します 253 の異なる値を持つ特定のタイプに特化されている可能性があります 元のコードでこのフォールバックを使用すると、次のエラー メッセージが表示されます:

 prog.cpp: In instantiation of 'constexpr std::size_t array_size(T) [with T = int*; std::size_t = unsigned int]':
 prog.cpp:24:30: required from here
 prog.cpp:18:5: error: static assertion failed: array-to-pointer decay has occured, cannot give you the size
 static_assert(not_an_array<T>::error, "array-to-pointer decay has occured, cannot give you the size");
 ^

これは私が欲しかった、完全にカスタマイズされたエラー メッセージです。

この手法には欠点があることに注意してください:SFINAE を使用して、呼び出しが整形式かどうかを検出することはできません。 関数本体を調べません。

結論

関数を呼び出してオーバーロードの解決に失敗した場合、多くの場合、考えられるすべての候補をリストした非常に長いエラー メッセージが表示されます。これを回避するには、最後の手段として選択されたテンプレート化されたフォールバック オーバーロードを作成するだけです。コード> d または 281 で構成される 294 で テンプレート パラメータに応じたブール値。後者のバージョンでは、完全にカスタマイズされたエラー メッセージが可能です。

関数のオーバーロードが多く、候補がないときに明確なメッセージが必要な場合に適用できます (308 など)。 )または、関数が 1 つしかなく、エラー メッセージ内に詳細情報が必要な場合でも、失敗した場合 (312 のように)

シリーズの次の投稿では、オーバーロードを選択する方法を正確に制御するための非常に強力な方法であるタグ ディスパッチを紹介します。