Можно ли исключить лямбда-тип?

Хотя хорошей практикой является использование только исключений типов, полученных из std::exception, C++ позволяет выбросить что-либо. Все приведенные ниже примеры действительны C++:

throw "foo";  // throws an instance of const char*
throw 5;      // throws an instance of int

struct {} anon;
throw anon;   // throws an instance of not-named structure

throw []{};   // throws a lambda!

Последний пример интересен, поскольку он потенциально позволяет передавать некоторый код для выполнения на сайте catch без необходимости определять отдельный класс или функцию.

Но возможно ли вообще поймать лямбду (или закрытие)? catch ([]{} e) не работает.

Ответы

Ответ 1

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

Каждое выражение лямбда вводит тип замыкания, который уникален для окружающей области. Таким образом, ваша наивная попытка не может работать, поскольку []{} имеет совершенно другой тип в выражении throw и обработчике!

Но вы правы. C++ позволяет вам бросить любой объект. Поэтому, если вы явно конвертируете лямбда перед рукой в тип, соответствующий обработчику исключений, это позволит вам вызывать это произвольное вызываемое. Например:

try {
    throw std::function<void()>{ []{} }; // Note the explicit conversion
} catch(std::function<void()> const& f) {
    f();
}

Это может иметь интересную утилиту, но я бы предостерег от бросания вещей, не полученных из std::exception. Лучшим вариантом, вероятно, будет создание типа, который выводится из std::exception и может содержать вызываемый.

Ответ 2

C++ позволяет бросать что угодно. И это позволяет вам ловить все, что вы бросаете. Можно, конечно, кинуть лямбду. Единственная проблема в том, что для того, чтобы поймать что-то, вам нужно знать тип или хотя бы родительский тип этого чего-либо. Так как лямбды не происходят из общей базы, вы должны знать тип вашей лямбды, чтобы поймать лямбду. Основная проблема заключается в том, что каждое лямбда-выражение даст вам значение определенного типа. Это означает, что и ваш бросок, и ваш улов должны основываться на одном и том же лямбда-выражении (примечание: одно и то же выражение, а не просто какое-то выражение, которое выглядит одинаково). Один из способов сделать эту работу в некоторой степени - заключить в себе создание лямбда-выражения для добавления функции. Таким образом, вы можете вызвать функцию в вашем выражении throw и использовать возвращаемый тип функции, чтобы определить тип для catch:

#include <utility>

auto makeMyLambda(int some_arg)
{
    return [some_arg](int another_arg){ return some_arg + another_arg; };
}

void f()
{
    throw makeMyLambda(42);
}

int main()
{
    try
    {
        f();
    }
    catch (const decltype(makeMyLambda(std::declval<int>()))& l)
    {
        return l(23);
    }
}

Попробуйте это auto makeMyLambda(int some_arg) {%0A++++return [some_arg](int another_arg){ return some_arg + another_arg%3B+}; } void f() {%0A++++throw makeMyLambda(42); } int main() {%0A++++try%0A++++{%0A++++ f();%0A++++}%0A++++Catch+(const decltype(makeMyLambda(std::declval()))& l)%0A++++{%0A++++ return l(23);%0A++++} } '),l:'5',n:'0',o:'C++ source #1',t:'0')),k:62.5,l:'4',m:65.46681664791902,n:'0',o:'',s:0,t:'0'),(g:!((h:output,i:(compiler:1,editor:1,wrap:'1'),l:'5',n:'0',o:'#1 with x86-64+Clang+(trunk)',t:'0')),header:(),l:'4',m:34.53318335208099,n:'0',o:'',s:0,t:'0')),k:62.5,l:'3',n:'0',o:'',t:'0'),(g:!((h:compiler,i:(compiler:clang_trunk,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',trim:'1'),lang:c++,libs:!(),options:'-O3 -std=c++17',source:1),l:'5',n:'0',o:'x86-64+Clang+(trunk) (Editor+#1,+Compiler+#1)+C++',t:'0')),k:37.5,l:'4',m:100,n:'0',o:'',s:0,t:'0')),l:'2',n:'0',o:'',t:'0')),version:4 rel=noreferrer>здесь.

Вы также можете просто использовать std::function как предложено в некоторых других ответах, что потенциально является более практичным подходом. Недостатки этого, однако, будут

  • Это означает, что вы на самом деле не выбрасываете лямбду. Вы бросаете функцию std::function, которая не совсем то, что вы просили 😉
  • Создание объекта std::function из лямбды может вызвать исключение

Ответ 3

Вы можете бросить и поймать std::function:

#include <iostream>
#include <functional>

void f() {
        throw std::function<void(void)>([]{std::cout << "lambda\n"; });
}

int main()
{
        try{ f(); }
        catch( std::function<void(void)> &e)
        {
                e();
                std::cout << "catch\n";
        }
}

Выход:

lambda
catch

Ответ 4

Конечно, можно поймать лямбду. Помните, что лямбда без асята преобразуется в указатель функции, поэтому что-то вроде:

try {
    throw []{};
} 
catch (void(*f)()) {
    f();
}

В противном случае std::function. Например:

int b;
try {
    throw [b, n = 23]{};
} 
catch (const std::function<void()>& f) {
    f();
}

Ответ 5

Лямбда - это уникальный анонимный тип. Единственный способ назвать тип лямбда-экземпляра - сохранить его в переменной, а затем выполнить decltype для этого типа переменной.

Есть несколько способов поймать брошенную лямбду.

try  {
  throw []{};
} catch(...) {
}

в этом случае вы не можете использовать его, кроме как бросить его снова.

try  {
  throw +[]{};
} catch(void(*f)()) {
}

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

try  {
  throw std::function<void()>([]{});
} catch(std::function<void()> f) {
}

Вы можете преобразовать его в std::function. Недостатком std::function является то, что он выделяет кучу для больших лямбд, которые теоретически могут вызвать его выброс.

Мы можем устранить это выделение кучи:

template<class Sig>
struct callable;

template<class R, class...Args>
struct callable<R(Args...)> {
  void* state = nullptr;
  R(*action)(void*, Args&&...) = nullptr;
  R operator()(Args...args) const {
    return action( state, std::forward<Args>(args)... );
  }
};

template<class Sig, class F>
struct lambda_wrapper;
template<class R, class...Args, class F>
struct lambda_wrapper<R(Args...), F>
:
  F,
  callable<R(Args...)>
{
  lambda_wrapper( F fin ):
    F(std::move(fin)),
    callable<R(Args...)>{
      static_cast<F*>(this),
      [](void* self, Args&&...args)->R {
        return static_cast<R>( (*static_cast<F*>(self))( std::forward<Args>(args)... ) );
      }
    }
  {}
  lambda_wrapper(lambda_wrapper && o):
    F(static_cast<F&&>(o)),
    callable<R(Args...)>( o )
  {
    this->state = static_cast<F*>(this);
  }
  lambda_wrapper& operator=(lambda_wrapper && o)
  {
    static_cast<F&>(*this) = (static_cast<F&&>(o));
    static_cast<callable<R(Args...)>&>(*this) = static_cast<callable<R(Args...)>&>( o );
    this->state = static_cast<F*>(this);
  }
};

template<class Sig, class F>
lambda_wrapper<Sig, F> wrap_lambda( F fin ) {
  return std::move(fin);
}

Теперь вы можете сделать:

try {
  throw wrap_lambda<void()>([]{});
} catch( callable<void()> const& f ) {
}

callable - это стирание типа с "меньшим весом", чем std::function как это не может привести к выделению новой памяти кучи.

Живой пример.