オーバーロードされた関数ポインターとその引数を渡す際の不正な型の推定


8

std::invoke関数がオーバーロードされている場合でも、関数タイプを推定する作業を行うためのラッパーを提供しようとしています。
(私は昨日、可変およびメソッドポインターバージョンについて関連する質問をしました)。

関数に1つの引数がある場合、このコード(C ++ 17)は通常の過負荷状態で期待どおりに機能します。

#include <functional>

template <typename ReturnType, typename ... Args>
using FunctionType = ReturnType (*)(Args...);

template <typename S, typename T>
auto Invoke (FunctionType<S, T> func, T arg)
{   
    return std::invoke(func, arg);
}

template <typename S, typename T>
auto Invoke (FunctionType<S, T&> func, T & arg)
{   
    return std::invoke(func, arg);
}

template <typename S, typename T>
auto Invoke (FunctionType<S, const T&> func, const T & arg)
{
    return std::invoke(func, arg);
}

template <typename S, typename T>
auto Invoke (FunctionType<S, T&&> func, T && arg)
{   
    return std::invoke(func, std::move(arg));
}

入力引数を増やすには、明らかにコードの膨張を抑える必要がありますが、それは別の問題です。

ユーザーにconst / referencesだけが異なるオーバーロードがある場合、次のようになります。

#include <iostream>

void Foo (int &)
{
    std::cout << "(int &)" << std::endl;
}

void Foo (const int &)
{
    std::cout << "(const int &)" << std::endl;
}

void Foo (int &&)
{
    std::cout << "(int &&)" << std::endl;
}

int main()
{
    int num;
    Foo(num);
    Invoke(&Foo, num);

    std::cout << std::endl;

    Foo(0);
    Invoke(&Foo, 0);
}

次にInvoke、g ++出力を使用して、関数を誤って推定します。

(int&)
(const int&)

(int &&)
(const int&)

そしてclang ++:

(int&)
(const int&)

(int &&)
(int &&)

(clangの出力が異なることを指摘してくれたgezaに感謝します)。

したがってInvoke、未定義の動作があります。

メタプログラミングがこの問題に取り組む方法だと私は思います。とにかく、Invokeサイトで型の推論を正しく処理することは可能ですか?


予想される出力は何ですか?(int&)(int &&)ですか?
LF

@LF、うん。これらはFooの出力なので、Invokeの出力でもあるはずです。
エリオット

1
私にとって、clangの結果は異なり(int &&)ます。2番目のケースでは2回出力されます。
下座

2
それはS議論の控除と関係があるに違いない。のconst T &バージョンをコメントアウトしてInvoke、エラーに注意してください。また、引数が明示的に指定されている場合(Invoke<void>(&Foo, num))、正しいバージョンが呼び出されます。
aparpara

2
最初のケースの理論は次のとおりです。コンパイラがnon-constを検討するInvokeと、constとnon-constの両方でインスタンス化できますFoo。また、戻り値の型(S)が両方とも同じであるかどうかはチェックされないため、推定できませんS。そのため、このテンプレートは無視されます。constのインスタンス化はconstでInvokeのみ実行FooできるためS、この場合は推論できます。したがって、コンパイラはこのテンプレートを使用します。
下座

回答:


1

理論

以下のために、各関数テンプレートInvoke、テンプレート引数控除は(それはそれを検討するためにオーバーロードの解決のために成功しなければなりません)とみなし、それぞれ Foo、それがために(ここでは、2)しかし、多くのテンプレートパラメータ推定できるかどうかを確認するために1つの関数のパラメータ(func関与)を。全体的な演繹は、正確に1つがFoo一致する場合にのみ成功します(それ以外の場合は、演繹する方法がないためですS)。(これは多かれ少なかれコメントで述べられました。)

最初の(「値による」)Invokeが存続することはありませんFoo。任意のから推測できます。同様に、2番目の(「非const参照」)オーバーロードは最初の2つを受け入れますFoo。これらは(の)の他の引数に関係なく適用されることに注意してください!Invokearg

3番目の(const T&)オーバーロードは、対応するFooオーバーロードを選択し、T= を推定しintます。最後は、最後のオーバーロード(T&&通常の右辺値参照)で同じことを行うため、その普遍的な参照(その場合は(または)Tとして推論され、の推論と競合します)にもかかわらず、左辺値引数を拒否します。int&const int&func

コンパイラ

引数が場合はarg右辺値で、両方のもっともらしい(いつものようにと、constのではありません)Invokeオーバーロードが控除で成功し、T&&(それが右辺値参照を結合するので、過負荷が勝つべきで右辺値)。

コメントからの場合:

template <typename U>
void Bar (U &&);
int main() {
  int num;
  Invoke<void>(&Bar, num);
}

&Bar関数テンプレートが含まれているため、からのT推定は行われません。したがってint、すべての場合に(として)正常に推定されます。その後、控除が発生し、再び識別するためにそれぞれの場合についてBar推定する、使用に特化(もしあれば)Uとして失敗int&const int&、およびint&それぞれ。int&例は、同一とはっきり優れているので、呼び出しはあいまいです。

だから、クランは右ここにあります。(ただし、ここには「未定義の動作」はありません。)

解決

一般的な答えはありません。特定のパラメータタイプは複数の値カテゴリ/ const-qualificationペアを受け入れることができるため、そのようなすべてのケースでオーバーロード解決を正しくエミュレートすることは容易ではありません。何らかの方法でオーバーロードセットを具体化する提案がありました。あなたはそれらのラインに沿った現在のテクニックの1つを考えるかもしれません(ターゲット関数名ごとの一般的なラムダのような)。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.