Не является ли аргумент шаблона (подпись) части std:: function его типа?

Учитывая следующий код, в чем причина двусмысленности? Могу ли я обойти это или мне придется держать (раздражающие) явные приведения?

#include <functional>

using namespace std;

int a(const function<int ()>& f)
{
    return f();
}

int a(const function<int (int)>& f)
{
    return f(0);
}

int x() { return 22; }

int y(int) { return 44; }

int main()
{
    a(x);  // Call is ambiguous.
    a(y);  // Call is ambiguous.

    a((function<int ()>)x);    // Works.
    a((function<int (int)>)y); // Works.

    return 0;
}

Интересно, что если я прокомментирую функцию a() с параметром function<int ()> и вызовом a(x) в своей основной части, компиляция корректно завершится ошибкой из-за несоответствия типа x и аргумента function<int (int)> доступна только функция a(). Если компилятор не работает в этом случае, почему возникает какая-либо двусмысленность, когда присутствуют две функции a()?

Я пробовал VS2010 и g++ v. 4.5. Оба дают мне ту же самую двусмысленность.

Ответ 1

Проблема состоит в том, что как function<int()>, так и function<int(int)> являются конструктивными из одной и той же функции. Вот как выглядит объявление конструктора std::function в VS2010:

template<class _Fx>
function(_Fx _Func, typename _Not_integral<!_Is_integral<_Fx>::value, int>::_Type = 0);

Игнорируя часть SFINAE, она конструктивна из всего, что угодно.
std::/boost::function используйте метод, называемый стиранием типа, чтобы позволить передавать суровые объекты/функции, так что они долго удовлетворяют сигнатуре при вызове. Один из недостатков заключается в том, что вы получаете ошибку в самой глубокой части реализации (где вызывается функция сохранения) при поставке объекта, который не может быть вызван, как требуется для его подписи, а не в конструкторе.


Проблема может быть проиллюстрирована этим небольшим классом:

template<class Signature>
class myfunc{
public:
    template<class Func>
    myfunc(Func a_func){
        // ...
    }
};

Теперь, когда компилятор ищет допустимые функции для набора перегрузки, он пытается преобразовать аргументы, если не существует идеальной функции фитинга. Преобразование может происходить через конструктор параметра функции или через оператор преобразования аргумента, заданного функции. В нашем случае это первое.
Компилятор пытается выполнить первую перегрузку a. Чтобы сделать его жизнеспособным, он должен сделать преобразование. Чтобы преобразовать a int(*)() в myfunc<int()>, он пытается создать конструктор myfunc. Являясь шаблоном, который принимает что-либо, преобразование, естественно, преуспевает.
Теперь он пытается сделать то же самое со второй перегрузкой. Конструктор все еще остается тем же и все еще берет что-либо, что ему дано, конверсия тоже работает.
Оставаясь с двумя функциями в наборе перегрузки, компилятор является грустным panda и не знает, что делать, поэтому он просто говорит, что вызов неоднозначен.


Итак, в конце часть Signature шаблона относится к типу при создании объявлений/определений, но не в том случае, когда вы хотите построить объект.


Edit:
При всем моем внимании, отвечая на титульный вопрос, я полностью забыл о вашем втором вопросе.: (

Могу ли я обойти это или мне придется держать (раздражающие) явные приведения?

Afaik, у вас есть 3 варианта.

  • Держите актерский состав
  • Сделайте объект function соответствующего типа и передайте его

    function<int()> fx = x; function<int(int)> fy = y; a(fx); a(fy);

  • Скрыть утомительное кастинг в функции и использовать TMP для получения правильной подписи

Версия TMP (шаблон метапрограммирования) довольно многословна и имеет шаблонный код, но скрывает кастинг от клиента. Примерную версию можно найти здесь, которая основана на метафайле get_signature, который частично специализируется на типах указателей функций (и дает хороший пример того, как сопоставление шаблонов может работать в С++):

template<class F>
struct get_signature;

template<class R>
struct get_signature<R(*)()>{
  typedef R type();
};

template<class R, class A1>
struct get_signature<R(*)(A1)>{
  typedef R type(A1);
};

Конечно, это нужно расширить для количества аргументов, которые вы хотите поддерживать, но это делается один раз, а затем зарывается в заголовок "get_signature.h".:)

Другим вариантом, который я рассматриваю, но немедленно отброшенным, была SFINAE, которая представила бы еще более шаблонный код, чем версия TMP.

Итак, да, это варианты, о которых я знаю. Надеюсь, один из них работает на вас.:)

Ответ 2

Я видел, что этот вопрос возникает слишком много раз. libС++ теперь компилирует этот код без двусмысленности (как соответствующее расширение).

Просроченное обновление

Это "расширение" оказалось достаточно популярным, поскольку оно было стандартизировано в С++ 14 (хотя я лично не был ответственным за выполнение этой работы).

Оглядываясь назад, я не получил это расширение точно. Ранее в этом месяце (2015-05-09) комитет проголосовал в LWG issue 2420, который эффективно изменяет определение Callable, чтобы, если std::function имеет возвращаемый тип void, он будет игнорировать возвращаемый тип обернутого функтора, но в остальном в противном случае считал бы его Callable, если все остальные совпадают, вместо того, чтобы рассматривать его не как Callable.

Эта настройка после С++ 14 не влияет на этот конкретный пример, поскольку задействованные типы возвращаемых последовательно int.

Ответ 3

Вот пример того, как обернуть std::function в класс, проверяющий invokability его параметров конструктора:

template<typename> struct check_function;
template<typename R, typename... Args>
struct check_function<R(Args...)>: public std::function<R(Args...)> {
    template<typename T,
        class = typename std::enable_if<
            std::is_same<R, void>::value
            || std::is_convertible<
                decltype(std::declval<T>()(std::declval<Args>()...)),
                R>::value>::type>
        check_function(T &&t): std::function<R(Args...)>(std::forward<T>(t)) { }
};

Используйте это:

int a(check_function<int ()> f) { return f(); }
int a(check_function<int (int)> f) { return f(0); }

int x() { return 22; }
int y(int) { return 44; }

int main() {
    a(x);
    a(y);
}

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

template<typename> struct check_function_exact;
template<typename R, typename... Args>
struct check_function_exact<R(Args...)>: public std::function<R(Args...)> {
    template<typename T,
        class = typename std::enable_if<
            std::is_convertible<T, R(*)(Args...)>::value>::type>
        check_function_exact(T &&t): std::function<R(Args...)>(std::forward<T>(t)) { }
};

Ответ 4

std::function<T> имеет преобразование ctor, которое принимает произвольный тип (т.е. нечто иное, чем a T). Конечно, в этом случае ctor приведет к ошибке несоответствия типа, но компилятор не дойдет до этого - вызов неоднозначен просто потому, что ctor существует.