c++exceptionlambdamove-semanticsclang-tidy

Warning "bugprone-exception-escape" when captured object throws in copy constructor


When I have a C++ lambda that captures an object by value, and that object can throw an exception in its copy constructor, clang-tidy will show a warning:

warning: an exception may be thrown in function '(lambda at :15:23)' which should not throw exceptions [bugprone-exception-escape]

I don't understand why this warning appears.

Example code:

#include <functional>

class MyClass {
   public:
    MyClass() {}
    MyClass(const MyClass& rhs) { throw std::exception(); }
    void do_stuff() const {}
};

void inner_func(std::function<void()>&& the_func) {}

void outer_func() {
    MyClass test_obj;

    auto the_lambda = [=]() { test_obj.do_stuff(); };
    inner_func(std::move(the_lambda));
}

(on godbolt.org: https://godbolt.org/z/67xW6Yenx)

The clang-tidy warning is shown for the line where the_lambda is created.

I don't understand why the lambda (or creating the lambda) must not throw exceptions. In the example, outer_func() does not care whether an exception happens. inner_func() also should not care about this.

Also, apparently the lambda needs to be std::move()d into another function for the warning to appear.

So it seems that merely creating the lambda is a problem already. But why is that the case? And how can I catch this exception that is warned about, or how else can I avoid the warning?

On Godbolt I tried this with clang-tidy 16 and 19, with same results.


Solution

  • The warning is triggered because the closure type of the_lambda has a throwing move constructor. And, as others pointed out, one of the purposes of this check is to report throwing move constructors.

    Why is the move constructor of the_lambda throwing? Because it internally calls the copy constructor of MyClass, which can throw. Note that the move constructor of a closure type is defaulted [expr.prim.lambda.closure/14/2], which makes it defined here as throwing.

    A simple demo of the same problem without lambdas:

    struct MyClass {
      MyClass() {}
      MyClass(const MyClass& rhs) { throw 1; }
    };
    
    struct Lambda {
      MyClass myclass;
    
      Lambda(MyClass param) : myclass(param) { }
      Lambda(const Lambda&) = default;
      Lambda(Lambda&&) = default;  // this calls throwing copy constructor of `MyClass`
    };
    
    void outer_func() {
      MyClass obj;
      Lambda l1(obj);
      Lambda l2(std::move(l1));  // this calls throwing move constructor of 'Lambda'
    }
    

    Godbolt link: https://godbolt.org/z/hze55P9Gb