c++ 模板运算符< < 的重载解决方案与预期不同

pcrecxhr  于 2023-06-25  发布在  其他
关注(0)|答案(1)|浏览(181)

问题A

给出下面的代码示例:

  1. #include <iostream>
  2. #include <string>
  3. class LogStream {
  4. public:
  5. LogStream& operator<<(int x) {
  6. std::cout << x;
  7. return *this;
  8. }
  9. LogStream& operator<<(const char* src) {
  10. std::cout << src;
  11. return *this;
  12. }
  13. };
  14. typedef char MyType[81];
  15. template <typename OS>
  16. OS& operator<<(OS &os, const MyType& data) {
  17. return os << "my version: " << data;
  18. }
  19. // error: use of overloaded operator '<<' is ambiguous
  20. // (with operand types 'LogStream' and 'char const[81]')
  21. /* LogStream& operator<<(LogStream &os, const MyType& data) {
  22. return os << "my version2: " << (const char*)data;
  23. } */
  24. struct Test {
  25. int x;
  26. MyType str;
  27. };
  28. template <typename OS>
  29. OS& operator<<(OS &os, const Test& data) {
  30. return os << "{ x: " << data.x << ", str: " << data.str << "}";
  31. }
  32. int main() {
  33. Test t = { 33, "333" };
  34. LogStream stream;
  35. stream << t.str;
  36. std::cout << std::endl;
  37. stream << t;
  38. }
实际输出
  1. my version: 333
  2. { x: 33, str: 333}
预期输出
  1. my version: 333
  2. { x: 33, str: my version: 333}

在线编译器:https://godbolt.org/z/6os8xEars
我的问题是:为什么第一个输出使用了我的专用版本MyType,而第二个没有?

问题B

我有一些关于模板专门化的相关问题:
1.当需要隐式转换时,函数模板和常规函数之间的优先级是什么,例如:

  1. struct MyType{};
  2. template <typename T>
  3. void test(T t, char (&data)[16]);
  4. void test(MyType t, const char* data);
  5. int main() {
  6. MyType mt;
  7. char src[16] = { "abc" };
  8. test(mt, src);
  9. }

1.是否有任何工具可以可视化重载解决过程,即使程序编译成功?是否有调试模板代码的方法?

41zrol4v

41zrol4v1#

主要问题的简短答复是:t不是const,但第二个运算符模板的Test参数是。因此,表达式t.strMyType&,但data.strconst MyType&

  1. template <typename OS>
  2. OS& operator<<(OS &os, const Test& data) {
  3. static_assert(std::same_as<const MyType&, decltype((data.str))>);
  4. return os << "{ x: " << data.x << ", str: " << data.str << "}";
  5. }
  6. int main() {
  7. Test t = { 33, "333" };
  8. static_assert(std::same_as<MyType&, decltype((t.str))>);
  9. LogStream stream;
  10. stream << t.str;
  11. std::cout << std::endl;
  12. stream << t;
  13. }

这种差异可能会影响重载解决方案,因为一个关键方面是将函数实参转换为相应形参类型所需的所谓implicit conversion sequence(ICS)。
不幸的是,重载解析并不是一件小事,因此有相当多的事情需要解包。对于表达式stream << t.str,可行函数和ICS如下所示:

  1. // argument is MyType&
  2. LogStream& LogStream::operator<<(const char*); // MyType& -> char* -> const char*
  3. LogStream& operator<<(LogStream&, const MyType&); // identity

第二个版本被视为标识转换,因为
引用参数直接绑定到参数表达式是Identity或派生到基的转换
为了决定两个候选函数中的一个是否是更好的匹配,编译器将考虑可行函数及其转换序列的许多方面。在这种情况下,规则3a适用:
S1是S2的子序列,不包括左值变换。恒等转换序列被认为是任何其他转换的子序列
因此,第二个ICS更好,使模板版本成为最佳可行函数。
对于第二个输出:

  1. // argument is const MyType&
  2. LogStream& LogStream::operator<<(const char*); // const MyType& -> const char*
  3. LogStream& operator<<(LogStream&, const MyType&); // identity

在这种情况下,规则3a不适用,因为排除数组到指针的转换,两个ICS都不是另一个的适当子序列。其他规则都不适用,因此ICS无法区分。因此,非模板操作符现在是最佳可行函数:
1.或者,如果不是,则F1是非模板函数,而F2是模板特化
这也是为什么您注解掉的操作符会是模棱两可的。如果您也注解掉stream << t;行,它就不再有歧义了。
此外,这是唯一一点,它的事项,其中一个重载是一个模板,当然除了要求它是一个有效的示例化。因此,在问题B1中,再次选择函数模板,因为它具有更好的ICS。
至于问题B2,我不知道有什么特定的工具,尽管可以从clang中得到这种输出。现在我使用编译器资源管理器来解决这样的问题。我大概知道规则,但你可以打赌,在回答这类问题之前,我必须仔细重读一遍。现在您已经有了这些解释,它应该给予您了解当您遇到过载问题时需要查找的(许多)东西。
关于更多的阅读,运算符重载规则的官方措辞在标准的over.match.best(https://eel.is/c++draft/over.match.best)部分。

**编辑:**我的首选解决方案是将“特殊”字符串类型 Package 在类中。但是,如果你真的必须使用C风格的char数组,你仍然可以通过引入一个单独的日志类来达到预期的效果:

  1. class MyLogStream
  2. {
  3. LogStream m_base{};
  4. public:
  5. MyLogStream& operator<<(const MyType& data) {
  6. m_base << "my custom operator: " << (const char*)data;
  7. return *this;
  8. }
  9. MyLogStream& operator<<(const auto& data) {
  10. m_base << data;
  11. return *this;
  12. }
  13. };
展开查看全部

相关问题