Использование 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>
    {
        // ...
    };

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

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Вы можете перегрузить функцию на несколько разных сигнатур обратного вызова.

Во-вторых, вышеупомянутый трюк.

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

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

Использование 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 все еще может быть достаточно удобен, чтобы проверить, является ли ваша функция вызываемым периодом, что не является проблемой, которую вы пытаетесь решить, поэтому в этом случае все еще не очень полезно.

Обыкновенная лямбда может иметь только одно тело, поэтому 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(); 
    };
Другие вопросы по тегам