V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
chuanqirenwu
V2EX  ›  C++

g++ 编译⚠️returning reference to local temporary object 但是运行依然得到预期结果?

  •  
  •   chuanqirenwu · 2022-04-04 16:35:40 +08:00 · 2052 次点击
    这是一个创建于 746 天前的主题,其中的信息可能已经有所发展或是发生改变。

    代码来自 C++ templates 第二版:

    #include <cstring>
    #include <iostream>
    
    using std::cout;
    using std::endl;
    
    // maximum of two values of any type (call-by-reference)
    template <typename T>
    T const &max(T const &a, T const &b)
    {
        cout << "T const &max(T const &a, T const &b) called" << endl;
        return b < a ? a : b;
    }
    
    // maximum of two C-strings (call-by-value)
    char const *max(char const *a, char const *b)
    {
        cout << "char const *max(char const *a, char const *b) called" << endl;
        cout << a << endl;
        cout << b << endl;
        return std::strcmp(b, a) < 0 ? a : b;
    }
    
    // maximum of three values of any type (call-by-reference)
    template <typename T>
    T const &max(T const &a, T const &b, T const &c)
    {
        return max(max(a, b), c); // error if max(a,b) uses call-by-value
    }
    
    int main()
    {
        char const *s1 = "frederic";
        char const *s2 = "anica";
        char const *s3 = "lucas";
        auto m2 = ::max(s1, s2, s3); // run-time ERROR
        //!!! m2 已经是一个 dangling reference ,但是仍然得到预期结果?
        cout << m2 << endl;
    }
    

    不理解的地方在最后一行,m2 已经是一个 dangling reference ,但是为什么输出仍然得到预期结果?

    编译使用的是 g++:

    $ g++ --version
    Configured with: --prefix=/Library/Developer/CommandLineTools/usr --with-gxx-include-dir=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/4.2.1
    Apple clang version 12.0.0 (clang-1200.0.31.1)
    Target: x86_64-apple-darwin21.1.0
    Thread model: posix
    InstalledDir: /Library/Developer/CommandLineTools/usr/bin
    

    编译输出:

    $ g++ test2.cpp -o test2 -std=c++11    
    test2.cpp:28:12: warning: returning reference to local temporary object [-Wreturn-stack-address]
        return max(max(a, b), c); // error if max(a,b) uses call-by-value
               ^~~~~~~~~~~~~~~~~
    test2.cpp:36:17: note: in instantiation of function template specialization 'max<const char *>' requested here
        auto m2 = ::max(s1, s2, s3); // run-time ERROR
                    ^
    1 warning generated.
    

    运行结果:

    char const *max(char const *a, char const *b) called
    frederic
    anica
    
    char const *max(char const *a, char const *b) called
    frederic
    lucas
    
    lucas
    
    14 条回复    2022-04-09 11:16:22 +08:00
    nightwitch
        1
    nightwitch  
       2022-04-04 16:38:47 +08:00
    undefined behaviour
    可能出现任意的结果
    chuanqirenwu
        2
    chuanqirenwu  
    OP
       2022-04-04 16:43:15 +08:00
    @nightwitch 意思是 g++ 下将这个 undefined behavior 定义成了最接近预期结果的 behavior 吗?从运行结果来看,就是得到的预期的输出:lucas
    nightwitch
        3
    nightwitch  
       2022-04-04 17:00:36 +08:00
    ub 的结果是不能预测的。
    也许你在多写几行或者换个编译参数他结果就变了。
    不用去深究它,也不要依赖你观察到的表现
    macrorules
        4
    macrorules  
       2022-04-04 17:04:19 +08:00
    @nightwitch 怎么看出是 UB 啊?
    nightwitch
        5
    nightwitch  
       2022-04-04 17:08:25 +08:00
    @macrorules
    https://en.cppreference.com/w/cpp/language/reference
    翻到最下面 “Accessing such a reference is undefined behavior. ”
    macrorules
        6
    macrorules  
       2022-04-04 17:08:41 +08:00
    如果你把 lucas 改成 eucas ,就会报错,因为 max(a, b) 的结果是一个临时变量,调用栈收缩之后就没了
    chuanqirenwu
        7
    chuanqirenwu  
    OP
       2022-04-04 17:21:50 +08:00
    @macrorules max(max(a, b), c) 整个返回应该都是临时变量,因此跟参数的值应该没有关系。
    icylogic
        8
    icylogic  
       2022-04-04 18:19:01 +08:00
    https://godbolt.org/z/PbMf5Ps8K

    a temporary bound to a return value of a function in a return statement is not extended: it is destroyed immediately at the end of the return expression. Such return statement always returns a dangling reference.
    yanqiyu
        9
    yanqiyu  
       2022-04-04 18:43:51 +08:00   ❤️ 1
    就算返回了临时变量的引用,但是也不意味着临时变量引用对应的地址会挪作他用,一般来说紧接着就用了而未进行压栈也大概率展示用不到
    phiysng
        10
    phiysng  
       2022-04-04 23:13:40 +08:00   ❤️ 1
    @chuanqirenwu 未定义就是真的未定义,不存在`定义成了最接近预期结果的 behavior `
    FrankHB
        11
    FrankHB  
       2022-04-08 18:43:55 +08:00
    能运行也是 UB 的一种。
    但是应该强调,返回临时变量的引用不一定就 UB ,这里只有限定返回局部自动对象的引用时才是。否则,这会显著干扰一些问题的理解,例如:
    https://gcc.gnu.org/bugzilla/show_bug.cgi?id=67795
    FrankHB
        12
    FrankHB  
       2022-04-08 19:19:22 +08:00
    @FrankHB 修正:返回非静态存储期的临时对象的引用。
    和 @nightwitch 指出的来源一样,返回自动对象的引用是悬空引用。这里的自动对象是一个被声明的局部变量。
    但是仔细看了下,OP 的代码中并不是这种情况。
    问题出在 max(max(a, b), c)返回的是 const char*类型的右值,通过 temporary materialization conversion 初始化一个临时对象并初始化 T const&[T=const char*]类型的返回值,在 return 外临时对象被销毁,所以返回悬空引用。
    注意这不涉及 C++意义上的变量。使用 g++时,中文警告[-Wreturn-local-addr]会把它叫做“临时变量”,这是技术上错的;而原文 returning reference to temporary 是对的,虽然过时( ISO C++17 前就直接叫 temporary ,不叫 temporary object )。
    注意这里实际上用的是软链接过去的 Apple clang++,警告的选项不一样。
    因为不是被声明变量,所以说 local (指的是作用域)也是技术上错的。
    OP 的注释也是错的,悬空引用是=右边的部分;而被声明的变量 m2 就不是引用,因为用的是 auto 而不是 auto&。
    chuanqirenwu
        13
    chuanqirenwu  
    OP
       2022-04-08 22:07:10 +08:00
    @FrankHB 谢谢!还是不理解,为什么说 m2 不是引用呢?返回类型不是 `T const &` 吗?
    FrankHB
        14
    FrankHB  
       2022-04-09 11:16:22 +08:00
    @chuanqirenwu 声明的类型通过初值符的类型推断,不保证相同。和在函数模板的参数列表里写不带&的参数类型规则相同,占位符不会被推断为引用类型,于是声明的 m2 不是引用类型的变量。如果要保留引用,可以 auto&或 auto&&之类。
    http://www.eel.is/c++draft/dcl.spec.auto#dcl.type.auto.deduct-3
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   2789 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 13:25 · PVG 21:25 · LAX 06:25 · JFK 09:25
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.