Разрешение перегрузки с помощью std:: function

Рассмотрим этот пример кода:

#include <iostream>
#include <functional>

typedef std::function<void()> func1_t;
typedef std::function<void(int)> func2_t;

struct X
{
   X (func1_t f)
   { }

   X (func2_t f)
   { }
};

int main ( )
{
   X x([](){ std::cout << "Hello, world!\n"; });
}

Я был уверен, что он не должен компилироваться, потому что компилятор не должен выбирать один из двух конструкторов. g++ - 4.7.3 показывает это ожидаемое поведение: он говорит, что вызов перегруженного конструктора неоднозначен. Однако g++ - 4.8.2 успешно компилирует его.

Является ли этот код правильным в С++ 11 или это ошибка/особенность этой версии g++?

Ответы

Ответ 1

В С++ 11...

Посмотрим на спецификацию шаблона конструктора std::function (который принимает любую Callable): [func.wrap.func.con]/7-10

template<class F> function(F f);
template <class F, class A> function(allocator_arg_t, const A& a, F f);

7 Требуется: F должно быть CopyConstructible. F должен быть Callable (20.10.11.2) для типов аргументов ArgTypes и типа возврата R. Конструктор копирования и деструктор A не должны бросать исключения.

8 Постусловия: !*this, если выполнено одно из следующих действий:

  • F является указателем функции NULL.
  • F является указателем NULL для члена.
  • F - это экземпляр шаблона класса функций, а !f

9 В противном случае *this задает копию F, инициализированную с помощью std::move(f). [оставить здесь примечание]

10 Броски: не должны бросать исключения, если F является указателем на функцию или reference_wrapper<T> для некоторого T. В противном случае, может bad_alloc или любое исключение, созданное F s копированием или перемещением конструктора.

Теперь, создавая или пытаясь построить (для разрешения перегрузки) a std::function<void(int)> из [](){} (т.е. с сигнатурой void(void)) нарушает требования конструктора std::function<void(int)>.

[res.on.required]/1

Нарушение предусловий, указанных в функциях. Требуется: абзац приводит к поведению undefined, если только функции Throws: paragraph указывает на исключение исключения при условии, что предварительное условие нарушено.

Итак, AFAIK, даже результат разрешения перегрузки undefined. Поэтому в этом аспекте выполняются обе версии g++/libstdС++.


В С++ 14 это было изменено, см. LWG 2132. Теперь для SFINAE-шаблона конструктора преобразования std::function требуется отказ от несовместимых Callables (подробнее о SFINAE в следующей главе):

template<class F> function(F f);
template <class F, class A> function(allocator_arg_t, const A& a, F f);

7 Требуется: F должно быть CopyConstructible.

8 Примечания: Эти конструкторы не должны участвовать в перегрузке если F является вызываемым (20.9.11.2) для типов аргументов ArgTypes... и тип возврата R.

[...]

"Не участвует в разрешении перегрузки" соответствует отказу через SFINAE. Чистый эффект заключается в том, что если у вас есть набор функций перегрузки foo,

void foo(std::function<void(double)>);
void foo(std::function<void(char const*)>);

и выражение вызова, такое как

foo([](std::string){}) // (C)

то вторая перегрузка foo выбирается однозначно: поскольку std::function<F> определяет F как свой внешний интерфейс, F определяет, какие типы аргументов передаются в std::function. Затем объект завершенной функции должен вызываться с этими аргументами (типами аргументов). Если a double передается в std::function, он не может быть передан функции, принимающей std::string, потому что нет преобразования doublestd::string. Для первой перегрузки foo аргумент [](std::string){} поэтому не считается Callable для std::function<void(double)>. Шаблон конструктора деактивируется, поэтому нет жизнеспособного преобразования от [](std::string){} до std::function<void(double)>. Эта первая перегрузка удаляется из набора перегрузки для разрешения вызова (C), оставляя только вторую перегрузку.

Обратите внимание на небольшое изменение в формулировке выше, из-за LWG 2420: Исключено, что если тип возврата R из std::function<R(ArgTypes...)> является void, тогда любой возвращаемый тип принимается (и отбрасывается) для Callable в шаблоне конструктора, упомянутом выше. Например, как []() -> void {}, так и []() -> bool {} являются вызываемыми для std::function<void()>. Поэтому следующая ситуация создает двусмысленность:

void foo(std::function<void()>);
void foo(std::function<bool()>);

foo([]() -> bool {}); // ambiguous

Правила разрешения перегрузки не пытаются ранжироваться среди разных пользовательских преобразований, и поэтому обе перегрузки foo жизнеспособны (прежде всего), и ни одна из них не лучше.


Как может помочь SFINAE?

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

#include <type_traits>
#include <iostream>

template<class T>
auto foo(T) -> typename std::enable_if< std::is_integral<T>::value >::type
{  std::cout << "foo 1\n";  }

template<class T>
auto foo(T) -> typename std::enable_if< not std::is_integral<T>::value >::type
{  std::cout << "foo 2\n";  }

int main()
{
    foo(42);
    foo(42.);
}

Аналогично, преобразование может быть сделано нежизнеспособным, используя SFINAE в конструкторе преобразования:

#include <type_traits>
#include <iostream>

struct foo
{
    template<class T, class =
             typename std::enable_if< std::is_integral<T>::value >::type >
    foo(T)
    {  std::cout << "foo(T)\n";  }
};

struct bar
{
    template<class T, class =
             typename std::enable_if< not std::is_integral<T>::value >::type >
    bar(T)
    {  std::cout << "bar(T)\n";  }
};

struct kitty
{
    kitty(foo) {}
    kitty(bar) {}
};

int main()
{
    kitty cat(42);
    kitty tac(42.);
}

Ответ 2

Это полностью допустимо. Так как С++ 11 лямбда-выражения (и ваш обертка std::function) создают объекты функции. Большая часть функциональных объектов состоит в том, что даже когда они являются общими, они остаются первоклассными объектами. В отличие от обычных шаблонов функций, они могут быть переданы и возвращены из функций.

Вы можете создавать избыточные наборы операторов явно с наследованием и использовать объявления. Следующее использование Mathias Gaunard демонстрирует "перегруженные лямбда-выражения".

template <class F1, class F2>
struct overload_set : F1, F2
{
    overload_set(F1 x1, F2 x2) : F1(x1), F2(x2) {}
    using F1::operator();
    using F2::operator();
};

template <class F1, class F2>
overload_set<F1,F2> overload(F1 x1, F2 x2)
{
    return overload_set<F1,F2>(x1,x2);
}

auto f = overload(
    [](){return 1;}, 
    [](int x){return x+1;}
);

int x = f();
int y = f(2);

источник

РЕДАКТИРОВАТЬ: Возможно, станет понятнее, если в приведенном примере вы замените

F1 -> std::function<void()> 
F2 -> std::function<void(int)>

и см. его компиляцию в gcc4.7

Шаблонное решение было предоставлено только для того, чтобы продемонстрировать, что концепция масштабируется до общего кода и может быть исключена.

В вашем случае при использовании более старого компилятора, такого как gcc 4.7, вы можете помочь с помощью явного приведения и gcc будет работать, как вы можете видеть в этом живом примере

На всякий случай, когда вам интересно, это не сработает, если вы начнете наоборот (попробуйте преобразовать лямбду, берущую int в std:: function без аргументов и т.д.)