如何在模板化转换运算符中消除这种结构的歧义?
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()
template 的 operator 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 的原始模板化版本中,当需要从 Bar
到 Foo
的隐式转换时,GCC 仅 "instantiates" 转换运算符声明,然后在实例化它们的主体之前解析重载。
至于为什么explicit
有帮助,是因为在第二种情况下,需要多转换一次(Bar
→int*
然后int*
→Foo
).
在对为什么我的代码在 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()
template 的 operator int*()
实例化被认为是重载的一部分f
初始化的分辨率。参见 [over.match.copy]:
1 [...] Assuming that “
cv1 T
” is the type of the object being initialized, withT
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 ofS
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-qualifiedT
” 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 的原始模板化版本中,当需要从 Bar
到 Foo
的隐式转换时,GCC 仅 "instantiates" 转换运算符声明,然后在实例化它们的主体之前解析重载。
至于为什么explicit
有帮助,是因为在第二种情况下,需要多转换一次(Bar
→int*
然后int*
→Foo
).