C++ 模板重载问题请教

2018-11-22 17:15:24 +08:00
 hackpro

原文解释是说 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
}
3234 次点击
所在节点    C++
25 条回复
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
2018-11-22 17:40:07 +08:00
指针の引用
hackpro
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
2018-11-22 18:05:08 +08:00
大概是某个右值 char const * 被返回成 char const *const & 了。

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

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

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


………… 决定放弃完全搞明白到底发生了什么
hackpro
2018-11-22 19:09:43 +08:00
@shylockhg 非常感谢,是我的理解错了 以为里面那层 max 重载会去调用模板函数
不过如果按照您的修改 把 char const *改成 char const *&之后应该就不存在这种问题吧……
zwhfly
2018-11-22 19:32:17 +08:00
@shylockhg 嘿嘿,难道不应该是:
char const * const & max(char const * const & a, char const * const & b)
HHehr0ow
2018-11-22 23:39:52 +08:00
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
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
2018-11-23 09:28:55 +08:00
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
2018-11-23 09:50:13 +08:00
@coordinate #12 感谢回复
也就是说如果 C++函数存在返回值 在实现上这个返回值会按照类似 class this 指针的方式写进函数原型?
ccpp132
2018-11-23 10:59:43 +08:00
ABI 是和平台编译器都有一定关系的。返回 int 的话基本上都是通过 eax 或 rax 寄存器,只有 size 比较大的 class 才会传指针
hackpro
2018-11-23 11:05:57 +08:00
@ccpp132 #14 多谢告知

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

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

无论哪个想要完全弄懂都基本不可能,文档和资料都少得可怜
GeruzoniAnsasu
2018-11-23 11:28:05 +08:00
wutiantong
2018-11-23 11:33:59 +08:00
@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 语义)

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/510475

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX