Неверный вывод типа при передаче указателя на перегруженную функцию и ее аргументов - PullRequest
8 голосов
/ 08 марта 2020

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

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

Уменьшение раздувания кода очевидно, требуется больше входных аргументов, но это отдельная проблема.

Если у пользователя есть перегрузки, отличающиеся только константой / ссылками, вот так:

#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 &)

и лязг ++:

(int &)
(const int &)

(int &&)
(int &&)

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

Так что Invoke имеет неопределенное поведение.

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

1 Ответ

1 голос
/ 09 марта 2020

Теория

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

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

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

Компиляторы

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

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

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 прямо здесь. (Но здесь нет «неопределенного поведения».)

Решение

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

...