继承:将派生类作为基类引用传递给期望基类引用的函数

4

考虑以下示例代码:

#include <iostream>

class base {
    public:
    base() {
        std::cout << "base constructed" << std::endl;
    }
    base(const base & source) {
        std::cout << "base copy-constructed" << std::endl;
    }
};

class derived : public base {
    public:
    derived() {
        std::cout << "derived constructed" << std::endl;
    }
    derived(const derived &) = delete;
    derived(const base & source) : base(source) {
        std::cout << "derived copy-constructed from base" << std::endl;
    }
};

int main() {
    derived a;
    base b(a);
    derived c(a);

    return 0;
}

为什么调用base::base(const base &)的方法是可以的,但是调用derived::derived(const base &)的方法却不行?两者都需要一个基类引用,但都得到了一个派生类引用。据我所知,派生类“是一个”基类。
当编译器使用派生类对象的引用时,为什么会坚持使用derived::derived(const derived &)而不是base::base(const base &)呢?

1
编译器在哪里抱怨(以及确切的消息是什么)?编辑我得到的错误prog.cpp:18:5: error: deleted function 'derived::derived(const derived&)' prog.cpp:27:16: error: used here - Cornstalks
显然,“删除”默认的某个东西并没有真正删除它的效果。一些可怕的、恐怖的遗物,曾经自豪的复制构造函数留下来,会在某个时候弹出来,用阴森的声音告诉你:“我已经死了!” - Omnifarious
3个回答

2
derived a; 
derived c(a);

您已经显式删除了拷贝构造函数。这意味着上面的第二行将无法编译通过。与base情况的不同之处在于,基本情况下没有声明以derived&为参数的base构造函数,因此将应用转换。

但对于derived的情况,确实存在这样一个函数,它被声明并删除了。重载解析器将找到两个函数derived(derived const &)derived(base const &),因为两个都被声明了,并选择第一个作为最佳匹配项。然后它将发现该函数被删除,并出现错误提示。

如果您想使用另一个带有derived对象的构造函数,则必须显式进行类型转换:

derived c( static_cast<base&>(a) );

在这种情况下,最佳的重载变成了 derived( base const& ),代码将会编译通过。

2
显然,“删除”默认事物之一并没有实际删除它的效果。曾经引以为傲的默认拷贝构造函数的一些可怕的遗留物仍然存在,会以低沉的声音弹出来告诉你:“我已经死了!”。
对于我来说,这并不是一个完全令人惊讶的事实,尽管在您提出这个问题之前我并不知道具体情况。我无法引用标准的相关部分(我相信相关部分没有提到鬼怪,尽管它应该有)。我也相当确定,如果你按照某些难以理解的故事跟进某些可怕的情况,就会发现这种情况发生的原因是非常合理的。
不幸的是,如果有比转换为基类更好的匹配项存在,那么将使用它。例如,在这段代码中:
#include <iostream>

class A {
};

class B {
 public:
   void foo(const A &) { ::std::cerr << "B::foo(const A &) called!\n"; }
   void foo(const B &) { ::std::cerr << "B::foo(const B &) called!\n"; }
};

int main()
{
    B b;
    A &ar = b;
    b.foo(b);
    b.foo(ar);
};

这样做会产生以下输出结果:
B::foo(const B &) called!
B::foo(const A &) called!

正如您所期望的那样。编译器不会将这种情况视为模棱两可。如果将void foo(const B&)更改为privateprotected成员,则编译器仍会优先匹配它,并告诉您尝试访问某些具有访问限定符的内容,而这些限定符指示您无法访问。

将某些内容设置为“已删除”只是将其声明为具有比private更受限制的特殊访问限定符。


1
关于第一个段落:在表达式derived(derived const&) = deleted;中,有两个部分:声明:derived(derived const&)和定义= deleted。虽然这可能不明显,但重要的区别是,deleted函数仍将参与重载决议(只考虑声明),但如果/当它被选择为最佳候选时,编译器会失败并告诉您它已经deleted。整个8.4.3节明确指出这是一个definition - David Rodríguez - dribeas
@DavidRodríguez-dribeas:所以,我把“= deleted”这个东西的定义描述成一种访问修饰符,比“private”还要更加专有,这样的表述并不完全不准确,是吗?通常情况下,我并不会经常阅读标准文档,因为我发现它们用的谨慎语言往往掩盖了底层模式,这些模式构成了逻辑的基础,解释了为什么某些事情会是它们的样子。 - Omnifarious
2
嗯,我不会这样称呼它,但在某种程度上你是对的,它是一个标签,告诉编译器在使用时抱怨。我不愿意与访问说明符进行比较的原因是,访问说明符选择可以访问成员的代码子集,但在这种情况下没有子集(除了空集)。 - David Rodríguez - dribeas

0
我要说这是因为 derived c(a); 的签名恰好与你删除的函数的签名完全匹配。C++ 会尽可能避免强制转换/提升/转换,而更喜欢最接近的匹配。将 a 强制转换为正确的类型(base)应该可以解决问题,使其停止匹配已删除函数的签名并开始匹配另一个构造函数的签名。

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