如何在模板化转换运算符中消除这种结构的歧义?

How to disambiguate this construction in a templated conversion operator?

在对为什么我的代码在 GCC 上给我一个歧义错误而在 Clang 上没有错误感到困惑之后,我简化了代码。可以在下面看到。

struct Foo
{
    // Foo(Foo&&) = delete;
    // Foo(const Foo&) = delete;
    Foo(int*) {}
};

struct Bar
{    
    template<typename T>
    operator T()
    {
        return Foo{nullptr};
    }
};

int main() { Foo f{Bar{}}; }

报错如下

main.cpp:17:18: error: call to constructor of 'Foo' is ambiguous
int main() { Foo f{Bar{}}; }
                 ^~~~~~~~
main.cpp:1:8: note: candidate is the implicit move constructor
struct Foo
       ^
main.cpp:1:8: note: candidate is the implicit copy constructor
main.cpp:5:1: note: candidate constructor
Foo(int*) {}
^

这次我无法成功编译 Clang,所以我想这只是一个 Clang 错误,这是预期的行为。

当我明确删除复制和移动构造函数(即取消注释前两行代码)时,我反而得到

note: candidate constructor has been explicitly deleted

但还是报错。那么,我将如何消除此处构造的歧义?

请注意,我特别添加了 Foo{nullptr} 而不是 nullptr,但没有区别。与显式标记 Foo 构造函数相同。这种歧义错误仅在 Bar 的转换运算符被模板化时发生。

我可以在转换运算符中添加一些 SFINAE,但我不确定要排除什么。例如,这将使它起作用:

template<typename T, std::enable_if_t<std::is_same<T, Foo>{}>* = nullptr>

这是我找到的另一个,这可能是我的答案:

template<typename T, std::enable_if_t<!std::is_same<T, int*>{}>* = nullptr> 

要解决歧义,请将 explicit 添加到转换运算符声明中:

struct Bar
{    
    template<typename T>
    explicit operator T()
    {
        return Foo{nullptr}; 
    }
};

为什么有必要?因为 Foo 有一个采用 int* 的构造函数,所以 operator T() templateoperator int*() 实例化被认为是重载的一部分f 初始化的分辨率。参见 [over.match.copy]:

1 [...] Assuming that cv1 T is the type of the object being initialized, with T a class type, the candidate functions are selected as follows:

  • (1.1) The converting constructors of T are candidate functions.

  • (1.2) When the type of the initializer expression is a class type “cv S”, the non-explicit conversion functions of S and its base classes are considered. When initializing a temporary object ([class.mem]) to be bound to the first parameter of a constructor where the parameter is of type “reference to possibly cv-qualified T” and the constructor is called with a single argument in the context of direct-initialization of an object of type “cv2 T”, explicit conversion functions are also considered.

从 (1.2) 可以看出,初始化只考虑隐式转换函数,因此存在歧义——因为编译器无法决定是使用对 Foo 的引用来构造 f 还是,如前所述,使用 operator int* 的 return 值中的 int*(通过 copy-initializing 获得)。 然而,初始化器表达式是一个临时对象时,我们也考虑显式 转换——但前提是它们匹配引用 Foo 的构造函数,我们的 "possibly cv-qualified T",即我们的 副本 移动构造函数 。整个行为与 [class.conv.fct¶2]:

一致

A conversion function may be explicit ([dcl.fct.spec]), in which case it is only considered as a user-defined conversion for direct-initialization ([dcl.init]). Otherwise, user-defined conversions are not restricted to use in assignments and initializations.

所以,第三次在这里说同样的话:如果它没有被标记为 explicit,没有什么可以阻止编译器尝试 copy-initialize 一个 int* 用于构造。

经过一些挖掘后我的最佳猜测:我在以下代码中遇到了同样的错误:

struct Foo { Foo(int*) {} };

struct Bar {    
   operator Foo(); // { return Foo{nullptr}; }
   /* explicit */ operator int*();
};

int main() { Foo f{Bar{}}; }

而且,当我取消注释注释代码时,问题就消失了。在我看来,在 OP 的原始模板化版本中,当需要从 BarFoo 的隐式转换时,GCC 仅 "instantiates" 转换运算符声明,然后在实例化它们的主体之前解析重载。

至于为什么explicit有帮助,是因为在第二种情况下,需要多转换一次(Barint*然后int*Foo).