涉及临时变量的运算符重载解析顺序

11

考虑以下最简示例:

#include <iostream>

using namespace std;

class myostream : public ostream {
    public:
        myostream(ostream const &other) :
            ostream(other.rdbuf())
        { }
};

int main() {
    cout << "hello world" << endl;

    myostream s(cout);
    s << "hello world" << endl;

    myostream(cout) << "hello world" << endl;
}

输出结果在g++和Visual C++上均为:

hello world
hello world
0x4012a4

看起来,将内容写入临时对象myostream(cout)的版本似乎更喜欢成员运算符 ostream::operator<<(void *),而不是自由运算符 operator<<(ostream &, char*)。这似乎取决于对象是否有名称。

为什么会这样?我如何防止这种行为发生?

编辑: 为什么会发生已经在各种答案中清楚了。至于如何预防这种情况,以下方法似乎可行:

class myostream : public ostream {
    public:
        // ...
        myostream &operator<<(char const *str) {
            std::operator<<(*this, str);
            return *this;
        }
};

然而,这会导致各种模糊性。


你可以将这个回答作为一个起点,去实现类似于你想要达到的目标:http://stackoverflow.com/questions/469696/what-is-your-most-useful-c-c-snippet/470999#470999 你需要给这个类添加功能来接受输入修饰符(std::hex, std::endl...),但这应该不难。 - David Rodríguez - dribeas
6个回答

7

如果一个对象没有名称(即它是临时的),它就不能绑定到非const引用。具体来说,它无法绑定到以下函数的第一个参数:

operator<<(ostream &, char *)

直到C++0x的出现,为我们带来了右值引用的强大功能。 - dirkgently

6

rvalues 无法绑定到非 const 引用。所以在你的例子中,类型为 ostream 的临时变量不能作为 free operator<<(std::ostream&, char const*) 的第一个参数,实际使用的是成员函数 operator<<(void*)。

如果需要,可以添加如下调用:

myostream(cout).flush() << "foo";

这将把rvalue转换成引用。

需要注意的是,在C++0X中,引入了rvalue引用,可以提供以rvalue引用为参数的operator<<重载,解决了该问题的根本原因。


你是不是想说“rvalues 无法绑定...”?还有“...将 rvalue 转换为 lvalue...”? - Éric Malenfant

3

我刚刚意识到了答案的一部分。临时变量不是lvalue,因此不能用作类型为ostream&的参数。

“如何使这个工作”这个问题仍然存在...


最简单的方法:提供成员函数,让它们为你完成工作。 - David Rodríguez - dribeas

1

由于迄今为止没有一个干净的解决方案,我将采用肮脏的解决方案:

myostream operator<<(myostream stream, char const *str) {
    std::operator<<(stream, str);
    return stream;
}

这仅仅是因为myostream有一个拷贝构造函数。(在内部,它由一个引用计数的std::stringbuf支持。)

0

虽然C++11通过引入右值引用解决了这个问题,但我认为这可能是针对C++11之前的一个变通方法。

解决方案是拥有一个成员函数<<操作符,在其中我们可以将其转换为基类的非const引用:

class myostream : public ostream {
    public:
        // ...
        template<typename T>
        ostream &operator<<(const T &t) {
            //now the first operand is no longer a temporary,
            //so the non-member operators will overload correctly
            return static_cast<ostream &>(*this) << t;
        }
};

-1

嗯,我不知道是什么C++规范导致了这个问题,但很容易理解为什么会发生这种情况。

一个临时变量存在于栈上,通常用于传递给另一个函数或对其进行单个操作。因此,如果您在其上调用free运算符:

operator<<(myostream(cout))

它将在此操作的末尾被销毁,并且第二个"<<"运算符附加endl将引用无效对象。从free "<<"运算符返回的值将是对已销毁的临时对象的引用。C++规范可能定义了有关free运算符的规则,以防止这种情况使C++程序员感到沮丧和困惑。

现在,在临时变量上执行"<<(void*)"成员运算符的情况下,返回值是对象本身,该对象仍然位于堆栈上并且未被销毁,因此编译器知道不要销毁它,而是将其传递给下一个成员运算符,即接受endl的运算符。临时变量上的运算符链接是一种简洁的C++代码功能,因此我相信C++规范设计者考虑过它并故意实现了编译器来支持它。

编辑

有人说这与非const引用有关。这段代码可以编译:

#include <iostream>
using namespace std;
class myostream : public ostream { 
    public: 
        myostream(ostream const &other) : 
            ostream(other.rdbuf()) 
        { } 
            ~myostream() { cout << " destructing "; }
    }; 
int _tmain(int argc, _TCHAR* argv[])
{
    basic_ostream<char>& result = std::operator << (myostream(cout), "This works");
    std::operator << (result, "illegal");
         return 0;
}

它返回

  This works destructing illegal

1
抱歉 - 您对临时变量的生命周期理解有误。 - anon
请随意指教。据我所知,这里的描述与我的解释一致:http://publib.boulder.ibm.com/infocenter/comphelp/v8v101/index.jsp?topic=/com.ibm.xlcpp8a.doc/language/ref/cplr382.htm。如果在临时对象上使用静态方法,则必须在清理该操作的调用堆栈时销毁该临时对象。如果它被用作成员操作的“this”,则它可以在操作完成后继续存在,因为它位于调用堆栈的底部,因此链接的成员运算符将起作用,但静态方法不会。 - David Gladfelter
你发布的链接是一个众所周知的C++信息贫瘠的来源(并且表现出奇怪的行为,将我翻转到另一页)。C++标准是参考文献,在这里它说(有效地)临时变量必须一直存在,直到它所属的完整表达式结束,而在这种情况下,完整的<<操作符链就是完整表达式。代码不能按预期工作的真正原因在我的答案中给出。 - anon
@David G - 你的代码编译通过是因为VC++允许将非const引用绑定到r-value作为非可移植扩展(但应该会收到警告)。 - Manuel
@David G - 你需要将“警告级别=4”设置才能看到警告。 - Manuel
显示剩余5条评论

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