【问题标题】:Shouldn't this code throw an ambiguous conversion error?这段代码不应该抛出一个模棱两可的转换错误吗?
【发布时间】:2016-05-27 07:33:17
【问题描述】:

我有两个类,AB,每个类都定义了到 B 的转换。 A 有一个到B 的转换运算符,B 有一个来自A 的构造函数。打电话给static_cast<B> 不应该是模棱两可的吗?该代码使用 g++ 编译并选择转换构造函数。

#include<iostream>

using namespace std;

struct B;
struct A {
    A(const int& n) : x(n) {}
    operator B() const;         //this const doesn't change the output of this code
    int x;
};

struct B{
    B(const double& n) : x(n) {}
    B(const A& a);
    double x;
};

A::operator B() const           //this const doesn't change the output of this code
{
    cout << "called A's conversion operator" << endl;
    return B(double(x));
}

B::B(const A& a)
{
    cout << "called B's conversion constructor" << endl;
    x = (double) a.x;
}

int main() {
    A a(10);
    static_cast<B>(a);            // prints B's conversion constructor
}

【问题讨论】:

  • 有趣。我更新了代码来测试这个答案。这个答案似乎暗示应该调用转换运算符,而这段代码调用转换构造函数。
  • 我认为你是对的。这些问题非常相似,所以它们看起来像骗子,但事实并非如此。

标签: c++ casting


【解决方案1】:

对于用户定义的转换序列;转换构造函数和转换运算符之间似乎没有优先级,它们都是候选者;

§13.3.3.1.2/1 用户定义的转换序列

用户定义的转换序列由初始标准转换序列和用户定义的转换 (12.3) 和第二个标准转换序列组成。如果用户定义的转换由构造函数 (12.3.1) 指定,则初始标准转换序列会将源类型转换为构造函数参数所需的类型。如果用户定义的转换由转换函数(12.3.2)指定,则初始标准转换序列将源类型转换为转换函数的隐式对象参数。

因此如果转换是;

B b2 = a; // ambiguous?

可能不明确,编译失败。 Clang 编译失败,g++ 接受代码并使用构造函数; demo code,VS 也接受代码。 VS 和 g++ 调用转换构造函数(根据 OP 代码)。

考虑到贴出的代码,需要考虑用户定义的转换序列(通过构造函数和转换运算符)和static_cast的使用。

§5.2.9/4 静态投射

如果声明 T t(e); 格式正确,则表达式 e 可以使用 static_cast&lt;T&gt;(e) 形式的 static_cast 显式转换为类型 T,对于某些发明的临时变量 t (8.5)。这种显式转换的效果与执行声明和初始化然后使用临时变量作为转换结果的效果相同。当且仅当初始化将其用作左值时,表达式e 才用作左值。

根据上面的引用,static_cast 等同于B temp(a);,因此使用了直接初始化序列。

§13.3.1.3/1 构造函数初始化

当类类型的对象被直接初始化 (8.5)、从相同或派生类类型的表达式复制初始化 (8.5) 或默认初始化 (8.5) 时,重载决议选择构造函数。对于直接初始化或默认初始化,候选函数是被初始化对象类的所有构造函数。对于复制初始化,候选函数是该类的所有转换构造函数(12.3.1)。参数列表是初始化器的表达式列表或赋值表达式。

一般情况下(不包括标记为explicitconst 的任何构造函数和运算符),鉴于B(const A&amp; a); 构造函数和从A 构造B,构造函数应该获胜,因为它提供考虑best viable function 时的完全匹配;因为不需要进一步的implicit conversions(第 13.3 节;重载解决方案)。


如果构造函数B(const A&amp; a); 被删除,转换(使用static_cast&lt;&gt; 仍然会成功,因为用户定义的转换运算符是一个候选并且它的使用是不模糊的。

§13.3.1.4/1 通过用户定义的转换复制初始化类

在 8.5 中指定的条件下,作为类类型对象的复制初始化的一部分,可以调用用户定义的转换来将初始化表达式转换为正在初始化的对象的类型。

引用来自 C++ 标准的 N4567 草案。


在对象的构造之外调用用户定义的转换序列也很有指导意义,即调用方法。

鉴于代码清单(和上面的规则);

#include <iostream>
using namespace std;
struct A;
struct B {
    B() {}
    B(const A&) { cout << "called B's conversion constructor" << endl; }
};
struct A {
    A() {}
    operator B() const { cout << "called A's conversion operator" << endl; return B(); }
};
void func(B) {}
int main() {
    A a;
    B b1 = static_cast<B>(a); // 1. cast
    B b2 = a; // 2. copy initialise
    B b3 ( a ); // 3. direct initialise
    func(a); // 4. user defined conversion
}

Clang、g++ (demo) 和 VS 提供不同的结果,因此可能有不同的合规水平。

  • clang 失败 2. 和 4.
  • g++ 接受 1. 到 4.
  • VS 失败 4.

根据上述规则,1. 到 3. 都应该成功,因为 B 转换构造函数是候选的,不需要进一步的用户转换;直接构造和复制初始化用于这些形式。阅读标准(上面的摘录,特别是 §13.3.3.1.2/1 和 §13.3.1.4/1,然后是 §8.5/17.6.2),2. 和 4. 可能/应该失败并且模棱两可 -因为正在考虑转换构造函数和转换运算符,没有明确的顺序。

我相信这很可能是一个意想不到的用例(类型能够以这种方式相互转换;有一个论点认为哪里会有一个转换序列是一般用例)。

【讨论】:

  • 我认为 2 和 4 应该失败,因为它们是 copy-initialization (但不是来自同一类型的表达式 - 因此由 [over.match.copy] , 而不是 [over.match.ctor]) 并且有两个用户定义的序列。 (两个用户定义的序列总是模棱两可的,除非它是相同的用户定义的转换结合不同排名的标准转换)
  • @MM 可能,但 [over.match.ctor] 明确表示“对于复制初始化,候选函数都是该类的所有转换构造函数(12.3.1)”我认为这两种解释都可以被应用。
  • [over.match.ctor] 说“从相同或派生类类型的表达式复制初始化”。 (该段的其余部分仅适用于第一句选择的情况)
  • @M.M 我明白你在说什么。我也进一步阅读了 [dcl.init]/17.6.2(第 8.5/17.6 和 17.6.2 节),一起阅读(8.5 和 13.3)对我来说更有意义。我也需要将其折叠到答案中。正如你所说,它确实使 2. 和 4. 模棱两可。
猜你喜欢
  • 1970-01-01
  • 2018-01-20
  • 1970-01-01
  • 1970-01-01
  • 2021-07-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-03-08
相关资源
最近更新 更多