【问题标题】:Templated conversion operator priority模板化转换运算符优先级
【发布时间】:2021-01-01 14:50:34
【问题描述】:

一些 C++ hack 使用转换运算符来获取有关构造函数的一些信息。 我想知道,在模板化强制转换运算符的解析中为T 选择具体类型的过程是什么。

#include <iostream>
#include <type_traits>

using std::cout;
using std::endl;

struct A {
    A(int) { cout << "int" << endl; }
    A() { cout << "def" << endl; }
    A(const A&) { cout << "copy" << endl; }
    A(A&&) { cout << "move" << endl; }
};

struct B {
    template<typename T> operator T()
        { return {}; }
};

template<typename Except>
struct C {
    template<typename T,
             std::enable_if_t<!std::is_same_v<T, Except>>* = nullptr> operator T()
        { return {}; }
};

template<typename T>
void f(A a = { T() }) {}

int main() {
    f<B>();
    f<C<A>>();

    return 0;
}

这段代码打印这个:

def
int

不是这个:

int
int

为什么要禁用转换以获取我想要的构造函数(int 版本)? C++ 标准说返回类型不参与寻找有效的模板重载,那么为什么它选择这个版本而不抱怨多种可能的解决方案呢?

生成文件:

EXE = C++Tuple
CXX = g++
CXXFLAGS = -std=c++17

run: $(EXE)
    ./$(EXE)
.PHONY: run

$(EXE): main.cpp
    $(CXX) $(CXXFLAGS) -o $@ $<

平台:

$ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/7/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 7.5.0-3ubuntu1~18.04' --with-bugurl=file:///usr/share/doc/gcc-7/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++ --prefix=/usr --with-gcc-major-version-only --program-suffix=-7 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04)
$ uname -r
4.4.0-19041-Microsoft

【问题讨论】:

  • 有趣的是,GCC 和 MSVC 拒绝编译这个(你似乎使用 Clang)。
  • 它是在 WSL 上使用 gcc 在 C++17 上编译的。 gcc -vgcc version 7.5.0。问题是当在这种特定情况下有多个候选者时,为强制转换运算符找到有效重载时的全局优先级是什么,那么为什么在第一种情况下调用 A() 而不是 A(int)
  • 我添加了Makefile以获取详细信息,但验证后似乎仍然可以编译...

标签: c++ c++17


【解决方案1】:

首先,包括当前的 GCC 在内的几个编译器/版本确实拒绝这样做。

复制初始化

让我们从一个更简单的案例开始:

template<typename T>
void f(A = T()) {}

在这里,我们有一个T 类型的表达式,我们想要(隐式)转换为A。不出所料,B 生成 operator A 作为其转换函数模板的特化,而 C 由于 SFINAE 而没有生成任何内容。

请注意,转换函数模板的“预期”返回类型肯定有助于模板参数推导(否则就不可能为它们推导任何东西!)。此外,T 永远不会被推断为 A&amp;&amp; 左右,即使约束条件允许这样做;即使某些其他类型(例如,派生类)被允许用于(非模板)转换函数(无需努力每个允许类型的费用)。

Clang 在这里产生一个令人困惑的错误消息,说它在尝试调用 A(int) 构造函数时无法将 C&lt;A&gt; 转换为 int;通常情况下,这种转换当然是可能的,但在这种情况下,[over.best.ics.general]/4 中关于 多个 用户定义转换的常规规则是不允许的:

但是,如果目标是构造函数的第一个参数[...],并且构造函数或用户定义的转换函数是[...]的候选者em>、[over.match.copy] 或 [...] 用户定义的转换序列不予考虑。

列表初始化

但是,多次转换规则通常不适用于列表初始化(因为您执行正常的重载决议,允许转换,inside each layer 大括号)。因此,考虑了A 的每个构造函数从参数到参数类型的(用户定义的)转换。

B

当前版本的 GCC、ICC 和 MSVC 都因歧义而拒绝这一点,因为 B 可以转换为 intA。 (ICC 有用地指出,由于 [over.ics.rank]/3.2.3,移动构造函数比复制构造函数更匹配,但仍然有两种选择。)很难猜测为什么 Clang 忽略了前一种可能性(没有诊断输出),但其他编译器似乎是正确:[dcl.init.list]/3.7 遵循正常的重载决议(除了更喜欢std::initializer_list 构造函数,它们是'在这里不相关),并且没有理由偏爱一个构造函数而不是另一个构造函数(因为在每种情况下都涉及用户定义的转换序列,然后是完全匹配的标准转换序列)。

C&lt;A&gt;

同样,const A&amp;A&amp;&amp; 构造函数的推导选择 T=A(这次是因为 [temp.deduct.conv] 的简化而不是 [over.match.copy]' s 限制)并且什么也没找到。因此只有“转换”C&lt;A&gt;intA 有效。所有四个编译器都同意这种情况,尽管 MSVC 错误地发出关于“非法”双重转换的警告。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-04-07
    • 2017-07-04
    • 2012-05-04
    • 2016-02-19
    • 2016-06-07
    • 2010-11-25
    • 2011-07-07
    相关资源
    最近更新 更多