【问题标题】:Copy constructor called endlessly from lambda capture group从 lambda 捕获组无休止地调用复制构造函数
【发布时间】:2021-01-21 16:26:59
【问题描述】:

我编写了以下类来创建任何类型的值,这些值在每次使用调用运算符时都是固定的或重新计算的:

template <typename T>
class DynamicValue {
    private:
    std::variant<T, std::function<T()>> getter;
    
    public:
    DynamicValue(const T& constant) : getter(constant){};
    template <typename F, typename = std::enable_if_t<std::is_invocable_v<F>>>
    DynamicValue(F&& function) : getter(function) {}
    DynamicValue(const T* pointer) : DynamicValue([pointer]() { return *pointer; }) {}
    DynamicValue(const DynamicValue& value) : getter(value.getter) {}
    DynamicValue(DynamicValue& value) : DynamicValue((const DynamicValue&) value) {}
    ~DynamicValue() {}
    T operator()() const { return getter.index() == 0 ? std::get<T>(getter) : std::get<std::function<T()>>(getter)(); }
};

我还写了这个函数,它接受一个DynamicValue&lt;int&gt; 并返回另一个DynamicValue&lt;int&gt;,它返回它的值加1:

DynamicValue<int> plus1(DynamicValue<int> a) {
    return [a] { return a() + 1; };
}

但是,当我尝试使用它时,程序崩溃了:

DynamicValue<int> a = 1;
DynamicValue<int> b = plus1(a);

您可以尝试一个现场示例here

经过一些测试,我认为问题在于复制构造函数,它被无休止地调用,但我不知道如何解决它。如何避免这种行为?

【问题讨论】:

  • 您的意思是写DynamicValue&lt;int&gt; plus1(DynamicValue&lt;int&gt;&amp; a) {?我们能否将这个问题作为由错字引起的而结束?
  • DynamicValue(DynamicValue&amp; value) 你不需要这个重载。惯用的 const 就足够了。实际上,您不需要任何复制 ctor。遵循零规则。
  • @πάνταῥεῖ 我不是故意写这个的,我试过了也没什么区别。
  • @bolov 我添加了这个重载作为我遇到的另一个错误的临时修复。无论哪种方式,在这种情况下,删除复制 ctor 都没有任何区别。
  • “在这种情况下删除复制 ctor 没有任何影响。” -- 请继续沿着这条路走下去。删除所有不需要重现错误的代码,c.f. minimal reproducible example.

标签: c++ lambda copy-constructor


【解决方案1】:

这段代码的一些重要部分:

  • lambda 按值(副本)捕获DynamicValue 对象。
  • lambda 用于将std::variant 初始化为std::function 替代方案。
  • DynamicValue 没有显式移动构造函数,因此可调用对象的模板用作移动构造函数。

有问题的代码路径始于从 lambda 构造 DynamicValue 对象的请求。这会调用模板构造函数,该构造函数会尝试将 lambda 复制到 std::function 替代 variant 中。到现在为止还挺好。复制(不移动) lambda 复制捕获的对象没有问题。

但是,当满足 CopyConstructible 命名要求时,此过程有效。此命名要求的一部分是MoveConstructible。为了使 lambda 满足 MoveConstructible,它的所有捕获都必须满足该指定要求。 DynamicValue 是这种情况吗?当您的标准库尝试移动 lambda(因此也是捕获的对象)并将复制作为后备时会发生什么?虽然DynamicValue 没有显式移动构造函数,但它是可调用的……

FDynamicValue&lt;T&gt; 时,模板构造函数充当移动构造函数。它尝试通过将源DynamicValue(问题代码中a 的捕获副本)转换为std::function 来初始化variant。这是允许的,制作源的副本,并且该过程继续直到需要移动副本,此时再次调用移动构造函数。这一次,它尝试通过将源DynamicValue 的副本转换为std::function 来初始化variant。这是允许的,制作源副本的副本,并且该过程继续进行,直到需要移动副本的副本,此时再次调用移动构造函数。等等。

每个“移动构造函数”都尝试将DynamicValue 移动到新对象的variant,而不是将DynamicValue 移动到新对象中。这将在每次移动时增加另一层开销,除非递归调用在构造完成之前爆炸。


解决方案是让DynamicValue 移动可构造。至少有两种方法可以做到这一点。

1) 显式提供移动构造函数。

    DynamicValue(DynamicValue&& value) : getter(std::move(value.getter)) {}

2) 排除 DynamicValue 作为模板构造函数的模板参数。

    template <typename F, typename = std::enable_if_t<std::is_invocable_v<F>>,
                          typename = std::enable_if_t<!std::is_same_v<std::decay_t<F>, DynamicValue>>>
    DynamicValue(F&& function) : getter(function) {}

请注意,当U 不是T 时,这仅将DynamicValue&lt;T&gt; 排除在模板参数之外,而不是DynamicValue&lt;U&gt;。这可能是另一个需要考虑的问题。

您可能想看看这是否也解决了导致您定义第二个复制构造函数的任何问题。这可能是一种无法解决这个潜在问题的创可贴方法。

【讨论】:

  • 作为 C++ 的初学者,非常感谢您的详细解释。这是一个 uni 项目的一部分,我决定将其复杂化以获得额外的积分并在此过程中学习更高级的 C++。随着截止日期的临近,这个决定已经让我很痛苦了:)你不知道你救了我的头痛!
猜你喜欢
  • 2017-04-18
  • 2016-03-05
  • 1970-01-01
  • 2013-10-14
  • 2017-02-08
  • 2019-08-07
  • 1970-01-01
  • 2013-06-23
  • 1970-01-01
相关资源
最近更新 更多