SFINAE и адрес перегруженной функции

Я экспериментирую с разрешением адреса перегруженной функции (bar) в контексте другого параметра функции (foo1/foo2).

struct Baz {};

int bar() { return 0; }
float bar(int) { return 0.0f; }
void bar(Baz *) {}

void foo1(void (&)(Baz *)) {}

template <class T, class D>
auto foo2(D *d) -> void_t<decltype(d(std::declval<T*>()))> {}

int main() {
    foo1(bar);      // Works
    foo2<Baz>(bar); // Fails
}

Нет проблем с foo1, который явно указывает тип bar.

Однако foo2, который отключает себя через SFINAE для всех, кроме одной версии bar, не может скомпилироваться со следующим сообщением:

main.cpp:19:5: fatal error: no matching function for call to 'foo2'
    foo2<Baz>(bar); // Fails
    ^~~~~~~~~
main.cpp:15:6: note: candidate template ignored: couldn't infer template argument 'D'
auto foo2(D *d) -> void_t<decltype(d(std::declval<T*>()))> {}
     ^
1 error generated.

Я понимаю, что С++ не может разрешить перегруженный адрес функции и одновременно выполнять вывод аргумента шаблона.

Это причина? Можно ли скомпилировать foo2<Baz>(bar); (или что-то подобное)?

Ответ 1

Как упоминалось в комментариях, [14.8.2.1/6] (рабочий проект, выводящий аргументы шаблона из вызова функции) в этом дело (внимание мое):

Когда P - это тип функции, тип указателя функции или указатель на тип функции-члена:

  • Если аргумент представляет собой набор перегрузки, содержащий один или несколько шаблонов функций, этот параметр рассматривается как невыводимый контекст.

  • Если аргумент представляет собой набор перегрузки (не содержащий шаблонов функций), попытка пробного аргумента пытается использовать каждый из членов набора. Если вывод только для одного из элементов набора перегрузки, этот член используется как значение аргумента для вычитания. Если вывод больше, чем один член набора перегрузки, этот параметр рассматривается как невыводимый контекст.

SFINAE принимает участие в игре после завершения вычитания, поэтому она не помогает работать со стандартными правилами.
Для получения дополнительной информации вы можете увидеть примеры в конце упомянутой выше пули.

О вашем последнем вопросе:

Есть ли способ скомпилировать foo2<Baz>(bar); (или что-то подобное)?

Две возможные альтернативы:

  • Если вы не хотите изменять определение foo2, вы можете вызвать его как:

    foo2<Baz>(static_cast<void(*)(Baz *)>(bar));
    

    Таким образом, вы явно выбираете функцию из набора перегрузки.

  • Если изменение foo2 разрешено, вы можете переписать его как:

    template <class T, class R>
    auto foo2(R(*d)(T*)) {}
    

    Это более или менее то, что у вас было до этого, no decltype в этом случае и тип возврата, который вы можете свободно игнорировать.
    На самом деле вам не нужно использовать какую-либо функцию SFINAE'd, чтобы этого сделать достаточно. В этом случае foo2<Baz>(bar); будет правильно разрешено.

Ответ 2

Какой-то общий ответ здесь: Выражение SFINAE для перегрузки по типу переданного указателя функции

В практическом случае нет необходимости использовать черты типа или decltype() - хорошее старое разрешение перегрузки выберет для вас наиболее подходящую функцию и разделит ее на "аргументы" и "тип возврата". Просто перечислите все возможные соглашения о вызовах

// Common functions
template <class T, typename R> void foo2(R(*)(T*)) {}

// Different calling conventions
#ifdef _W64
template <class T, typename R> void foo2(R(__vectorcall *)(T*)) {}
#else
template <class T, typename R> void foo2(R(__stdcall *)(T*)) {}
#endif

// Lambdas
template <class T, class D>
auto foo2(const D &d) -> void_t<decltype(d(std::declval<T*>()))> {}

Может быть полезно обернуть их в шаблонную структуру

template<typename... T>
struct Foo2 {
    // Common functions
    template <typename R> static void foo2(R(*)(T*...)) {}
    ...
};
Zoo2<Baz>::foo2(bar);

Хотя для этого потребуется больше кода для функций-членов, так как они имеют модификаторы (const, volatile, &&)