“Overload resolution for template operator<< not as expected."

huangapple go评论78阅读模式
英文:

Overload resolution for template operator<< not as expected

问题

Question 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}

Question B

有关模板特化的相关问题:

  1. 当需要隐式转换时,函数模板和普通函数之间的优先级如何?

    在这种情况下,编译器会优先选择普通函数而不是函数模板。因此,在给定的示例中,当调用test(mt, src)时,编译器会选择匹配度更高的test(MyType t, const char* data),而不是模板函数。

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

    有一些工具和技术可以帮助可视化重载解析和调试模板代码,例如:

    • 编译器错误消息: 编译器通常会提供有关重载解析失败的详细信息,包括候选函数和它们之间的冲突。这些错误消息通常包含了有关为什么选择了特定函数或模板的信息。

    • IDE集成: 许多集成开发环境(IDE)提供了代码分析工具,可以可视化重载解析过程。例如,Visual Studio可以在代码中显示函数签名并帮助您理解哪个函数被调用。

    • 模板元编程工具: 模板元编程库(如Boost.MPL)可以帮助您在编译时查看模板参数和类型,从而帮助调试模板代码。

    • 模板调试技巧: 您可以使用编译器选项来生成模板展开的详细信息,以便查看模板展开后的代码。这可以帮助您了解模板代码的实际工作方式。

    请注意,可视化重载解析和模板调试通常需要深入了解C++模板系统以及使用特定工具和IDE的经验。

英文:

Question A

Given the code example here:

#include &lt;iostream&gt;
#include &lt;string&gt;

class LogStream {
public:
    LogStream&amp; operator&lt;&lt;(int x) {
        std::cout &lt;&lt; x;
        return *this;
    }
    LogStream&amp; operator&lt;&lt;(const char* src) {
        std::cout &lt;&lt; src;
        return *this;
    }
};

typedef char MyType[81];

template &lt;typename OS&gt;
OS&amp; operator&lt;&lt;(OS &amp;os, const MyType&amp; data) {
  return os &lt;&lt; &quot;my version: &quot; &lt;&lt; data;
}

// error: use of overloaded operator &#39;&lt;&lt;&#39; is ambiguous
//        (with operand types &#39;LogStream&#39; and &#39;char const[81]&#39;)

/* LogStream&amp; operator&lt;&lt;(LogStream &amp;os, const MyType&amp; data) {
  return os &lt;&lt; &quot;my version2: &quot; &lt;&lt; (const char*)data;
} */


struct Test {
    int x;
    MyType str;
};

template &lt;typename OS&gt;
OS&amp; operator&lt;&lt;(OS &amp;os, const Test&amp; data) {
  return os &lt;&lt; &quot;{ x: &quot; &lt;&lt; data.x &lt;&lt; &quot;, str: &quot; &lt;&lt; data.str &lt;&lt; &quot;}&quot;;
}

int main() {
    Test t = { 33, &quot;333&quot; };
    LogStream stream;

    stream &lt;&lt; t.str;
    std::cout &lt;&lt; std::endl;
    stream &lt;&lt; t;
}

Actual Output

my version: 333
{ x: 33, str: 333}

Expected Output

my version: 333
{ x: 33, str: my version: 333}

Online compiler: https://godbolt.org/z/6os8xEars

My problem is: why does the first output use my specialized version of MyType, but the second one doesn't?

Question B

I have some related question about template specialization:

  1. What is the priority between function templates and regular functions when implicit conversion is needed, e.g:
struct MyType{};

template &lt;typename T&gt;
void test(T t, char (&amp;data)[16]);

void test(MyType t, const char* data);

int main() {
    MyType mt;
    char src[16] = { &quot;abc&quot; };
    test(mt, src);
}
  1. Are there any tools visualize the overload resolution process, even when a program compiles successfully? Are there any ways to debug template code?

答案1

得分: 1

以下是您要求的翻译部分:

主要问题的简短回答是:变量 t 不是 const 的,但是你第二个操作符模板的参数 Testconst 的。因此,表达式 t.str 是一个 MyType&amp;,但 data.str 是一个 const MyType&amp;

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

不幸的是,重载决议并不是一件简单的事情,因此有很多东西需要解释。对于表达式 stream &lt;&lt; t.str,可行的函数和ICS将如下所示:

// 参数是 MyType&amp;
LogStream&amp; LogStream::operator&lt;&lt;(const char*); // MyType&amp; -&gt; char* -&gt; const char*
LogStream&amp; operator&lt;&lt;(LogStream&amp;, const MyType&amp;); // identity

第二个版本被认为是标识转换,因为:

将引用参数直接绑定到参数表达式是标识或从派生到基类的转换

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

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

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

对于第二个输出:

// 参数是 const MyType&amp;
LogStream&amp; LogStream::operator&lt;&lt;(const char*); // const MyType&amp; -&gt; const char*
LogStream&amp; operator&lt;&lt;(LogStream&amp;, const MyType&amp;); // identity

在这种情况下,规则3a不适用,因为除了数组到指针的转换外,两者都不是彼此的适当子序列。没有其他规则适用,因此ICS是无法区分的。结果,非模板操作符现在成为最佳可行函数:

  1. 或者,如果不是这样,F1 是一个非模板函数,而 F2 是一个模板特化

这也是为什么你注释掉的操作符会产生歧义的原因。如果你也注释掉 stream &lt;&lt; t; 这一行,它就不再具有歧义。

此外,在这里,唯一关键的地方是其中一个重载是一个模板,当然还有一个要求,即它必须是有效的实例化。所以,在问题B1中,函数模板再次被选择,因为它具有更好的ICS。

至于问题B2,我不知道具体的工具,尽管可能可以从clang中获取这种类型的输出。如今,我使用Compiler Explorer来解决这类问题。我大致了解这些规则,但你可以肯定我在回答这类问题之前会仔细阅读它们。现在你有了这些解释,应该可以让你对重载问题的解决方法有一些了解。

要了解更多信息,有关运算符重载规则的官方文本位于标准的[over.match.best]节。

编辑: 我更喜欢的解决方案是将“特殊”的字符串类型封装在一个类中。但是,如果你真的必须使用C风格的字符数组,你仍然可以通过引入一个单独的日志类来实现所需的结果:

class MyLogStream
{
    LogStream m_base{};
public:    
    MyLogStream&amp; operator&lt;&lt;(const MyType&amp; data) {
        m_base &lt;&lt; "my custom operator: " &lt;&lt; (const char*)data;
        return *this;
    }

    MyLogStream&amp; operator&lt;&lt;(const auto&amp; data) {
        m_base &lt;&lt; data;
        return *this;
    }
};
英文:

The short answer to the main question is: t is not const, but the Test parameter to your second operator template is. So, the expression t.str is a MyType&amp;, but data.str is a const MyType&amp;:

template &lt;typename OS&gt;
OS&amp; operator&lt;&lt;(OS &amp;os, const Test&amp; data) {
    static_assert(std::same_as&lt;const MyType&amp;, decltype((data.str))&gt;);
    return os &lt;&lt; &quot;{ x: &quot; &lt;&lt; data.x &lt;&lt; &quot;, str: &quot; &lt;&lt; data.str &lt;&lt; &quot;}&quot;;
}

int main() {
    Test t = { 33, &quot;333&quot; };
    static_assert(std::same_as&lt;MyType&amp;, decltype((t.str))&gt;);
    LogStream stream;

    stream &lt;&lt; t.str;
    std::cout &lt;&lt; std::endl;
    stream &lt;&lt; t;
}

This kind of difference can affect overload resolution, because a key aspect is the so-called implicit conversion sequence (ICS) required to transform a function argument to the type of the corresponding parameter.

Unfortunately, overload resolution is not trivial, so there are quite a few things to unpack. For the expression stream &lt;&lt; t.str, the viable functions and ICSs will be like this:

// argument is MyType&amp;
LogStream&amp; LogStream::operator&lt;&lt;(const char*); // MyType&amp; -&gt; char* -&gt; const char*
LogStream&amp; operator&lt;&lt;(LogStream&amp;, const MyType&amp;); // identity

The second version is counted as the identity conversion because

> Binding of a reference parameter directly to the argument expression is either Identity or a derived-to-base Conversion

To decide whether one of two candidates is a better match, the compiler will consider numerous aspects of the viable functions and their conversion sequences. In this case rule 3a applies:

> S1 is a subsequence of S2, excluding lvalue transformations. The identity conversion sequence is considered a subsequence of any other conversion

Therefore, the second ICS is better, making the template version the best viable function.

For the second output:

// argument is const MyType&amp;
LogStream&amp; LogStream::operator&lt;&lt;(const char*); // const MyType&amp; -&gt; const char*
LogStream&amp; operator&lt;&lt;(LogStream&amp;, const MyType&amp;); // identity

In this case, rule 3a does not apply since, excluding the array-to-pointer conversion, neither ICS is a proper subsequence of the other. None of the other rules apply, so the ICSs are indistinguishable. As a result, the non-template operator is now the best viable function:

> 4) or, if not that, F1 is a non-template function while F2 is a template specialization

This is also why the operator you commented out would be ambiguous. It is no longer ambiguous if you also comment out the line stream &lt;&lt; t;.

Additionally, this is the only point where it matters that one of the overloads is a template, of course apart from the requirement that it be a valid instantiation. So, in question B1, it is again the case that the function template is selected because it has the better ICS.

As for question B2, I'm not aware of any specific tools, although it may be possible to get this kind of output from clang. Nowadays I use Compiler Explorer to figure out problems like these. I know the rules roughly, but you can bet I have to reread them closely before answering this kind of question. Now that you have these explanations, it should give you some idea of the (many) things to look for when you have a problem with overloads.

For more reading, the official wording of the rules for operator overloading is in the section [over.match.best] of the standard.

Edit: My preferred solution would wrap the "special" string type in a class. However, if you really must use C-style char arrays, you can still achieve the desired result by introducing a separate logging class:

class MyLogStream
{
    LogStream m_base{};
public:    
    MyLogStream&amp; operator&lt;&lt;(const MyType&amp; data) {
        m_base &lt;&lt; &quot;my custom operator: &quot; &lt;&lt; (const char*)data;
        return *this;
    }

    MyLogStream&amp; operator&lt;&lt;(const auto&amp; data) {
        m_base &lt;&lt; data;
        return *this;
    }
};

huangapple
  • 本文由 发表于 2023年6月12日 16:31:46
  • 转载请务必保留本文链接:https://go.coder-hub.com/76454830.html
  • c++
  • operator-overloading
  • overload-resolution
  • templates
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定