Подтвердить что ты не робот

Как перегрузить конструкторы подписи функции std::?

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

Пример:

class Foo {
  Foo( std::function<void()> fn ) {
    ...code...
  }
  Foo( std::function<int()> fn ) {
    ...code...
  }
};

Foo a( []() -> void { return; } );    // Calls first constructor
Foo b( []() -> int { return 1; } );   // Calls second constructor

Это не будет компилироваться, жалуясь, что оба конструктора по существу идентичны и неоднозначны. Это, конечно, нонсенс. Я пробовал enable_if, is_same и множество других вещей. Признание указателей функций не может быть и речи, потому что это предотвратит прохождение состояния lambdas. Неужели должен быть способ достичь этого?

Мои шаблонные навыки немного не хватает, я боюсь. Обычные шаблонные классы и функции достаточно просты, но играть глупых педерастов с компилятором немного из моей лиги. Может кто-нибудь помочь мне, пожалуйста?

Я знаю, что варианты этого вопроса заданы раньше, но они в основном сосредоточены на нормальных функциях, а не на конструкторах; или перегрузка аргументами вместо возвращаемых типов.

4b9b3361

Ответ 1

Вот несколько распространенных сценариев, и почему я не думаю, что для них подходит std::function:

struct event_queue {
    using event = std::function<void()>;
    std::vector<event> events;

    void add(event e)
    { events.emplace_back(std::move(e)); }
};

В этой простой ситуации сохраняются функторы конкретной сигнатуры. В этом свете моя рекомендация кажется довольно плохим, не так ли? Что может пойти не так? Такие вещи, как queue.add([foo, &bar] { foo.(bar, baz); }), работают хорошо, и стирание типа - это именно тот объект, который вы хотите, так как предположительно функторы гетерогенных типов будут сохранены, поэтому его затраты не являются проблемой. На самом деле это одна из ситуаций, когда допустимо использование std::function<void()> в сигнатуре add. Но читайте дальше!

В какой-то момент в будущем вы поймете, что некоторые события могут использовать некоторую информацию, когда они возвращаются, поэтому вы пытаетесь:

struct event_queue {
    using event = std::function<void()>;
    // context_type is the useful information we can optionally
    // provide to events
    using rich_event = std::function<void(context_type)>;
    std::vector<event> events;
    std::vector<rich_event> rich_events;

    void add(event e) { events.emplace_back(std::move(e)); }
    void add(rich_event e) { rich_events.emplace_back(std::move(e)); }
};

Проблема с этим заключается в том, что для С++ 14 - только в С++ 11 гарантируется, что что-то простое, как queue.add([] {}), компилятору разрешено отклонять код. (В последнее время достаточно libstdС++ и libС++ - это две реализации, которые уже соответствуют С++ 14 в этом отношении.) Такие вещи, как event_queue::event e = [] {}; queue.add(e);, все еще работают! Так что, возможно, это хорошо использовать, если вы кодируете С++ 14.

Однако даже с С++ 14 эта функция std::function<Sig> может не всегда делать то, что вы хотите. Рассмотрим следующее, что сейчас недействительно и будет также в С++ 14:

void f(std::function<int()>);
void f(std::function<void()>);

// Boom
f([] { return 4; });

По уважительной причине тоже: std::function<void()> f = [] { return 4; }; не является ошибкой и отлично работает. Возвращаемое значение игнорируется и забывается.

Иногда std::function используется в тандеме с вычитанием шаблона, как показано в этом вопросе и что один, Это, как правило, добавляет дополнительный слой боли и трудностей.


Проще говоря, std::function<Sig> специально не обрабатывается в стандартной библиотеке. Он остается определяемым пользователем типом (в том смысле, что он, в отличие от, например, int), который следует нормальным правилам разрешения перегрузки, преобразования и вычитания шаблонов. Эти правила действительно сложны и взаимодействуют друг с другом - это не сервис для пользователя интерфейса, который им нужно помнить, чтобы передать ему вызывающий объект. std::function<Sig> имеет этот трагический аллюр, где он выглядит так, что помогает сделать интерфейс кратким и читаемым, но это действительно справедливо только в том случае, если вы не перегружаете такой интерфейс.

У меня лично есть множество признаков, которые могут проверить, является ли тип вызываемым в соответствии с сигнатурой или нет. В сочетании с выразительные EnableIf или Requires предложения Я все же могу поддерживать приемлемо читаемый интерфейс. В свою очередь, в сочетании с некоторыми ранжированными перегрузками я могу предположить, что логика "вызовет эту перегрузку, если функтор дает что-то, конвертируемое в int при вызове без аргументов или возврата к этой перегрузке в противном случае". Это может выглядеть так:

class Foo {
public:
    // assuming is_callable<F, int()> is a subset of
    // is_callable<F, void()>
    template<typename Functor,
             Requires<is_callable<Functor, void()>>...>
    Foo(Functor f)
        : Foo(std::move(f), select_overload {})
    {}

private:
    // assuming choice<0> is preferred over choice<1> by
    // overload resolution

    template<typename Functor,
             EnableIf<is_callable<Functor, int()>>...>
    Foo(Functor f, choice<0>);
    template<typename Functor,
             EnableIf<is_callable<Functor, void()>>...>
    Foo(Functor f, choice<1>);
};

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

Ответ 2

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

Во-первых, вы можете распаковать сигнатуры прошедших типов, просмотрев T::operator() и/или проверить, имеет ли он тип R (*)(Args...).

Затем проверьте эквивалентность подписи.

Второй подход заключается в обнаружении совместимости вызовов. Напишите класс признаков следующим образом:

template<typename Signature, typename F>
struct call_compatible;

который является либо std::true_type, либо std::false_type в зависимости от того, если decltype<F&>()( declval<Args>()... ) конвертируется в возвращаемое значение Signature. В этом случае это решит вашу проблему.

Теперь необходимо выполнить больше работы, если две подписи, которые вы перегружаете, совместимы. Т.е. предположим, что у вас есть std::function<void(double)> и std::function<void(int)> - они совместимы с перекрестными вызовами.

Чтобы определить, какой из них "лучший", вы можете просмотреть здесь по моему предыдущему вопросу, где мы можем взять кучу подписей и найти, какие лучше всего подходит. Затем выполните проверку типа возврата. Это становится сложным!

Если мы используем решение call_compatible, то вы делаете это:

template<size_t>
struct unique_type { enum class type {}; };
template<bool b, size_t n=0>
using EnableIf = typename std::enable_if<b, typename unique_type<n>::type>::type;

class Foo {
  template<typename Lambda, EnableIf<
    call_compatible<void(), Lambda>::value
    , 0
  >...>
  Foo( Lambda&& f ) {
    std::function<void()> fn = f;
    // code
  }
  template<typename Lambda, EnableIf<
    call_compatible<int(), Lambda>::value
    , 1
  >...>
  Foo( Lambda&& f ) {
    std::function<int()> fn = f;
    // code
  }
};

который является общей моделью для других решений.

Вот первый удар в call_compatible:

template<typename Sig, typename F, typename=void>
struct call_compatible:std::false_type {};

template<typename R, typename...Args, typename F>
struct call_compatible<R(Args...), F, typename std::enable_if<
  std::is_convertible<
    decltype(
      std::declval<F&>()( std::declval<Args>()... )
    )
    , R
  >::value
>::type>:std::true_type {};

который еще не проверен/не скомпилирован.

Ответ 3

Итак, у меня есть совершенно новое решение этой проблемы, которое работает в MSVC 2013 и не сосать (например, смотреть указатель на operator()).

Отправка тега.

Тег, который может переносить любой тип:

template<class T> struct tag{using type=T;};

Функция, которая принимает выражение типа и генерирует тег типа:

template<class CallExpr>
tag<typename std::result_of<CallExpr>::type>
result_of_f( tag<CallExpr>={} ) { return {}; }

и используйте:

class Foo {
private:
  Foo( std::function<void()> fn, tag<void> ) {
    ...code...
  }
  Foo( std::function<int()> fn, tag<int> ) {
    ...code...
  }
public:
  template<class F>
  Foo( F&& f ):Foo( std::forward<F>(f), result_of_f<F&()>() ) {}
};

и теперь Foo(something) пересылается в конструктор std::function<int()> или std::function<void()> в зависимости от контекста.

Мы можем сделать tag<> умнее, поддерживая преобразование, если хотите, добавив к нему ctor. Затем функция, возвращающая double, отправит на tag<int>:

template<class T> struct tag{
  using type=T;
  tag(tag const&)=default;
  tag()=default;
  template<class U,
    class=typename std::enable_if<std::is_convertible<U,T>::value>::type
  >
  tag( tag<U> ){}
};

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

Хотя это не работает напрямую в VS2012, вы можете перенаправить на функцию инициализации в тело конструктора.