簡體   English   中英

返回一個 std::tie - 懸空引用?

[英]Returning a std::tie - dangling reference?

從函數返回 std::tie 的問題。 如果我理解正確,那么 std::tie 只包含引用。 因此,返回一個指向函數局部變量的 std::tie 是一個非常糟糕的主意。 編譯器不應該能夠檢測到這一點並發出警告嗎?

實際上,我們的代碼中存在這個錯誤,並且我們錯過了檢測它的所有編譯器和消毒劑。 我很困惑,這沒有被任何工具報告。 還是我理解有誤?

#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;
}

你對終生行為的理解是正確的。 std::tie僅存儲引用,您必須確保在引用對象被銷毀后不使用它們。 按值函數參數在函數調用結束時或在包含函數調用的完整表達式結束時被銷毀(實現定義)。 所以使用存儲在bad_references中的引用會導致未定義的行為。

您可能對編譯器警告功能和 linter 功能期望過高。 他們通常不會對代碼進行廣泛的分析。 這里的分析需要通過多個函數調用層、對成員的存儲以及函數的返回來跟蹤引用。 這種更復雜的分析是靜態分析器的用途。

但是,從 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 中繼和優化的情況下,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

可能內聯使消毒劑更難檢測到這一點。 他們可能不會在內聯之前添加檢查。

自我回答這個問題你應該知道的是 RAII(資源獲取是初始化——有人說這是 C++ 中需要知道的最重要的事情)在你的例子中:

foo(s_t s); 將通過復制構造函數初始化s 當 foo 退出時, s將被銷毀,以便sa引用一個懸空對象

bar(s_t &s)可以,因為&s是對現有變量的引用(通常)比函數調用的壽命更長

bad(s_t s) fine(s_t &s)因為它是返回值而不是引用/指向局部變量的指針

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM