2015-07-06 3 views
4

Может ли общий лямбда использовать «Замена не является ошибкой» Правило? ПримерИспользование SFINAE с общими lambdas

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> 
    { 
     // ... 
    }; 

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

+0

Ваша лямбда кажется [отлично работает] (http: // ideone. com/rFOkmA) как написано? –

+0

@KerrekSB ОК, это потрясающе, следует ли перефразировать вопрос, чтобы сосредоточиться на бит SFINAE? –

+6

Я понятия не имею, какую проблему вам нужно решить ... –

ответ

7

Lambdas являются объектами функции под капотом. Generic lambdas являются объектами функции с шаблоном 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. В библиотеке промышленного качества я мог бы сделать наследование двоичным, а не линейным (чтобы ограничить глубину экземпляров шаблонов и глубину дерева наследования).

В стороне, есть четыре причины, по которым я знаю, чтобы SFINAE включал лямбды.

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

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

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

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

+0

Теперь я помню публикацию о создании наборов перегрузок для лямбда-функций (http://stackoverflow.com/a/22147375/2567683) (одинаково плохо получилось). Извините за путаницу, я должен был сформулировать свой вопрос лучше с самого начала, это хорошая тема, и это очень жалко, что это будет упущено –

2

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

Однако на самом деле вам не нужно различать разные типы возвращаемых данных. Если 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.

Если вам нужно что-то более сложное, то это один из тех случаев, когда вы должны prefer a functor over a lambda.


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

+0

Лямбда с включенной SFINAE может быть протестирована против (могу я назвать ее некоторыми вещами или нет?), Поэтому она весьма полезна. В качестве примера я могу использовать расширенную лямбду SFINAE и передать ее функции ['curry (F)'] (http://stackoverflow.com/q/26655685/1774667) и вызвать ее, когда я пройду достаточно параметры. Или я мог собрать кучу лямбда, объединить их в один объект и использовать SFINAE, чтобы определить, на какой из них я должен отправить вызов (набор перегрузок лямбда). SFINAE lamdas имеют * ранний отказ *, который полезен даже вне разрешения перегрузки. – Yakk

+0

@Yakk Да, это хороший момент. – Barry

+0

... Откуда вы знаете, какую проблему пытается решить OP?Вы можете понять вопрос за пределами названия? Как это возможно? – Yakk

2

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