采用以下代码:
#include <iostream>
struct Base {
char x = 'b';
};
struct Derived : Base {
operator Base() { return Base { 'a' }; }
};
int main() {
Derived derived;
auto base = static_cast<Base>(derived);
std::cout << "BASE -> " << base.x << std::endl;
}
在G 和Clang 下,这会产生:
BASE -> b
我期待以下内容:
BASE -> a
为什么?因为为什么要阅读此代码,我看到Derived
内部的转换操作员返回包含'a'
的Base
实例。
clang 给我发出警告的礼貌:
main.cpp:9:5: warning: conversion function converting 'Derived' to its base class 'Base' will never be used
operator Base() { return Base { 'a' }; }
研究此警告,我发现这是通过设计(释义为清晰):
:class.conv.fct
转换函数的类型([dcl.fct])是"函数不使用参数返回转换型-ID"。转换函数从不用于将(可能是CV符号)对象[...]转换为该类型(或对其引用的参考)的(可能是CV的)基类[...]。
因此,似乎两个编译器都在这里做正确的事情。我的问题是,为什么标准需要此行为?
如果您可以在C 中覆盖转换为基础类,那么您可能会破坏很多东西。例如,您如何才能访问类的实际基类实例?您可能需要一些baseof
模板,类似于std::addressof
,这些模板用于绕过IL构想的operator&
超载。
允许这将引起有关代码含义的混乱。有了此规则,很明显,将类转换为基类的基础类实例(在所有情况下)。
让我们有一个 Animal
s的层次结构,我想编写一个函数,该函数在不切片的情况下使用可修改的Animal
。我该怎么做?我有两个选择:
void by_ref(Animal& );
void by_ptr(Animal* );
我可以称为:
Dog dog = ...;
by_ref(dog);
by_ptr(&dog);
今天,这两个调用之间的唯一区别将是两个函数内部使用的语法,可能是针对nullptr
的检查。这是因为Dog
至Animal&
和Dog*
到Animal*
是保证标准派生到基础转换的标准。没有替代。
但是想象一下我是否可以写:
struct Dog : Animal {
operator Animal&() { ... };
};
现在,这两个电话可以做完全不同的事情!我的Dog*
到Animal*
转换仍然是同一只狗,但是我的Dog
到Animal&
转换是完全不同的Dog
。它甚至可能是Cat
。这将使所有这些代码基本上都无法推论。
您将需要一种特殊的机制来绝对为您提供特定类型的基本子对象:
by_ref(std::base<Animal>(dog));
基本上必须使用来保证依赖继承的任何代码的正确性。这是很多代码。
对什么好处?如果您想要不同的基本子对象,则可以写一个不同的命名函数:
struct Dog : Animal {
Animal& foo();
};
命名可能是关于编程的两件事之一,但是要提出自己的名字比打开一袋可悲的袋子更好,这将允许人们写自己的派生到基础,但不太明确的转换。
C 的设计师已决定应适用此一般原则(stroustrup, C 编程语言,第18章)
只有在没有呼叫的情况下(即仅使用内置的转换)无法解决呼叫时,才考虑用户定义的转换。
对于好是坏的,从派生到基础上有内置的转换,因此从未考虑过用户定义的运算符。
标准通常遵循STROUSTRUP设计的路径,除非有很好的理由不这样做。
stroustrup引用了总体类型转换设计(同上)的以下基本原理:
转换规则既不是最简单的,也不是最简单的记录,也不是最通用的。但是,它们更安全,而由此产生的决议通常不如替代方案令人惊讶。手动解决一个容易 模棱两可的比找到由未被预见的转换引起的错误。