能否捕获Lambda类型的异常?

59

尽管仅抛出从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站点传递一些代码,而无需定义单独的类或函数。

但是捕获lambda(或闭包)是否可能呢?catch([]{} e)不能工作。

更新(2022/11/14):

请参见此处我自己的答案,考虑到C++20的特性。


我认为我们可以假设 catch(...){} 不是期望的。 - Joshua
15
catch (...) { asm("call %rax"); } 不安全的问题需要不安全的解决方案。 - sudo rm -rf slash
@Joshua 如果只是为了不让捕获的异常对象可访问,那么catch(...)并不理想。 - Krzysiek Karbowiak
原来有人对Lambda异常很好奇:https://dev59.com/6Z_ha4cB1Zd3GeqPwk5x - Krzysiek Karbowiak
1
@sudo rm -rf slash:我认为你没有尝试过。你把x64调用约定搞错了。 - Joshua
1
@Joshua 是的,我没有尝试过它。那更像是一个玩笑而不是一个严肃的想法。我也认为没有什么强制编译器使用rax。 - sudo rm -rf slash
5个回答

52

异常处理程序是基于类型匹配的,用于将异常对象与处理程序匹配的隐式转换比其他上下文中更加受限。

每个 lambda 表达式都会引入一个闭包类型,该类型唯一地属于其周围的作用域。因此,您的天真尝试不起作用,因为[]{}在 throw 表达式和处理程序中具有完全不同的类型

但您是正确的。C++允许您抛出任何对象。因此,如果您事先显式将 lambda 转换为与异常处理程序匹配的类型,则可以调用任意可调用对象。例如:

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

这可能具有有趣的实用性,但我建议不要抛出不派生自std :: exception的东西。一个更好的选择可能是创建一个从std :: exception派生并可以保存可调用对象的类型。


8
当然,我不会在生产代码中使用这个。相反,我正在探究语言的复杂性。虽然我尝试通过指向函数的指针来捕获异常,但我没有想到可以使用std::function 来抛出和捕获。 - Krzysiek Karbowiak
4
如果类型设计得当,我认为就可以在生产中实现它。正如我之前提到的那样,它可能具有有趣的效用。创新毕竟是将已知的东西以新颖的方式利用起来 :) - StoryTeller - Unslander Monica
2
可用于匹配catch表达式的确切规则在此答案中提供。不幸的是,如果将lambda作为throw []{};抛出,似乎没有办法捕获它,因为没有公共基类,并且该类型不是指针,因此指针规则不适用。 - Silvio Mayolo

25
C++允许抛出任何东西,也允许捕获你所抛出的一切。当然,你可以抛出lambda。唯一的问题是,要想捕获某些东西,你需要知道它的类型或至少是它的父类型。由于lambda不从共同的基类派生,因此你必须知道你的lambda的类型才能捕获一个lambda。这主要问题是每个lambda表达式会给你一个不同类型的rvalue。这意味着你的throw和catch需要基于相同的lambda表达式(注意:是相同的表达式,而不仅仅是看起来完全一样的某个表达式)。我能想到的一种使其在一定程度上工作的方式是将要抛出的lambda的创建封装在一个函数中。这样,你可以在你的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);
    }
}

这里试一试。

你也可以像其他答案中建议的那样,直接使用std::function,这可能是一种更实用的方法。然而,这样做的缺点是:
  • 这意味着你实际上没有抛出一个lambda表达式。你抛出了一个std::function,这不是你要求的。
  • 从lambda表达式创建std::function对象可能会抛出异常

8
您可以抛出和捕获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

2

Lambda是一种独特的匿名类型。唯一命名lambda实例类型的方法是将其存储在变量中,然后对该变量类型进行decltype

有几种方法可以捕获抛出的lambda表达式。

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

在这种情况下,您不能使用它,除非再次抛出它。
try  {
  throw +[]{};
} catch(void(*f)()) {
}

无状态的lambda可以转换为函数指针。

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

你可以将其转换为std :: function std :: function 的缺点是对于较大的lambda,它会进行堆分配,这理论上可能导致它抛出异常。
我们可以消除堆分配:
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更轻量级的类型擦除,因为它不会导致新的堆内存分配。

实时示例


1
@krzy,你漏了一个+;我从未暗示过。throw +[]{}; - Yakk - Adam Nevraumont

0

现在是2022年,当前的C++标准是C++20,它为我们提供了一些更有趣的机会。我在看到这个演讲之后意识到了这一点:C++ Lambda Idioms,特别是这部分

Lambda现在是默认可构造的,并且允许在未求值的上下文中使用(例如decltype),这使得我们可以这样做:

#include <iostream>

using lambda = decltype([]{ std::cout << "I am a lambda\n"; });

auto foo()
{
    throw lambda();
}

int main()
{
    try {
        foo();
    } catch (const lambda& l) {
        l();
    }
}

在编译器浏览器上试一试

所以目前看来,简短的答案是是的,可以捕获lambda类型的异常


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接