模板运算符的重载解决<<不符合预期

Overload resolution for template operator<< not as expected

提问人:Bingo 提问时间:6/12/2023 最后编辑:Bingo 更新时间:6/18/2023 访问量:204

问:

问题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

我有一些关于模板专业化的相关问题:

  1. 当需要隐式转换时,函数模板和常规函数之间的优先级是多少,例如:
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);
}
  1. 是否有任何工具可以可视化重载解决过程,即使程序编译成功?有什么方法可以调试模板代码吗?
C++ 模板 运算符重 重载解析

评论

0赞 463035818_is_not_an_ai 6/12/2023
你期待什么输出?您有一个重载要打印 a,另一个重载要打印 . 调用第一个,因为是 a,调用后者,因为是MyTypeTeststream << t.strt.strMyTypestream << ttTest
0赞 Bingo 6/12/2023
我希望第二行使用我的指定版本。所以期望输出是MyType{ x: 33, str: my version: 333}
1赞 463035818_is_not_an_ai 6/12/2023
这更多的是关于重载解析和数组到指针的衰减,而不是模板实例化。
2赞 john 6/12/2023
另一个很好的问题是,为什么无限递归的定义不是无限递归的?OS& operator<<(OS &os, const MyType& data)
1赞 super 6/12/2023
从过载中移除可给出预期的输出。不过,我会把它留给其他人来解释原因。:-)constTest

答:

1赞 sigma 6/14/2023 #1

主要问题的简短回答是:t 不是 const,但第二个运算符模板的 Test 参数是。所以,表达式是 ,但是一个:t.strMyType&data.strconst 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 无法区分。因此,非模板运算符现在是最佳可行函数:

  1. 或者,如果不是这样,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;
    }
};

评论

0赞 Bingo 6/14/2023
非常感谢您的专业回答!这是对我实际输出的解释。但我还有一个问题。实际上,是一个库实现,我们无法更改它。我只想重载类型.我怎样才能实现这个目标?这似乎是不可能的,因为转换是.我这里也有一个代码示例LogStreamoperator<<(const char* str)char[81]array-to-pointerlvalue transformation
0赞 Bingo 6/14/2023
另一个小问题。由于评论中的常量问题,我重新确定了输出差异。然后我去验证是 const。但我用来检查恒常性。它显示不是 const,这与您的第一个结果不同。那么 和 之间有什么不一样呢?data.strstd::is_const_v<decltype(data.str)>data.strstatic_assertis_const_vsame_as
0赞 sigma 6/15/2023
@Bingo:char 数组也是必需的吗?正如其他注释中所指出的,最简单的解决方案是避免这些并使用类类型,例如 ,甚至是自定义包装类。std::stringstd::string_view
0赞 sigma 6/15/2023
@Bingo 至于其他评论,那是由于我没有提到的其他一些技术细节。即,与以下不同:在本例中,前者只是成员的类型名,但后者是命名此成员的表达式的 cv-ref-qualified 类型。你可以使用 ,但你必须编写 std::is_const_v<std::remove_reference_t<decltype((data.str))>>。C++不是很有趣吗!decltype(data.str)decltype((data.str))MyTypeis_const_v
0赞 Bingo 6/15/2023
是的,C++很有趣...... :(char 数组是必需的。我不想向 char 数组添加包装器,因为我已经实现了一个模板日志函数来记录任何指定的结构类型。无法在此常规日志函数中添加包装器。我真正需要的是 char 数组的模板规范......