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

Bin*_*ngo 5 c++ templates operator-overloading overload-resolution

问题一

给出这里的代码示例:

#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;
}
Run Code Online (Sandbox Code Playgroud)

实际产量

my version: 333
{ x: 33, str: 333}
Run Code Online (Sandbox Code Playgroud)

预期输出

my version: 333
{ x: 33, str: my version: 333}
Run Code Online (Sandbox Code Playgroud)

在线编译器: 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);
}
Run Code Online (Sandbox Code Playgroud)
  1. 即使程序编译成功,是否有任何工具可以可视化重载解决过程?有没有办法调试模板代码?

sig*_*gma 1

主要问题的简短答案是: t 不是 const,但第二个运算符模板的 Test 参数是 const。因此,表达式t.str是 a MyType&,但是data.str是 a 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;
}
Run Code Online (Sandbox Code Playgroud)

这种差异可能会影响重载解析,因为一个关键方面是将函数参数转换为相应参数的类型所需的所谓隐式转换序列(ICS)。

不幸的是,重载解析并不是微不足道的,因此有很多东西需要解压。对于表达式stream << t.str,可行的函数和 ICS 将如下所示:

// argument is MyType&
LogStream& LogStream::operator<<(const char*); // MyType& -> char* -> const char*
LogStream& operator<<(LogStream&, const MyType&); // identity
Run Code Online (Sandbox Code Playgroud)

第二个版本算作身份转换,因为

将引用参数直接绑定到参数表达式可以是 Identity,也可以是派生到基数的转换

为了确定两个候选函数之一是否更匹配,编译器将考虑可行函数及其转换序列的许多方面。在这种情况下,规则 3a 适用:

S1 是 S2 的子序列,不包括左值变换。恒等转换序列被视为任何其他转换的子序列

因此,第二个ICS更好,使模板版本成为最佳可行功能。

对于第二个输出:

// argument is const MyType&
LogStream& LogStream::operator<<(const char*); // const MyType& -> const char*
LogStream& operator<<(LogStream&, const MyType&); // identity
Run Code Online (Sandbox Code Playgroud)

在这种情况下,规则 3a 不适用,因为除了数组到指针的转换之外,两个 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;
    }
};
Run Code Online (Sandbox Code Playgroud)