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

C++ 模板重载问题请教

  •  
  •   hackpro · 2018-11-22 17:15:24 +08:00 · 3285 次点击
    这是一个创建于 2198 天前的主题,其中的信息可能已经有所发展或是发生改变。

    原文解释是说 c-strings 这种重载返回了一个局部变量的引用,便随着 stack unwinding 被回收了。

    但是没有明白为什么这段代码会产生一个局部变量的引用,恳请 v 站各位大侠帮忙指点,多谢~

    return max (max(a,b), c); becomes a run-time error because for C-strings, max(a,b) creates a new, temporary local value that is returned by reference, but that temporary value expires as soon as the return statement is complete, leaving main() with a dangling reference. Unfortunately, the error is quite subtle and may not manifest itself in all cases.

    Note, in contrast, that the first call to max() in main() doesn ’ t suffer from the same issue. There temporaries are created for the arguments (7, 42, and 68), but those temporaries are created in main() where they persist until the statement is done.

    代码如下

    #include <cstring>
    
    // maximum of two values of any type (call-by-reference)
    template<typename T>
    T const& max (T const& a, T const& b)
    {
    	return b < a ? a : b;
    }
    
    // maximum of two C-strings (call-by-value)
    char const* max (char const* a, char const* b)
    {
    	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 ()
    {
    	auto m1 = ::max(7, 42, 68); // OK
    	char const* s1 = "frederic";
    	char const* s2 = "anica";
    	char const* s3 = "lucas";
    	auto m2 = ::max(s1, s2, s3); //run-time ERROR
    }
    
    25 条回复    2018-11-23 15:17:17 +08:00
    shylockhg
        1
    shylockhg  
       2018-11-22 17:37:51 +08:00
    #include <cstring>

    // maximum of two values of any type (call-by-reference)
    template<typename T>
    T const& max (T const& a, T const& b)
    {
    return b < a ? a : b;
    }

    // maximum of two C-strings (call-by-value)
    char const*& max (char const*& a, char const*& b)
    {
    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 ()
    {
    auto m1 = ::max(7, 42, 68); // OK
    char const* s1 = "frederic";
    char const* s2 = "anica";
    char const* s3 = "lucas";
    auto m2 = ::max(s1, s2, s3); //run-time ERROR
    }
    shylockhg
        2
    shylockhg  
       2018-11-22 17:40:07 +08:00
    指针の引用
    hackpro
        3
    hackpro  
    OP
       2018-11-22 18:04:32 +08:00
    @shylockhg #1 感谢大佬回复

    您的意思是:
    1、调用::max(s1,s2,s3) a,b,c 会被推导成 char const * & 引用类型
    2、这时候由于非模板函数和模板函数重载,由于 char const *& 区别于 char const * 所以会选择模板函数
    3、这时 a,b 被推导成 char const *&
    以上这三步有问题吗?
    yulon
        4
    yulon  
       2018-11-22 18:05:08 +08:00
    大概是某个右值 char const * 被返回成 char const *const & 了。

    你这么写代码是要被 linus 打死的你知道吗,这段代码里虽然局部变量的引用没出事,但这习惯也够危险了,劝你没把握别直接返回引用类型,真想返回引用请用隔壁 C# 吧_(:з」∠)_
    shylockhg
        5
    shylockhg  
       2018-11-22 18:15:51 +08:00   ❤️ 1
    @hackpro
    没听懂你在说啥,char const* max (char const* a, char const* b)返回值,T const& max (T const& a, T const& b, T const& c)返回引用,后面的函数返回了前面函数返回的值(栈变量)的引用
    coordinate
        6
    coordinate  
       2018-11-22 18:23:31 +08:00
    `max(max(a,b), c)`内部的`max`会产生临时对象,你引用一个临时对象,当然不会有什么好的结果。
    GeruzoniAnsasu
        7
    GeruzoniAnsasu  
       2018-11-22 19:04:20 +08:00
    这个例子实在是太混沌邪恶了。。。
    我简单改了改尝试了一下各种衍生

    发现 return max (max(a,b), c); 包一层 std::move,既没有编译器警告运行起来也完全不会有问题

    所以这个例子深究起来后边还有大坑


    ………… 决定放弃完全搞明白到底发生了什么
    hackpro
        8
    hackpro  
    OP
       2018-11-22 19:09:43 +08:00
    @shylockhg 非常感谢,是我的理解错了 以为里面那层 max 重载会去调用模板函数
    不过如果按照您的修改 把 char const *改成 char const *&之后应该就不存在这种问题吧……
    zwhfly
        9
    zwhfly  
       2018-11-22 19:32:17 +08:00
    @shylockhg 嘿嘿,难道不应该是:
    char const * const & max(char const * const & a, char const * const & b)
    HHehr0ow
        10
    HHehr0ow  
       2018-11-22 23:39:52 +08:00   ❤️ 1
    main() 里面,
    ```
    auto m2 = ::max(s1, s2, s3); //run-time ERROR
    ```
    这句会进入第 3 个 function template
    ```
    // 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
    }
    ```
    这句又进入了第 2 个 function template
    ```
    // maximum of two C-strings (call-by-value)
    char const* max (char const* a, char const* b)
    {
    return std::strcmp(b,a) < 0 ? a : b;
    }
    ```
    此时,由于返回类型是 char const*,一个指针 variable,不论 a/b 哪个更大,都会返回一个 variable,类型是 char const*,值是 a/b 中 strcmp 较大的那个指向的地址。这个 variable 就是所谓的 temporary variable。

    类似于
    ```
    T a = foo();
    ```
    foo() evaluate 完之后,所有 foo() 中的变量 life cycle 都结束了,那 assignment 要拿谁做等号右边的 variable ?这种情况就会产生一个 temporary variable 用来临时存放返回值,等 assignment 结束后,temporary variable 的 life cycle 也结束了。当然,实际代码中 temporary variable 可能被 RVO 优化掉,更或者被 C++17 的 copy elision 处理掉。这里不展开了。

    第 2 个 function template return 后,回到第 3 个 function template,此时,等价于
    ```
    return max(temporary_variable, c);
    ```
    这里会再进一次第 2 个 function template,返回后等价于
    ```
    return temporary_variable_2;
    ```
    然而,第 3 个 function template 返回的类型是 T const&,也就是返回了 temporary variable 的引用,一直传递到了 main 里面,而这个 temporary variable 的 life cycle 也就到第 3 个 function template 结束而已。对 temporary variable 的使用超过的它的 life cycle,是一种 run time error。

    此时会不会 crash 就是 UB 了,一般编译器不会做类似 variable life cycle 一结束就清除它的内存之类激进的事情,所以 temporary variable 的内存地址里“可能”暂时还会是它原本的内容( UB ),将这些字节解释回变量的内容也“可能”得到原来变量的值( UB again )。并且
    ```
    auto m2 = ::max(s1, s2, s3);
    ```
    这里,auto 会得到 decay 的类型,去掉了引用,因此只要这个 temporary variable “曾经”所在的内存能撑过这句,就能得到原本的变量值。


    ```
    auto m1 = ::max(7, 42, 68); // OK
    ```
    没问题的原因是它从头到尾就不会进第 2 个 function template,始终是引用飞来飞去,引用的就是 7/42/68 这三个 integer literal 产生的 temporary variable,life cycle 是到该语句结束,然后被 auto 得到 decay 的类型后 copy 一份。不属于 UB。

    港真,好好写人能读懂的代码,不要乱飞这些乱七八糟的类型更重要。
    hackpro
        11
    hackpro  
    OP
       2018-11-23 00:26:59 +08:00
    @HHehr0ow 好详细的回复,您辛苦了!
    不过对于函数调用的参数拷贝,我一直有些疑惑,还望解答。

    int f(int x)
    {
    return x+1;
    }

    int main()
    {
    int a = 0;
    int b = f(a);
    }

    如果不考虑优化的话,参数总共被拷贝了两次?
    1st: a -> x
    2nd: x+1 -> b ?
    还是说 x+1 的值被放在的某个返回值位置,然后这个返回值再赋值给 a ?

    另外,这个返回值在函数的堆栈里到底是怎么存储的,有这方面的博客推荐吗,多谢!
    coordinate
        12
    coordinate  
       2018-11-23 09:28:55 +08:00   ❤️ 1
    f 函数在编译器中可能会变成这样
    void f(X& __result, int x)
    {
    __result.X::XX(x+1);
    return;
    }
    而 b = f(a)会变成
    int b;
    f(b, a);
    以上操作成为 named return value
    hackpro
        13
    hackpro  
    OP
       2018-11-23 09:50:13 +08:00
    @coordinate #12 感谢回复
    也就是说如果 C++函数存在返回值 在实现上这个返回值会按照类似 class this 指针的方式写进函数原型?
    ccpp132
        14
    ccpp132  
       2018-11-23 10:59:43 +08:00
    ABI 是和平台编译器都有一定关系的。返回 int 的话基本上都是通过 eax 或 rax 寄存器,只有 size 比较大的 class 才会传指针
    hackpro
        15
    hackpro  
    OP
       2018-11-23 11:05:57 +08:00
    @ccpp132 #14 多谢告知

    @ccpp132 @coordinate
    另外有一点不是很理解的是,调用另外一个函数的时候压栈顺序是:
    1、参数
    2、返回值地址
    3、局部变量

    如果是这样的话,函数返回 stack unwinding 的时候参数是怎么销毁的呢?
    arzterk
        16
    arzterk  
       2018-11-23 11:07:09 +08:00
    @hackpro 哪有那么麻烦,返回值如果是 int,一般直接放在寄存器[eax]里面的,外面调用函数直接出栈了读寄存器就可以了,如果是复杂的对象入参或者返回值,现代编译器也能优化掉复制构造,一般人也不用太关心这个
    arzterk
        17
    arzterk  
       2018-11-23 11:11:54 +08:00   ❤️ 1
    @hackpro 要搞清楚这些幺蛾子,必读<C++对象模型>
    GeruzoniAnsasu
        18
    GeruzoniAnsasu  
       2018-11-23 11:20:17 +08:00
    @hackpro stack unwinding 需要借助其它 section 保存的 unwind 信息,运行时栈上的内容是绝不足以支撑 unwind 的,然而如何保存 unwind 信息没有被标准定义,Linux 和 win 上的实现又是完全不同的,windows 通过 SEH,linux 则是使用类似 dwarf2 的调试信息并保存到.eh_frame 节

    无论哪个想要完全弄懂都基本不可能,文档和资料都少得可怜
    wutiantong
        20
    wutiantong  
       2018-11-23 11:33:59 +08:00   ❤️ 1
    @hackpro
    不考虑编译器优化的话,int b = f(a); 这句意味着:
    1. ‘ x ’ constructed from ‘ a ’ (copy 语义)
    2. 'return value of f(a)' constructed from expression '(x+1)' (move 语义)
    3. 'b' constructed from 'return value of f(a)' (move 语义)
    wutiantong
        21
    wutiantong  
       2018-11-23 11:37:42 +08:00
    正如上面各位大佬所说,原题代码的一个可行的修改方案是把第二个 max 的函数声明改为:

    char const* const& max (char const* const& a, char const* const& b)

    (虽然这种代码其实很蠢,逃?
    hackpro
        22
    hackpro  
    OP
       2018-11-23 12:07:45 +08:00
    @arzterk #16
    其实我想问的是如果编译器不进行优化的话 被调函数的参数 /返回值是怎么和原调函数进行数据交换的
    PS: 非常感谢您推荐的书

    @GeruzoniAnsasu #18
    感谢大佬

    @wutiantong #20
    非常感谢您的回复
    按照您说的如果编译器不进行优化的话 第二步返回值由 x+1 构建 这个返回值是存在被调函数的栈帧中吗
    另外对于一般的函数调用如果不考虑编译器优化的话,参数 /返回地址 /返回值在栈帧中的布局大概是什么样的
    我从参考链接中了解到的顺序为:参数 /返回地址 (请见参考链接第 13 页)
    https://www.cs.bham.ac.uk/~hxt/2015/c-plus-plus/stack.pdf
    wutiantong
        23
    wutiantong  
       2018-11-23 13:21:47 +08:00   ❤️ 1
    @hackpro

    其实 C++的*标准*并不涉及内存上的堆栈问题(包括寄存器),*标准*不关注这些东西,需要关注这些东西的是具有不同*实现*的编译器。
    但是*标准*确实会关注对象的生命周期,在*实现*中对象的生命周期与堆栈之间有紧密的关联。

    “这个返回值是存在被调函数的栈帧中吗?” 答:这个返回值是一个临时变量。

    换个角度来说,我写 C++好几年了,我只关注*标准*定义了哪些概念哪些行为,这对我而言就足够了。
    我不会特意去关注具体*实现*的内部细节,除非它产生的行为与*标准*产生了偏差。

    所以我也不关心“函数调用时栈帧的实际布局是什么样子的”,这个问题对我而言既无意义也无帮助。
    wutiantong
        24
    wutiantong  
       2018-11-23 13:23:34 +08:00
    @hackpro 关于生命周期和临时变量,请参考: https://en.cppreference.com/w/cpp/language/lifetime
    hackpro
        25
    hackpro  
    OP
       2018-11-23 15:17:17 +08:00 via iPhone
    @wutiantong 非常感谢大佬的指点
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2763 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 15:40 · PVG 23:40 · LAX 07:40 · JFK 10:40
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.