繁体   English   中英

编译器通过用户定义的右值引用转换以不同方式处理重载决议

[英]Compilers work differently for overload resolution with user-defined conversion to rvalue reference

当 gcc 和 clang select 不同的重载构造函数从用户定义的转换运算符隐式转换时,我遇到了一个奇怪的行为。

有关代码在这里:

#include <cstdio>
#include <utility>
#include <type_traits>

template <typename T>
class foo {
  T& t_;
public:
  foo(T& t) : t_(t) {}
  operator T() const & { return t_; }
  operator T&&() && { return std::move(t_); }
};

class bar {
  int val_;
public:
  bar(int v) : val_(v) {}
  bar(const bar& b) : val_(b.val_) { printf("copy constructed\n"); }
  bar& operator=(const bar& b)     { printf("copy assigned\n"); val_ = b.val_; return *this; }
  bar(bar&& mv) : val_(mv.val_)    { printf("move constructed\n"); mv.val_ = -1; }
  bar& operator=(bar&& mv)         { printf("move assigned\n"); val_ = mv.val_; mv.val_ = -1; return *this; }
};

int main() {
  bar v(1);
  foo<bar> f(v);
  bar v2(std::move(f));
}

class foo是类型T的包装器,如果它是右值引用(当由std::move转换时),则应隐式转换为TT&&

但是,一些编译器更喜欢T而不是T&& ,即使它是右值引用。 结果看起来像:

⟩ clang++-15 -std=c++17 test.cpp && ./a.out
copy constructed

⟩ clang++-15 -std=c++14 test.cpp && ./a.out
move constructed

⟩ g++ -std=c++14 test.cpp && ./a.out
move constructed

⟩ g++ -std=c++17 test.cpp && ./a.out
move constructed

仅对于 clang 和-std=c++17或更高版本,复制构造函数优于移动构造函数。

编译器版本:

⟩ g++ --version
g++ (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

⟩ clang++-15 --version
Ubuntu clang version 15.0.4-++20221102053308+5c68a1cb1231-1~exp1~20221102053355.92
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin

为什么不同编译器和 C++ 版本的优先级不同? 还是我违反了一些规则?

bar v2(std::move(f));

顶级重载决议在bar的复制构造函数和移动构造函数之间。 bar确实有第三个构造函数,它采用int ,但显然这不是选定的构造函数,所以我将忽略它。

无论选择foo<bar>的哪个转换 function,该转换 function 的结果都将是一个右值,因此bar的移动构造函数将是首选。 话虽如此,我也将忽略复制构造函数而不深入研究标准。 (似乎移动构造函数的使用并不是这里争议的一部分。尽管 OP 的程序打印“copy constructed”,但此复制构造是由operator T完成的;复制构造函数不是在v2本身上调用的构造函数。 ) 问题是将使用什么隐式转换序列将类型foo<bar>的右值转换为参数类型bar&& 由于问题被标记为 [c++17],因此所有引用都将指向 C++17 标准。

根据 [over.best.ics]/1,隐式转换序列“受 object 的初始化规则或单个表达式引用的规则约束”。 因此,我们必须参考 [dcl.init.ref],它管理引用的初始化。 正在初始化的引用具有类型bar&& ,并且初始化是来自类型foo<bar>的右值的复制初始化。 达到的情况在该部分的 p5.2.1 中:

如果初始化表达式

  • [不适用的情况被删除],或
  • 具有 class 类型(即T2是 class 类型),其中T1T2不相关,并且可以转换为右值或 function 类型“ cv3 T3 ”的左值,其中“ cv1 T1 ”是引用兼容的带有“ cv3 T3 ”(见 16.3.1.6),

然后 [...] 转换的结果 [...] 称为转换后的初始值设定项。 如果转换后的初始值设定项是纯右值,则将其类型T4调整为类型“ cv1 T4 ” (7.5) 并应用临时物化转换 (7.4)。 在任何情况下,引用都绑定到生成的 glvalue(或适当的基 class 子对象)。

我们知道初始化器可以转换为bar类型的 xvalue 或 prvalue,并且bar与自身引用兼容,因此将执行转换并将引用绑定到该转换的结果。 唯一的问题是转换是如何完成的:通过operator bar还是operator bar&&

要回答这个问题,我们必须查看参考部分 16.3.1.6,也称为 [over.match.ref]:

在 11.6.3 中指定的条件下,引用可以直接绑定到 glvalue 或 class prvalue,这是将转换 function 应用于初始化表达式的结果。 重载解析用于将select转换为function来调用。 假设“reference to cv1 T ”为被初始化引用的类型,“ cv S ”为初始化表达式的类型, S为class类型,候选函数选择如下:

  • 考虑了S及其基类的转换函数。 那些未隐藏在S中的非显式转换函数和产生类型“对cv2 T2的左值引用”(当初始化对函数的左值引用或右值引用时)或“ cv2 T2 ”或“对cv2 T2的右值引用”(当初始化函数的右值引用或左值引用),其中“ cv1 T ”与“ cv2 T2 ”引用兼容(11.6.3),是候选函数。 对于直接初始化,[...]

参数列表有一个参数,即初始化表达式。 [注意:此参数将与转换函数的隐式 object 参数进行比较。 ——尾注]

这里, T是 cv 非限定引用类型,即bar 有一种转换 function 会产生barT2 ,其中T2bar ),还有一种会产生bar&&“对T2的右值引用”,其中T2bar )。 由于bar在这两种情况下都与T2 (即bar本身)引用兼容,因此两个转换函数都是候选函数。 必须执行过载解析以确定调用哪一个。 这里,参数仍然是std::move(f) ,即foo<bar>类型的右值,但参数是隐含的 object 参数:

  • 对于operator T ,隐含的 object 参数具有类型foo<bar> const &因为运算符是用const &声明的。
  • 对于operator T&& ,隐含的 object 参数具有类型foo<bar>&&因为运算符是用&&声明的。

为了执行重载决议,我们考虑各自的隐式转换序列,然后尝试确定一个是否比另一个更好。 两者都是身份转换,因为foo<bar>的右值可以直接绑定到foo<bar> const &foo<bar>&& (参见 [over.ics.ref]/1)。 但正如我们所知,将右值引用绑定到右值比将左值引用绑定到右值要好。 这是规则 [over.ics.rank]/3.2.3:

标准转换序列S1是比标准转换序列S2更好的转换序列,如果

  • [...] 或者,如果不是那样,
  • [...] 或者,如果不是那样,
  • S1S2是引用绑定 (11.6.3) 并且都没有引用在没有ref-qualifier的情况下声明的非静态成员 function 的隐式 object 参数, S1将右值引用绑定到右值, S2绑定左值引用,或者,如果不是那样,[...]

(这里的附带条件适用;两个函数都有ref-qualifiers )。

由于隐式 object 参数的隐式转换序列在operator T&&的情况下比operator T符 T 更好,因此前者是最可行的 function。

GCC 是正确的。 应该调用operator T&& ,然后调用移动构造函数。 Clang 在 C++17 模式下调用operator T (它在内部执行复制构造)。 没有调用移动构造函数,因为 Clang 已经省略了它。

这是我们的线索,Clang 的行为可能与核心问题 2327有关。 我认为 Clang 维护者可能已经乐观地实施了一些建议的解决方案来解决这个问题(尽管 Richard Smith 似乎没有提供详细的措辞)。 如果问题以与您所看到的行为一致的方式得到解决,并且委员会批准它作为针对 C++17 的缺陷报告,则 Clang 的行为将被认为是正确的。 另一方面,如果它以与 GCC 行为一致的方式解决,则 Clang 可能必须更改。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM