从 lambda 捕获组中无休止地调用复制构造函数

Copy constructor called endlessly from lambda capture group

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

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<int> 和 returns 另一个 DynamicValue<int> 其中 returns 它的值加 1:

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

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

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

您可以尝试一个实例 here

经过一些测试,我认为问题出在复制构造函数上,它被无休止地调用,但我不确定如何解决它。我怎样才能避免这种行为?

此代码的一些重要部分:

  • lambda 按值(复制)捕获 DynamicValue 对象。
  • lambda 用于将 std::variant 初始化为 std::function 替代项。
  • DynamicValue 没有明确的移动构造函数,因此使用可调用对象的模板作为移动构造函数。

有问题的代码路径从请求从 lambda 构造 DynamicValue 对象开始。这会调用模板构造函数,它会尝试将 lambda 复制到 variantstd::function 替代项中。到目前为止,一切都很好。复制 (不动) lambda 毫无问题地复制捕获的对象。

不过,这个程序在CopyConstructible named requirement is satisfied. Part of this named requirement is being MoveConstructible时有效。为了使 lambda 满足 MoveConstructible,它的所有捕获都必须满足该命名要求。 DynamicValue 是这样吗?当您的标准库尝试 移动 lambda(因此也是捕获的对象)并以复制作为后备时会发生什么?虽然 DynamicValue 没有明确的移动构造函数,但它是可调用的...

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

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


解决方案是让 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<T> 排除在模板参数之外,而不排除 DynamicValue<U>。这可能是另一个需要考虑的问题。

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