返回std::tie-悬空引用?

8

关于从函数返回std::tie的问题。 如果我理解正确,那么std::tie只包含引用。因此,返回指向函数局部变量的std::tie是一个非常糟糕的主意。编译器不应该能够检测到这一点并发出警告吗?

实际上,我们在代码中遇到了这个错误,我们拥有的所有编译器和sanitizer都未能检测到它。我很困惑为什么没有任何工具报告这个问题。还是我理解错了什么?

#include <tuple>

struct s_t {
    int a;
};

int& foo(s_t s) {
    return s.a; // warning: reference to local variable 's' returned
}

int& bar(s_t &s) {
    return s.a; // ok
}

auto bad(s_t s) {
    return std::tie(s.a); // no warning
}

auto fine(s_t &s) {
    return std::tie(s.a); // no warning
}

int main() {

    s_t s1,s2;

    auto bad_references = bad(s1);
    auto good_references = fine(s2);
    // ...

    return 0;
}

很可能是因为构造函数调用“清理”后,工具错过了初始化表达式。 - Swift - Friday Pie
编译器不应该能够检测到这个问题并发出警告吗?是的,编译器不一定能够检测到这个问题并发出警告。编译器进行浅层静态分析,否则编译时间和/或内存占用会受到影响。标准不要求诊断;编译器警告不一定全面(深入)。 - Eljay
2个回答

5

您对生命周期行为的理解是正确的。std::tie 仅存储引用,您必须确保在所引用的对象被销毁后不再使用它们。按值传递的函数参数会在函数调用结束或包含函数调用的完整表达式结束时被销毁(实现定义)。因此,使用存储在 bad_references 中的引用将导致未定义的行为。

您可能只是期望编译器警告功能和代码检查工具提供的功能太多了。它们通常不会对代码进行全面的分析。这里的分析需要跟踪引用经过多个函数调用层、成员变量的存储和函数返回。这种更复杂的分析正是静态分析器的任务。

然而,从版本12.1开始,GCC似乎使用内联函数调用的结果来报告 -O2 -Wall -Wextra 选项:

In file included from <source>:1:
In constructor 'constexpr std::_Head_base<_Idx, _Head, false>::_Head_base(const _Head&) [with long unsigned int _Idx = 0; _Head = int&]',
    inlined from 'constexpr std::_Tuple_impl<_Idx, _Head>::_Tuple_impl(const _Head&) [with long unsigned int _Idx = 0; _Head = int&]' at /opt/compiler-explorer/gcc-12.1.0/include/c++/12.1.0/tuple:435:21,
    inlined from 'constexpr std::tuple< <template-parameter-1-1> >::tuple(const _Elements& ...) [with bool _NotEmpty = true; typename std::enable_if<_TCC<_Dummy>::__is_implicitly_constructible<const _Elements& ...>(), bool>::type <anonymous> = true; _Elements = {int&}]' at /opt/compiler-explorer/gcc-12.1.0/include/c++/12.1.0/tuple:729:28,
    inlined from 'constexpr std::tuple<_Elements& ...> std::tie(_Elements& ...) [with _Elements = {int}]' at /opt/compiler-explorer/gcc-12.1.0/include/c++/12.1.0/tuple:1745:44,
    inlined from 'auto bad(s_t)' at <source>:16:24:
/opt/compiler-explorer/gcc-12.1.0/include/c++/12.1.0/tuple:193:9: warning: storing the address of local variable 's' in '*(std::_Head_base<0, int&, false>*)<return-value>.std::_Head_base<0, int&, false>::_M_head_impl' [-Wdangling-pointer=]
  193 |       : _M_head_impl(__h) { }
      |         ^~~~~~~~~~~~~~~~~
<source>: In function 'auto bad(s_t)':
<source>:15:14: note: 's' declared here
   15 | auto bad(s_t s) {
      |          ~~~~^
<source>:15:14: note: '<unknown>' declared here

虽然我没有设法让当前版本的Clang和MSVC产生诊断信息,但我猜这对于GCC而言也只有在所有相关函数调用都被内联时才会起作用。例如,使用-O0时,GCC不会产生警告,如果存在更多复杂的函数调用层,则可能也不会产生警告。

像clang-analyzer这样的静态分析器会报告

<source>:16:5: warning: Address of stack memory associated with local variable 's' is still referred to by the stack variable 'bad_references' upon returning to the caller.  This will be a dangling reference [clang-analyzer-core.StackAddressEscape]
    return std::tie(s.a); // no warning
    ^
<source>:27:27: note: Calling 'bad'
    auto bad_references = bad(s1);

请参阅https://godbolt.org/z/zE8Px8vT9,其中包含相关信息。

使用Clang trunk且未启用优化时,ASAN也会报告问题:

=================================================================
==1==ERROR: AddressSanitizer: stack-use-after-return on address 0x7fddb5e00020 at pc 0x55678be240c4 bp 0x7ffe4ed87ef0 sp 0x7ffe4ed87ee8
READ of size 4 at 0x7fddb5e00020 thread T0
    #0 0x55678be240c3 in main /app/example.cpp:31:12
    #1 0x7fddb849e0b2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x240b2) (BuildId: 9fdb74e7b217d06c93172a8243f8547f947ee6d1)
    #2 0x55678bd6231d in _start (/app/output.s+0x2131d)

Address 0x7fddb5e00020 is located in stack of thread T0 at offset 32 in frame
    #0 0x55678be23d2f in bad(s_t) /app/example.cpp:15

  This frame has 1 object(s):
    [32, 36) 's' <== Memory access at offset 32 is inside this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
      (longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-use-after-return /app/example.cpp:31:12 in main

[...]

请查看https://godbolt.org/z/eYo7cWeaM

可能是内联导致Sanitizer难以检测到此问题。他们可能不会在内联之前添加检查。


1
你需要了解的是 RAII(资源获取即初始化)——有人说这是在 C++ 中最重要的需要知道的事情之一。在你的示例中: foo(s_t s); 会通过复制构造函数初始化 s。当 foo 退出时,s 将被销毁,因此 s.a 引用一个悬空对象。 bar(s_t &s) 是可以的,因为 &s 是引用到已存在的变量(通常比函数调用存在更长时间)。 bad(s_t s)fine(s_t &s) 是可以的,因为它们返回的是值而不是指向本地变量的引用/指针。请注意保留 HTML 标签。

在第二种情况下,根据定义,它必须(保证)在函数使用期间存在。不确定“坏”到什么程度是可以接受的,s 可能会在函数退出之前停止存在。这是实现定义的,发生了什么。 - Swift - Friday Pie
1
"bad(s_t s)" 不行。它和 "foo(s_t s)" 有同样的问题。 - user17732522

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