为什么Visual C++编译器在这里调用了错误的重载函数?

3
为什么Visual C++编译器在这里调用了错误的重载函数?
我有一个 ostream 的子类,用来定义格式化缓冲区。有时候我想要创建一个临时对象,并立即使用通常的 << 运算符将一个字符串插入到它中间,像这样:
M2Stream() << "the string";

很不幸,程序调用了operator<<(ostream, void *)成员重载,而不是operator<<(ostream, const char *)非成员重载。

我编写了下面的示例作为测试,在其中定义了自己的M2Stream类以重现问题。

我认为问题在于M2Stream()表达式产生了一个临时变量,这会导致编译器更喜欢void *重载。但是为什么呢?如果我将非成员重载的第一个参数设置为const M2Stream&,则出现歧义。

另一个奇怪的事情是,如果我首先定义一个const char *类型的变量,然后再调用它,而不是使用字面char字符串,它就会调用所需的const char *重载,如下所示:

const char *s = "char string variable";
M2Stream() << s;  

好像字面字符串的类型与const char*变量不同!它们不应该是相同的类型吗?为什么编译器在我使用临时字符字符串和字面字符字符串时会调用void*重载函数?

#include "stdafx.h"
#include <iostream>
using namespace std;


class M2Stream
{
public:
    M2Stream &operator<<(void *vp)
    {
        cout << "M2Stream bad operator<<(void *) called with " << (const char *) vp << endl;
        return *this;
    }
};

/* If I make first arg const M2Stream &os, I get
\tests\t_stream_insertion_op\t_stream_insertion_op.cpp(39) : error C2666: 'M2Stream::operator <<' : 2 overloads have similar conversions
        \tests\t_stream_insertion_op\t_stream_insertion_op.cpp(13): could be 'M2Stream &M2Stream::operator <<(void *)'
        \tests\t_stream_insertion_op\t_stream_insertion_op.cpp(20): or 'const M2Stream &operator <<(const M2Stream &,const char *)'
        while trying to match the argument list '(M2Stream, const char [45])'
        note: qualification adjustment (const/volatile) may be causing the ambiguity
*/
const M2Stream & operator<<(M2Stream &os, const char *val)
{
    cout << "M2Stream good operator<<(const char *) called with " << val << endl;
    return os;
}


int main(int argc, char argv[])
{
    // This line calls void * overload, outputs: M2Stream bad operator<<(void *) called with literal char string on constructed temporary
    M2Stream() << "literal char string on constructed temporary";

    const char *s = "char string variable";

    // This line calls the const char * overload, and outputs: M2Stream good operator<<(const char *) called with char string variable
    M2Stream() << s;  

    // This line calls the const char * overload, and outputs: M2Stream good operator<<(const char *) called with literal char string on prebuilt object
    M2Stream m;
    m << "literal char string on prebuilt object";
    return 0;
}

输出:

M2Stream bad operator<<(void *) called with literal char string on constructed temporary
M2Stream good operator<<(const char *) called with char string variable
M2Stream good operator<<(const char *) called with literal char string on prebuilt object

在VS 2003中确认过这一点。我注意到,如果您将void * vp更改为int * vp,则会获得您期望的行为。在某些情况下必须进行某种隐式转换为void。 - Doug T.
我使用了VS2008和VC6编译器进行编译。用2008编译,我得到了上述的输出结果。但是用VC6编译,第一次调用时我也得到了const char*,这很令人惊讶。 - Naveen
@Naveen:VC6错误地允许非const引用绑定到临时对象--请参见litb或我的答案,了解为什么这会导致事情“工作”(尽管根据标准,它们实际上不应该工作)。 - j_random_hacker
5个回答

10
编译器做得很对:Stream() << "hello"; 应该使用定义为成员函数的operator<<。因为临时流对象只能与const引用绑定,而不是非const引用,因此处理char const*的非成员运算符将不被选中。这是有意设计的,因为如果修改该运算符,则会出现歧义,因为编译器无法决定要使用哪个可用的运算符。所有这些运算符都是为了拒绝针对临时对象的非成员operator<<而设计的。
然后,是的,字符串文字具有与char const*不同的类型。字符串字面量是一个由const字符组成的数组。但在你的情况下,这并不重要。我不知道MSVC++添加了哪些operator<<的重载。它可以添加进一步的重载,只要它们不影响有效程序的行为。
至于为什么M2Stream() << s;即使第一个参数是非const引用也可以工作...嗯,MSVC++有一个扩展,允许非const引用绑定到临时对象。将警告级别设置为4可以看到有关它的警告(类似于“使用了非标准扩展...”)。
现在,因为有一个接受void const*的成员operator<<,而char const*可以转换为它,所以将选择该运算符,并将地址作为其输出,因为这就是void const*重载的用途。我在你的代码中看到,你实际上有一个 `void*` 的重载,而不是 `void const*` 的重载。尽管字符串字面值的类型是 `char const[N]`(其中 N 是您放置的字符数),但字符串字面值可以转换为 `char*`。但是这种转换已经被弃用了。字符串字面值转换为 `void*` 不是标准行为。这似乎是 MSVC++ 编译器的另一个扩展。但这就解释了为什么字符串字面值与 `char const*` 指针的处理方式不同。标准规定如下:

非宽字符串字面值(2.13.4)可以转换为 "指向 char 的指针" 类型的右值;宽字符串字面值可以转换为 "指向 wchar_t 的指针" 类型的右值。在任一情况下,结果都是指向数组的第一个元素的指针。只有当存在显式适当的指针目标类型时,才考虑进行此转换,而不是在需要将 lvalue 转换为 rvalue 时进行转换。[注:此转换已被弃用。请参见 Annex D。]


1
像往常一样,+1,非常彻底 :) 但是你能解释一下为什么有些operator<<()是成员函数而有些不是吗?我看不出它有任何作用,并且破坏了一些看起来应该有效的方便技巧。 - j_random_hacker
谢谢。我重新编写了代码,通过为我的 ostream 派生类定义一些额外的 <<() 重载来避免问题。 - Carlos A. Ibarra
1
以下是編程相關內容的中文翻譯:這是另一個很好的解釋,為什麼 rvalue 無法綁定到非 const 引用。這與您無法將“取地址符”(&)應用於 rvalue 的原因有關。 - Doug T.
在上面的链接中还指出:“(我应该提到VC有一个邪恶的扩展,允许这样做,但如果你使用/W4编译,它会在邪恶的扩展被激活时发出警告。)” - Doug T.
1
注意,我在说“允许...绑定到临时变量”时有点马虎。我应该说“允许...绑定到右值”。它们并不完全相同。抛出的异常对象是临时对象,但它们是左值,并通常绑定到非const引用:try { throw 10; } catch(int &r) { ... } 临时(未命名的抛出异常对象)被存储在某个地方。它是一个左值,因此可以绑定到“int &r”。(它被称为临时对象,因为它仅在异常处理期间暂时存在)。 - Johannes Schaub - litb
显示剩余5条评论

5
第一个问题是由于奇怪和棘手的C++语言规则引起的:
  1. 通过调用构造函数创建的临时对象是rvalue
  2. rvalue不能绑定到非const引用。
  3. 但是,可以在rvalue对象上调用非const方法。
发生的情况是,非成员函数ostream& operator<<(ostream&, const char*)尝试将你创建的M2Stream临时对象绑定到非const引用,但失败了(规则#2);但是ostream& ostream::operator<<(void*)是一个成员函数,因此可以将其绑定到它上面。在缺少const char*函数的情况下,它被选为最佳重载。
我不确定IOStreams库的设计者为什么决定将void*operator<<()作为方法而不是const char*operator<<(),但事实就是这样,所以我们必须处理这些奇怪的不一致性。
我不确定第二个问题为什么会出现。你是否在不同的编译器中得到相同的行为?这可能是编译器或C++标准库的bug,但至少要先尝试使用常规的ostream复制行为。

你提供了一个很好的解释,我认为我会给你点赞 :) - Johannes Schaub - litb
谢谢!这种情况不会发生在常规的ostream上(否则字面字符串将始终输出为0x00123456指针),但最初它发生在我使用basic_ostream<>子类时。 - Carlos A. Ibarra
抱歉,对于常规的ostream来说确实会发生这种情况。以下代码输出一个十六进制地址: stringbuf b; ostream(&b) << "hello"; cout << b.str() << endl; - Carlos A. Ibarra
再次感谢您清晰的解释。我在选择您和litb的答案时遇到了困难,最终选择了他的答案,因为他对第二个问题的解释更好,但是您的解释更加清晰。 - Carlos A. Ibarra
@Carlos:别担心,很高兴我能帮到你。我以前也曾经遇到过这个问题,让我头疼不已! :) - j_random_hacker

1
问题在于您正在使用临时流对象。将代码更改为以下内容,它就可以工作:
M2Stream ms;
ms << "the string";

基本上,编译器拒绝将临时对象绑定到非const引用。

关于您提出的第二点:为什么当您具有“const char *”对象时它会绑定,我认为这是VC编译器中的一个错误。但我无法确定,当您只有字符串文字时,它会转换为'void *'并转换为'const char *'。 当您具有'const char *'对象时,那么第二个参数不需要进行转换 - 这可能是VC允许非标准行为的触发器,以允许非const引用绑定。

我相信8.5.3 / 5是涵盖此内容的标准部分。


我不确定这是否正确。如果我在我的电脑上将void更改为int,似乎可以工作。 - Doug T.
1
"void*"是特殊的。大多数类型都可以转换为它,我猜VC也允许将其转换为“const char *”。如果您使用不同的编译器进行上述更改,则代码将无法编译。 - Richard Corden
只是为了澄清 - (由于某种原因我无法删除我的先前评论)- 另一个编译器也不会接受您的代码,因为“const char *”到“void *”不是标准的。 - Richard Corden
+1. 对于流使用命名变量是最明智的解决方法(尽管它感觉有些不太优雅...)而且你关于第二种情况的不同转换的推理听起来很有道理。 - j_random_hacker

0

我不确定你的代码是否应该编译。我认为:

M2Stream & operator<<( void *vp )

应该是:

M2Stream & operator<<( const void *vp )

实际上,仔细查看代码后,我相信你所有的问题都是由于const引起的。以下代码按预期工作:
#include <iostream>
using namespace std;


class M2Stream
{
};

const M2Stream & operator<<( const M2Stream &os, const char *val)
{
    cout << "M2Stream good operator<<(const char *) called with " << val << endl;
    return os;
}


int main(int argc, char argv[])
{
    M2Stream() << "literal char string on constructed temporary";

    const char *s = "char string variable";

    // This line calls the const char * overload, and outputs: M2Stream good operator<<(const char *) called with char string variable
    M2Stream() << s;  

    // This line calls the const char * overload, and outputs: M2Stream good operator<<(const char *) called with literal char string on prebuilt object
    M2Stream m;
    m << "literal char string on prebuilt object";
    return 0;
}

我复制粘贴失败了 - 现在请您试一下。 - anon
好的,它能工作是因为您删除了成员"void * operator"。我没有这个便利,因为我试图解决的原始问题是在basic_ostream<>的子类中,而这个basic_ostream有一个接受void *的成员运算符,所以我不能将其删除。 - Carlos A. Ibarra

0
你可以使用这样的重载:
template <int N>
M2Stream & operator<<(M2Stream & m, char const (& param)[N])
{
     // output param
     return m;
}

作为额外的奖励,您现在知道N是数组的长度。

1
这在 VC 上可能可行,但不是标准做法。临时的“M2Stream()”无法绑定到非 const 引用:“M2Stream & m”。 - Richard Corden
我已经在GCC上尝试了这个特定的代码片段,但是类似的函数在GCC和VC8上都能正常工作。 - unwesen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接