为什么C 标准要求编译器忽略对基本类型转换操作员的呼叫



采用以下代码:

#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的检查。这是因为DogAnimal&Dog*Animal*是保证标准派生到基础转换的标准。没有替代。

但是想象一下我是否可以写:

struct Dog : Animal {
    operator Animal&() { ... };
};

现在,这两个电话可以做完全不同的事情!我的Dog*Animal*转换仍然是同一只狗,但是我的DogAnimal&转换是完全不同的Dog。它甚至可能是Cat。这将使所有这些代码基本上都无法推论。

您将需要一种特殊的机制来绝对为您提供特定类型的基本子对象:

by_ref(std::base<Animal>(dog));

基本上必须使用来保证依赖继承的任何代码的正确性。这是很多代码。


对什么好处?如果您想要不同的基本子对象,则可以写一个不同的命名函数:

struct Dog : Animal {
    Animal& foo();
};

命名可能是关于编程的两件事之一,但是要提出自己的名字比打开一袋可悲的袋子更好,这将允许人们写自己的派生到基础,但不太明确的转换。

C 的设计师已决定应适用此一般原则(stroustrup, C 编程语言,第18章)

只有在没有呼叫的情况下(即仅使用内置的转换)无法解决呼叫时,才考虑用户定义的转换。

对于好是坏的,从派生到基础上有内置的转换,因此从未考虑过用户定义的运算符。

标准通常遵循STROUSTRUP设计的路径,除非有很好的理由不这样做。

stroustrup引用了总体类型转换设计(同上)的以下基本原理:

转换规则既不是最简单的,也不是最简单的记录,也不是最通用的。但是,它们更安全,而由此产生的决议通常不如替代方案令人惊讶。手动解决一个容易 模棱两可的比找到由未被预见的转换引起的错误。

最新更新