为什么 C++ 认为我的类是可复制构造的,而实际上它无法被复制构造?

考虑以下场景:

template<typename T>
struct Base
{
    // 默认构造
    Base() = default;

    // 不可复制构造
    Base(Base const &) = delete;
};

template<typename T>
struct Derived : Base<T>
{
    Derived() = default;
    Derived(Derived const& d) : Base<T>(d) {}
};

// 這個斷言會通過?
static_assert(
    std::is_copy_constructible_v<Derived<int>>);

为什么这个断言通过了?显然,你无法复制一个 Derived<int>,因为这样做会尝试复制 Base<int>,而 Base<int> 不可复制。事实上,如果你尝试复制它,你会得到一个错误:

void example(Derived<int>& d)
{
    Derived<int> d2(d);
    // msvc: 错误 C2280: 'Base<T>::Base(const Base<T> &)':
    //       尝试引用已删除的函数
    // gcc: 错误: 使用已删除的函数 'Base<T>::Base(const Base<T>&)
    //       [with T = int]'
    // clang: 错误: 调用已删除的构造函数 'Base<int>'
}

好的,编译器认为 Derived<int> 是可复制构造的,但当我们尝试进行复制构造时,却发现它实际上不可复制!问题在于,编译器通过检查类是否具有未被删除的复制构造函数来确定可复制构造性。而对于 Derived<T> 而言,它确实具有未被删除的复制构造函数。你自己声明了它!

    Derived(Derived const& d) : Base<T>(d) {}

所以,确实存在复制构造函数。它无法被实例化,但编译器并不关心。它根据你提供的信息进行判断,而你告诉它可以进行复制。毕竟,另一个可能的复制构造函数应该是

    Derived(Derived const& d) : Base<T>() {}

而这个构造函数可以成功实例化。复制一个 Derived 时,会默认构造 Base 基类,而不是复制构造它。假设我们将定义移出行外。

template<typename T>
struct Derived : Base<T>
{
    Derived() = default;
    Derived(Derived const& d);
};

对于“这个类是否可复制构造?”这个问题,答案应该是什么?你不知道定义是什么,只知道它的声明。编译器是否应该停止编译并显示错误信息“无法预测未来”?但是,如果你不想在头文件中暴露复制构造函数的实现呢?判断可复制构造的规则是是否存在未被删除的复制构造函数。对于Derived而言,它确实存在。它可能无法实例化,但这并非is_copy_constructible所关注的点。¹ 由于不可复制性默认继承,我们只需让复制构造函数默认即可:

template<typename T>
struct Derived : Base<T>
{
    Derived() = default;
    Derived(Derived const& d) = default;
};

如果任何基类不可复制构造,则隐式定义或显式默认的复制构造函数会被定义为删除,此时声明会被视为已指定 = delete。该 = delete 可被 is_copy_constructible 检测并导致断言失败。但如果你显式定义了一个未被删除的自定义复制构造函数,编译器会假设你会履行承诺。相关阅读为什么 std::is_copy_constructible 报告一个仅支持移动的对象向量是可复制构造的

¹ 要求类型既完整又所有成员都已定义是不合理的要求,因为这需要所有类方法的定义都出现在头文件中。你的整个程序已被简化为一个仅头文件的项目。

本文文字及图片出自 Why does C++ think my class is copy-constructible when it can’t be copy-constructed?

你也许感兴趣的:

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注