std::string 引用类成员的奇怪行为

Weird behavior with std::string reference class member

提问人:morpheus 提问时间:7/29/2023 最后编辑:anastaciumorpheus 更新时间:8/3/2023 访问量:247

问:

给定此代码:

#include <iostream>

class Foo {
    public:
        Foo(const std::string& label) : label_(label) {}

        void print() {
            std::cout << label_;
        }

    private:
        const std::string& label_;
};

int main() {
    auto x = new Foo("Hello World");
    x->print();
}

我得到

Hello World!

当我运行它时。如果我这样修改它:

// g++ -o test test.cpp -std=c++17
#include <iostream>

class Base {
    public:
        Base(const std::string& label) : label_(label) {}

        void print() {
            std::cout << label_;
        }

    private:
        const std::string& label_;
};

class Derived : public Base {
    public:
        Derived(const std::string& label) : Base(label) {}
};

int main() {
    auto x = new Derived("Hello World");
    x->print();
}

我仍然得到:

Hello World

但是如果我这样修改它:

// g++ -o test test.cpp -std=c++17
#include <iostream>

class Base {
    public:
        Base(const std::string& label) : label_(label) {}

        void print() {
            std::cout << label_;
        }

    private:
        const std::string& label_;
};

class Derived : public Base {
    public:
        Derived() : Base("Hello World") {}
};

int main() {
    auto x = new Derived();
    x->print();
}

我没有得到任何输出。谁能向我解释一下?我是这样编译程序的:

g++ -o test test.cpp -std=c++17

如果它有所作为,这是在 Mac 上。

C++ 引用传递 stdstring 按值传递 const-reference

评论

14赞 tkausl 7/29/2023
您正在存储对临时对象的引用。
6赞 Eljay 7/29/2023
尝试存储对非临时对象的引用。或者更好的是,只需存储对象的副本。观察到的行为都是由于未定义的行为造成的——任何观察到的行为都是允许的。
6赞 Jerry Coffin 7/29/2023
不过,这种情况不仅仅是存储对临时对象的引用。将引用绑定到临时对象通常会将临时对象的生存期延长到引用的生存期。声称这有UB需要解释为什么在这种情况下不应该发生这种情况。在我看来,这是一个完全合理的问题。const
1赞 dalfaB 7/29/2023
这 3 种情况都是未定义的行为。调用函数时,临时构造的引用对象不再存在,奇怪的是,在前 2 种情况下,已经销毁的对象在堆栈中留下了可用标记,而不是在最后一种情况下。const std::string("Hello world")print()
1赞 Paul Sanders 7/29/2023
我希望你不会在生产代码中快速而松散地使用这样的引用。它给你带来的痛苦真的不值得。

答:

3赞 anastaciu 7/29/2023 #1

这三段代码都是不正确的,只是指向临时对象的指针,作为临时对象,你不能保证字符串仍然在 时所指的位置。label_std::string"Hello World"label_x->print()

如果我们使用优化,编译器会发出悬空的引用警告,奇怪的是,只有这样它才会意识到问题。

在 gcc 13.2 中使用编译器标志:-Wall -Wextra -O3

https://godbolt.org/z/9xjsxhrTT

推测,也许是临时性在声明对象的地方,因此在作用域内,尽管是一个参数,但它可以活得足够长。在第三种情况下,临时直接传递给基构造函数,因此它可能会在 之前被丢弃。,在行动发生的地方,不知道临时的。mainx->print()main

来自 Java 或 C#,其中除了原始类型之外的所有内容都是通过引用传递的,无需担心,这可能会引起一些混淆,事实是 C++ 并非如此,程序员有责任选择,引用类成员不会保存外部引用数据,如果是暂时的,一旦程序认为适合其内存管理,它就会消失。在这种情况下,如评论部分所述,您应该按值传递数据,而不是通过引用,是 的所有者,它是应该存储它的位置。label_Foo

评论

1赞 Angelicos Phosphoros 7/29/2023
>也许临时在主作用域的事实允许它活得足够长,我敢打赌它的旧内存仍然包含旧值(没有被覆盖),但它应该是死的,因为语句结束了。
0赞 morpheus 7/29/2023
如果这 3 个都不正确,那么在 C++ 中将字符串文字传递给函数的正确方法是什么?
1赞 anastaciu 7/29/2023
@morpheus,这不是它的传递方式,而是它的存储方式,label_属于谁?不是上课吗?那么它根本不应该是一个引用,它应该只是,相应地更改构造函数参数类型,然后数据存储在它所属的位置。const std::string _label
0赞 HTNW 7/31/2023
@morpheus 一般来说,对象内部的引用是一个非常糟糕的主意,除非你非常清楚你在做什么以及为什么需要它们。
2赞 Jerry Coffin 7/29/2023 #2

在“正常”情况下,将引用绑定到临时引用会将临时引用的生存期延长到引用的生存期。例如,考虑如下代码:const

std::string foo() { return "Hello World"; }

void bar() {
    std::string const& extended_life = foo();
    std::cout << extended_life << "\n";
}

返回的 by 是一个临时对象,其生存期通常会在创建它的完整表达式(语句)的末尾过期。stringfooreturn

但是,由于我们将其绑定到引用,因此其生存期将延长到引用的生存期,因此在打印出时,行为是完全定义的。constbar

但是,当所涉及的引用是类的成员时,这并不适用。该标准没有直接解释为什么会这样,但我怀疑这主要是实现的难易问题。

如果我有类似的东西,编译器必须“知道”的声明,并从中获取其返回类型。它还直接“知道”这是对 的引用,因此返回的内容与生存期延长之间的联系相当直接和直接。Foo const &foo = bar();bar()fooconst

但是,当您在类中内部存储某些内容时,编译器(至少可能)无法访问该类的内部。例如,在第三种情况下,编译器可以编译时只看到这么多关于和:mainBaseDerived

class Base {
    public:
        Base(const std::string& label);
        void print();
    private:
        const std::string& label_;
};

class Derived : public Base {
    public:
        Derived();
};

基于此,编译器无法知道传递给 ctor 的字符串是否以任何方式与 .label_print()label_

只有通过分析通过类内容的数据流(在编译调用代码时可能不可用),它才能弄清楚存储的内容或使用方式。要求编译器在代码可能不可用时对其进行分析将导致无法实现的语言。即使所有的源代码都可用,这种关系也可能非常复杂,在某些时候,编译器将不再能够确定正在发生的事情并弄清楚它需要做什么。label_