Вывод неправильного типа при передаче указателя перегруженной функции и ее аргументов

Я пытаюсь предоставить оболочку вокруг std::invoke, чтобы выполнять работу по выводу типа функции, даже когда функция перегружена.
(я спросил связанный вопрос вчера для версии с переменным числом аргументов и указателем метода).

Когда функция имеет один аргумент, этот код (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++:

(целое &)
(константное целое &)

(целое &&)
(константное целое &)

И клан++:

(целое &)
(константное целое &)

(целое &&)
(целое &&)

(Спасибо geza за указание на то, что выходы clang были другими).

Итак, Invoke имеет неопределенное поведение.

Я подозреваю, что метапрограммирование было бы подходом к решению этой проблемы. Независимо от этого, возможно ли правильно обрабатывать вывод типа на сайте Invoke?


person Elliott    schedule 08.03.2020    source источник
comment
Каков ожидаемый результат? Это (int&) (int&&)?   -  person L. F.    schedule 08.03.2020
comment
@L.F., ага. Это выходные данные Foo, поэтому они также должны быть выходными данными Invoke.   -  person Elliott    schedule 08.03.2020
comment
Для меня clang дает другой результат: он печатает (int &&) дважды для второго случая.   -  person geza    schedule 08.03.2020
comment
@geza, На самом деле, у меня тоже самое. Я явно ошибся. Я исправлю это. Спасибо. Очевидно, что Invoke ведет себя неопределенно.   -  person Elliott    schedule 08.03.2020
comment
Должно быть, это как-то связано с выводом S аргумента. Попробуйте закомментировать const T & версию Invoke и обратите внимание на ошибку. Также, если аргумент указан явно (Invoke<void>(&Foo, num)), вызывается правильная версия.   -  person aparpara    schedule 08.03.2020
comment
@aparpara, ты прав! Я не ожидал, что тип возвращаемого значения вызовет проблему, учитывая, что все возвращаемые типы в этом примере одинаковы. Что касается удаления const T&, то это просто уход от проблемы без ее решения.   -  person Elliott    schedule 08.03.2020
comment
Вот теория для первого случая: когда компилятор рассматривает неконстантный Invoke, он может создать его как с константным, так и с неконстантным Foo. И он не проверяет, что возвращаемый тип (S) одинаков для обоих, поэтому он говорит, что не может вывести S. Поэтому он игнорирует этот шаблон. В то время как создание экземпляра const Invoke может быть выполнено только с const Foo, поэтому в этом случае он может вывести S. Следовательно, компилятор использует этот шаблон.   -  person geza    schedule 08.03.2020
comment
Я не знаю, что именно происходит и, прежде всего, кто прав между g++ и clang++, но... мне кажется, что вы игнорируете то, что ссылка && аргумента функции Foo() является ссылкой совершенно другого типа ( ссылка на значение r) по сравнению с && для arg из Invoke (это ссылка на значение шаблона, а также ссылка на пересылку).   -  person max66    schedule 08.03.2020
comment
@ max66, я не понимаю, что ты имеешь в виду. Как вы думаете, почему Invoke не принимает это во внимание?   -  person Elliott    schedule 08.03.2020
comment
Во-первых, потому что вы используете (внутри && версии Invoke()) std::move() вместо std::forward; во-вторых, потому что мне кажется (возможно, я ошибаюсь), что ваш код основан на идее, что T && arg всегда есть T && (в шаблонной функции).   -  person max66    schedule 08.03.2020
comment
@ max66, хорошо, я думаю, что понимаю, о чем вы говорите, хотя я не уверен, что согласен с std::move, мое понимание было (правильным или нет), что любая входная функция Bar(T&&) будет выведена как Bar (T&), если вход был lvalue . Если вы замените main следующим кодом, он будет вести себя так, как я ожидал в g++, но не будет компилироваться в clang++: template <typename T> void Bar (T && t) { Foo(std::forward<T>(t)); } int main() { int num; Bar(num); Invoke<void>(&Bar, num); }   -  person Elliott    schedule 09.03.2020
comment
@Elliott - Честно говоря, я не знаю, что именно происходит (у меня большие проблемы с пониманием идеальных деталей пересылки), но мне кажется, что вы правы, когда говорите, что любая функция ввода Bar(T&&) будет выведена как Bar (T&), если бы ввод был lvalue. Поэтому, если вы используете std::move() вместо l-значения, вы (потенциально) совершите катастрофу: внутри функции вы можете сделать функцию устаревшей, когда ожидается, что извне она останется действительной.   -  person max66    schedule 09.03.2020
comment
@ max66, я имел в виду, что если Bar(T&&) выводится в Bar(int&), скажем, из-за вызова int lvalue, я думал, что когда вы делаете int x; Invoke(&Bar, x), он вызовет Invoke (FunctionType<S, T&>, T&). Это то, что происходит с g++ (используя мой код из моего предыдущего комментария и помещая печать в вызовы), но, к сожалению, это вызывает ошибку компиляции для clang++.   -  person Elliott    schedule 09.03.2020


Ответы (1)


Теория

Для каждого шаблона функции Invoke вывод аргумента шаблона (который должен быть выполнен успешно для разрешения перегрузки) рассматривает каждый Foo, чтобы увидеть, может ли он вывести любое количество параметров шаблона (здесь , two) для задействованного функционального параметра one (func). Общий вывод может быть успешным только в том случае, если совпадает ровно один Foo (поскольку в противном случае невозможно вывести S). (Это было более или менее указано в комментариях.)

Первый («по значению») Invoke никогда не выживает: он может вывести любой из Foo. Точно так же вторая (не-const ссылка) перегрузка принимает первые два Foo. Обратите внимание, что они применяются независимо от другого аргумента для Invoke (для arg)!

Третья (const T&) перегрузка выбирает соответствующую перегрузку Foo и выводит T=int; последний делает то же самое с последней перегрузкой (где T&& является обычной ссылкой rvalue) и, следовательно, отклоняет аргументы lvalue, несмотря на его универсальную ссылку (которая выводит T как int& (или const int&) в этом случае и противоречит выводам func).

Компиляторы

Если аргумент для arg является значением r (и, как обычно, не является константой), обе возможные перегрузки Invoke преуспеют в дедукции, и перегрузка T&& должна победить (поскольку она связывает ссылку rvalue с значением r).

Для случая из комментариев:

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

Никакого вывода из &Bar не происходит, поскольку используется шаблон функции, поэтому T успешно выводится (как int) в каждом случае. Затем для каждого случая снова выполняется вывод, чтобы определить используемую специализацию Bar (если она есть), и U выводится как fail, int&, const int& и int& соответственно. Корпуса int& идентичны и явно лучше, поэтому вызов неоднозначен.

Итак, Clang прямо здесь. (Но здесь нет «неопределенного поведения».)

Решение

У меня нет для вас общего ответа; поскольку некоторые типы параметров могут принимать несколько пар «значение-категория/константа-квалификация», правильно эмулировать разрешение перегрузки во всех таких случаях будет непросто. Были предложения так или иначе материализовать наборы перегрузок; вы можете рассмотреть один из существующих методов в этом направлении (например, общая лямбда для каждого имени целевой функции).

person Davis Herring    schedule 09.03.2020