模板化转换运算符优先级

raf*_*foo 6 c++ c++17

一些 C++ hacks 使用转换运算符来获取有关构造函数的一些信息。我想知道,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;
}
Run Code Online (Sandbox Code Playgroud)

此代码打印此:

def
int
Run Code Online (Sandbox Code Playgroud)

而不是这个:

int
int
Run Code Online (Sandbox Code Playgroud)

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

生成文件:

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

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

$(EXE): main.cpp
    $(CXX) $(CXXFLAGS) -o $@ $<
Run Code Online (Sandbox Code Playgroud)

平台:

def
int
Run Code Online (Sandbox Code Playgroud)

Dav*_*ing 2

首先,一些编译器/版本,包括当前的 GCC,确实拒绝了这种书面形式。

\n

复制初始化

\n

让我们从一个更简单的情况开始:

\n
template<typename T>\nvoid f(A = T()) {}\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

请注意,转换函数模板的“预期”返回类型肯定有助于模板参数推导(因为否则将不可能为它们推导任何内容!)。此外,即使约束条件允许,也T永远不会被推导出为或如此;A&&尽管某些其他类型(例如派生类)允许用于(非模板)转换函数(为此不需要为每个允许的类型花费精力),但仅使用单数的“明显”类型进行此类推导。

\n

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

\n
\n

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

\n
\n

列表初始化

\n

但是,多重转换规则通常不适用于列表初始化(因为您执行正常的重载解析,允许在每层大括号内进行转换)。因此,A会考虑从参数到每个构造函数的参数类型的(用户定义)转换。

\n

B

\n

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

\n

C<A>

\n

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

\n