提问人:Bingo 提问时间:6/12/2023 最后编辑:Bingo 更新时间:6/18/2023 访问量:204
模板运算符的重载解决<<不符合预期
Overload resolution for template operator<< not as expected
问:
问题A
给出此处的代码示例:
#include <iostream>
#include <string>
class LogStream {
public:
LogStream& operator<<(int x) {
std::cout << x;
return *this;
}
LogStream& operator<<(const char* src) {
std::cout << src;
return *this;
}
};
typedef char MyType[81];
template <typename OS>
OS& operator<<(OS &os, const MyType& data) {
return os << "my version: " << data;
}
// error: use of overloaded operator '<<' is ambiguous
// (with operand types 'LogStream' and 'char const[81]')
/* LogStream& operator<<(LogStream &os, const MyType& data) {
return os << "my version2: " << (const char*)data;
} */
struct Test {
int x;
MyType str;
};
template <typename OS>
OS& operator<<(OS &os, const Test& data) {
return os << "{ x: " << data.x << ", str: " << data.str << "}";
}
int main() {
Test t = { 33, "333" };
LogStream stream;
stream << t.str;
std::cout << std::endl;
stream << t;
}
实际输出
my version: 333
{ x: 33, str: 333}
预期输出
my version: 333
{ x: 33, str: my version: 333}
在线编译器:https://godbolt.org/z/6os8xEars
我的问题是:为什么第一个输出使用我的专用版本,而第二个输出不使用?MyType
问题B
我有一些关于模板专业化的相关问题:
- 当需要隐式转换时,函数模板和常规函数之间的优先级是多少,例如:
struct MyType{};
template <typename T>
void test(T t, char (&data)[16]);
void test(MyType t, const char* data);
int main() {
MyType mt;
char src[16] = { "abc" };
test(mt, src);
}
- 是否有任何工具可以可视化重载解决过程,即使程序编译成功?有什么方法可以调试模板代码吗?
答:
主要问题的简短回答是:t 不是 const,但第二个运算符模板的 Test 参数是。所以,表达式是 ,但是一个:t.str
MyType&
data.str
const MyType&
template <typename OS>
OS& operator<<(OS &os, const Test& data) {
static_assert(std::same_as<const MyType&, decltype((data.str))>);
return os << "{ x: " << data.x << ", str: " << data.str << "}";
}
int main() {
Test t = { 33, "333" };
static_assert(std::same_as<MyType&, decltype((t.str))>);
LogStream stream;
stream << t.str;
std::cout << std::endl;
stream << t;
}
这种差异可能会影响重载分辨率,因为一个关键方面是将函数参数转换为相应参数类型所需的所谓隐式转换序列 (ICS)。
不幸的是,过载解决并非易事,因此有很多东西需要解开。对于表达式,可行函数和 ICS 将如下所示:stream << t.str
// argument is MyType&
LogStream& LogStream::operator<<(const char*); // MyType& -> char* -> const char*
LogStream& operator<<(LogStream&, const MyType&); // identity
第二个版本计为身份转换,因为
将引用参数直接绑定到参数表达式是 Identity 或派生到基数的转换
为了确定两个候选函数中的一个是否更匹配,编译器将考虑可行函数及其转换序列的许多方面。在这种情况下,规则3a适用:
S1 是 S2 的子序列,不包括左值变换。身份转换序列被视为任何其他转换的子序列
因此,第二个 ICS 更好,使模板版本成为最佳可行功能。
对于第二个输出:
// argument is const MyType&
LogStream& LogStream::operator<<(const char*); // const MyType& -> const char*
LogStream& operator<<(LogStream&, const MyType&); // identity
在这种情况下,规则 3a 不适用,因为除了数组到指针的转换之外,任何一个 ICS 都不是另一个 ICS 的正确子序列。其他规则均不适用,因此 ICS 无法区分。因此,非模板运算符现在是最佳可行函数:
- 或者,如果不是这样,F1 是非模板函数,而 F2 是模板专用化
这也是为什么你注释掉的运算符会模棱两可。如果您也注释掉该行,则不再模棱两可。stream << t;
此外,这是重载之一是否为模板的唯一重要点,当然,除了要求它是有效的实例化之外。因此,在问题 B1 中,再次选择函数模板是因为它具有更好的 ICS。
至于问题 B2,我不知道有任何特定的工具,尽管可以从 clang 中获得这种输出。现在,我使用Compiler Explorer来解决这样的问题。我大致了解规则,但你可以打赌,在回答这种问题之前,我必须仔细阅读它们。现在您已经有了这些解释,它应该让您了解当您遇到过载问题时要寻找的(许多)事情。
如需更多阅读,操作员重载规则的官方措辞位于标准的 [over.match.best] 部分。
编辑:我的首选解决方案是将“特殊”字符串类型包装在一个类中。但是,如果您确实必须使用 C 样式的 char 数组,您仍然可以通过引入单独的日志记录类来获得所需的结果:
class MyLogStream
{
LogStream m_base{};
public:
MyLogStream& operator<<(const MyType& data) {
m_base << "my custom operator: " << (const char*)data;
return *this;
}
MyLogStream& operator<<(const auto& data) {
m_base << data;
return *this;
}
};
评论
LogStream
operator<<(const char* str)
char[81]
array-to-pointer
lvalue transformation
data.str
std::is_const_v<decltype(data.str)>
data.str
static_assert
is_const_v
same_as
std::string
std::string_view
is_const_v<std::remove_reference_t<decltype((data.str))>>
。C++不是很有趣吗!decltype(data.str)
decltype((data.str))
MyType
is_const_v
上一个:如何连接两个基于元组的对象
评论
MyType
Test
stream << t.str
t.str
MyType
stream << t
t
Test
MyType
{ x: 33, str: my version: 333}
OS& operator<<(OS &os, const MyType& data)
const
Test