Использование SFINAE с общими лямбда-выражениями

Могут ли универсальные лямбда-выражения воспользоваться правилом "Ошибка замены не является ошибкой"? Пример

auto gL = 
    [](auto&& func, auto&& param1, auto&&... params) 
        -> enable_if_t< is_integral<
            std::decay_t<decltype(param1)>
        >::value>
    {
        // ...
    };

auto gL =  
     [](auto&& func, auto&& param1, auto&&... params) 
        -> enable_if_t< !is_integral<
            std::decay_t<decltype(param1)>
        >::value>
    {
        // ...
    };

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


person Nikos Athanasiou    schedule 06.07.2015    source источник
comment
Кажется, ваша лямбда работает просто отлично, как написано?   -  person Kerrek SB    schedule 06.07.2015
comment
@KerrekSB ОК, это здорово, я должен перефразировать вопрос, чтобы сосредоточиться на бите SFINAE?   -  person Nikos Athanasiou    schedule 06.07.2015
comment
Я понятия не имею, какую проблему вам нужно решить...   -  person Kerrek SB    schedule 06.07.2015
comment
Не должно быть никаких проблем, если вы просто возвращаете результат вызова функции, даже если функция возвращает void. Проблема возникает, когда вам нужно временно сохранить возвращаемое значение, сделать что-то еще, а затем вернуть его (или ничего не возвращать в случае типа возврата void). В этом случае вы, вероятно, можете обойти это, создав оболочку RAII, которая выполняет часть do something else в своем деструкторе. Или отправка тегов другим лямбда-выражениям/функциям на основе result_of.   -  person Praetorian    schedule 06.07.2015
comment
@Praetorian Я как раз писал именно об этом. Изменил пример. Вопрос касается использования SFINAE с общими лямбда-выражениями. Можно ли это сделать, будет ли это сделано, почему нельзя, не следует ли? ...   -  person Nikos Athanasiou    schedule 06.07.2015
comment
@RyanHaining Какое отношение тип возвращаемого значения имеет к бенчмаркингу?   -  person Nikos Athanasiou    schedule 06.07.2015
comment
Timer t; return f(args...); и Timer::~Timer печатает результат.   -  person Kerrek SB    schedule 06.07.2015
comment
Я неправильно понял комментарии, я прочитал это как func, возвращающее некоторое timer, которое назначено k   -  person Ryan Haining    schedule 06.07.2015
comment
вы хотите SFINAE без перегрузки?   -  person Ryan Haining    schedule 06.07.2015
comment
sfinae работает только тогда, когда есть лучшая перегрузка, которую можно выбрать, когда первая ошибка. Поскольку вы не можете перегружать лямбда-выражения, неясно, чего достигнет sfinae.   -  person Dani    schedule 06.07.2015
comment
@Дани, это идеальная причина (на которую также указал Райан). Я был захвачен тем, что всё это объект шаблонной функции под капотом, потому что я забыл о перегрузке лямбда-выражений. Я не понимаю, почему бы вам не опубликовать это как ответ   -  person Nikos Athanasiou    schedule 06.07.2015
comment
Вы можете использовать выражение SFINAE в завершающем типе возвращаемого значения, здесь измененная версия примера Керрека, который отклоняет возвращаемый тип void. Чего вы не можете сделать, так это предоставить дополнительную версию, которая работает только для возвращаемого типа void.   -  person Praetorian    schedule 06.07.2015
comment
@Yakk Я прошу не согласиться. Добавление bolg придает акцент (греч. έμφασις = εν + φαινομαι = заставить что-то появиться). Пожалуйста, перечитайте вопрос. Это не проблема реализации, а вопрос о языке и функции (SFINAE для общих лямбда-выражений).   -  person Nikos Athanasiou    schedule 06.07.2015
comment
Намного лучше! Комментарии удалены, голосование отменено.   -  person Yakk - Adam Nevraumont    schedule 06.07.2015


Ответы (3)


Лямбды — это функциональные объекты под капотом. Общие лямбда-выражения — это функциональные объекты с шаблоном operator()s.

template<class...Fs>
struct funcs_t{};

template<class F0, class...Fs>
struct funcs_t<F0, Fs...>: F0, funcs_t<Fs...> {
  funcs_t(F0 f0, Fs... fs):
    F0(std::move(f0)),
    funcs_t<Fs...>(std::move(fs)...)
  {}
  using F0::operator();
  using funcs_t<Fs...>::operator();
};
template<class F>
struct funcs_t<F>:F {
  funcs_t(F f):F(std::move(f)){};
  using F::operator();
};
template<class...Fs>
funcs_t< std::decay_t<Fs>... > funcs(Fs&&...fs) {
  return {std::forward<Fs>(fs)...};
}

auto f_all = funcs( f1, f2 ) генерирует объект, который является перегрузкой как f1, так и f2.

auto g_integral = 
  [](auto&& func, auto&& param1, auto&&... params) 
    -> std::enable_if_t< std::is_integral<
        std::decay_t<decltype(param1)>
    >{}>
  {
    // ...
  };

auto g_not_integral =  
 [](auto&& func, auto&& param1, auto&&... params) 
    -> std::enable_if_t< !std::is_integral<
        std::decay_t<decltype(param1)>
    >{}>
{
    // ...
};

auto gL = funcs( g_not_integral, g_integral );

и вызов gL выполнит разрешение перегрузки SFINAE для двух лямбда-выражений.

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


Кроме того, я знаю 4 причины, по которым SFINAE включает лямбда-выражения.

Во-первых, с новым std::function вы можете перегружать функцию несколькими разными сигнатурами обратного вызова.

Во-вторых, описанный выше трюк.

В-третьих, каррирование объекта функции, где он оценивает, имеет ли он правильное количество и тип аргументов.

Далее, автоматическая распаковка кортежей и тому подобное. Если я использую стиль передачи продолжения, я могу спросить у переданного в продолжении, примет ли он распакованный кортеж или будущий разобранный и т. д.

person Yakk - Adam Nevraumont    schedule 06.07.2015
comment
Теперь я помню сообщение о создании наборов перегрузок для лямбда-функций (одинаково плохой прием). Извините за путаницу, я должен был лучше сформулировать свой вопрос с самого начала, это хорошая тема, и жаль, что ее не заметят. - person Nikos Athanasiou; 07.07.2015

Общая лямбда может иметь только одно тело, поэтому SFINAE здесь не очень полезен.

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

auto gL = 
    [](auto&& func, auto&&... params)
    {
        // start a timer
        using Ret = decltype(std::forward<decltype(func)>(func)(
            std::forward<decltype(params)>(params)...));
        std::packaged_task<Ret()> task{[&]{
            return std::forward<decltype(func)>(func)(
                std::forward<decltype(params)>(params)...); }};
        auto fut = task.get_future();
        task();
        // stop timer and print elapsed time
        return fut.get(); 
    };

Если вы хотите избежать накладных расходов на packaged_task и future, легко написать собственную версию:

template<class T>
struct Result
{
    template<class F, class... A> Result(F&& f, A&&... args)
        : t{std::forward<F>(f)(std::forward<A>(args)...)} {}
    T t;
    T&& get() { return std::move(t); }
};
template<>
struct Result<void>
{
    template<class F, class... A> Result(F&& f, A&&... args)
        { std::forward<F>(f)(std::forward<A>(args)...); }
    void get() {}
};

auto gL = 
    [](auto&& func, auto&&... params)
    {
        // start a timer
        using Ret = decltype(std::forward<decltype(func)>(func)(
            std::forward<decltype(params)>(params)...));
        Result<Ret> k{std::forward<decltype(func)>(func),
            std::forward<decltype(params)>(params)...};
        // stop timer and print elapsed time
        return k.get(); 
    };
person ecatmur    schedule 06.07.2015

Использование SFINAE заключается в удалении перегрузки или специализации из набора кандидатов при разрешении данной функции или шаблона. В вашем случае у нас есть лямбда - это функтор с одним operator(). Перегрузки нет, поэтому нет смысла использовать SFINAE1. Тот факт, что лямбда является универсальной, что делает ее operator() шаблоном функции, не меняет этого факта.

Однако на самом деле вам не нужно различать разные типы возвращаемых значений. Если func возвращает void для заданных аргументов, вы все равно можете return это сделать. Вы просто не можете назначить его временному. Но и этого делать не обязательно:

auto time_func = [](auto&& func, auto&&... params) {
    RaiiTimer t;
    return std::forward<decltype(func)>(func)(
        std::forward<decltype(params)>(params)...); 
};

Просто напишите RaiiTimer, конструктор которого запускает таймер, а деструктор останавливает его и печатает результат. Это будет работать независимо от типа возвращаемого значения func.

Если вам нужно что-то более сложное, то это один из тех случаев, когда вам следует предпочесть функтор лямбде.


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

person Barry    schedule 06.07.2015
comment
Лямбду с поддержкой SFINAE можно протестировать (могу ли я вызывать ее с чем-то или нет?), так что это весьма полезно. В качестве примера я могу взять расширенную лямбду SFINAE и передать ее функции curry(F), и она будет вызываться, когда у меня есть передано достаточно параметров. Или я мог бы собрать кучу лямбда-выражений, объединить их в один объект и использовать SFINAE, чтобы определить, на какой из них я должен отправить вызов (перегруженный набор лямбда-выражений). Ламды SFINAE имеют ранний отказ, что полезно даже за пределами разрешения перегрузки. - person Yakk - Adam Nevraumont; 06.07.2015
comment
... Откуда вы знаете, какую проблему пытается решить ОП? Вы можете понять вопрос, выходящий за рамки заголовка? Как это возможно? - person Yakk - Adam Nevraumont; 06.07.2015
comment
@Yakk Я думаю, он просто хочет рассчитать время любого func и заставить gL вернуть результат (или нет)? - person Barry; 06.07.2015