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

求教个神奇的 C++ 打印问题

  •  
  •   Betsy · 107 天前 · 2532 次点击
    这是一个创建于 107 天前的主题,其中的信息可能已经有所发展或是发生改变。

    代码

    #include <iostream>
    #include <vector>
    #include <cstdint>
    #include <optional>
    
    using GroupId = std::uint64_t;
    using ReducedGroupId = GroupId;
    
    struct Memo {
        std::optional<ReducedGroupId> GetReduceGroupId(const GroupId& group_id) {
            // omit
            return std::make_optional<ReducedGroupId>(group_id);
        }
    };
    
    int main(int argc, char* argv[]) {
        std::vector<GroupId> tmp;
    
        Memo memo;
        GroupId group_id = 1;
        const ReducedGroupId& reduced_group_id = memo.GetReduceGroupId(group_id).value();
        std::cout << "3.1.->|" << reduced_group_id << ":" << &reduced_group_id << std::endl;
    
        tmp.push_back(4);
        std::cout << "3.2.->|" << reduced_group_id << ":" << &reduced_group_id << std::endl;
    
        tmp.push_back(5);
        std::cout << "3.3.->|" << reduced_group_id << ":" << &reduced_group_id << std::endl;
        return 0;
    }
    
    

    结果

    3.1.->|1:0x7ffe4fcd3530
    3.2.->|4:0x7ffe4fcd3530
    3.3.->|5:0x7ffe4fcd3530
    

    问题

    • 3.1, 3.2, 3.3 为啥打印结果不一样?
    29 条回复    2024-08-07 10:02:45 +08:00
    zhouxiaoyuan
        1
    zhouxiaoyuan  
       107 天前 via Android
    不是 c++神奇,是没管理好对象的生命周期,不能引用临时变量 GetReduceGroupId 。

    const ReducedGroupId& reduced_group_id = memo.GetReduceGroupId(group_id).value();
    Betsy
        2
    Betsy  
    OP
       107 天前 via iPhone
    @zhouxiaoyuan GetReduceGroupId() 返回值不用 optional 修饰,却又没问题。
    Donaldo
        3
    Donaldo  
       107 天前   ❤️ 2
    ```c++
    const ReducedGroupId& reduced_group_id = memo.GetReduceGroupId(group_id).value();
    ```
    华点在这一行,memo.GetReduceGroupId(group_id)是个临时的右值,你取完就悬垂了,所以这个这时候他的值是个 UB 。具体是什么全看编译器实现:
    - Apple M2 aarch64+ clang16: 1, 1, 1
    - Windows x86 + msvc14: 1, 1, 1
    - Linux x86 + gcc14: 1, 4, 5

    想要避免 UB ,多加一行把这个临时值存起来就好了
    ```c++
    auto rgi = memo.GetReduceGroupId(group_id);
    const ReducedGroupId& reduced_group_id = rgi.value();
    ```
    这里 auto 类型可以是 std::optional<ReducedGroupId>也可以是它的右值引用 std::optional<ReducedGroupId> &&。
    chrisyunhua
        4
    chrisyunhua  
       107 天前 via Android
    去掉 & 改为 `const ReducedGroupId id = ...` 也可以。

    @Betsy 不用 optional 没问题,我的理解是:因为 GetReducedGroupId() 属于 prvalue ,赋给 const & 时触发了 lifetime extension ;而 optional.value() 返回类型是 const & 属于 lvalue ,不触发 lifetime extension 。

    https://en.cppreference.com/w/cpp/language/lifetime
    noahlias
        5
    noahlias  
       107 天前
    我怎么觉得是你的编译器问题 或者编译 option 的问题

    https://godbolt.org/z/4b6x9WKaE

    我看这里返回结果是一样的
    ```
    ASM generation compiler returned: 0
    Execution build compiler returned: 0
    Program returned: 0
    3.1.->|1:0x7ffdce1d3300
    3.2.->|1:0x7ffdce1d3300
    3.3.->|1:0x7ffdce1d3300
    ```
    blinue
        6
    blinue  
       107 天前   ❤️ 1
    未定义行为就是编译器怎么做都可以,有一篇很好的博客 https://devblogs.microsoft.com/oldnewthing/20140627-00/?p=633
    bfjm
        7
    bfjm  
       107 天前 via iPhone
    @Betsy 不用 optional 修饰 没问题 应该是提升了返回值的生命周期了(const)
    bfjm
        8
    bfjm  
       107 天前 via iPhone
    这里应该主要的问题是 optional 被析构了,你还在拿他的一个成员变量的引用,所以出现了垂悬引用,这里的解决方案可以参考前面几楼的答案,还可以提升 optional 的返回值的生命周期
    bfjm
        9
    bfjm  
       107 天前 via iPhone
    @bfjm 去掉.value()
    InkStone
        10
    InkStone  
       107 天前
    试了一下编译时候有 warning 的……都不用开-Wall

    test.cpp:21:46: warning: object backing the pointer will be destroyed at the end of the full-expression [-Wdangling-gsl]
    const ReducedGroupId& reduced_group_id = memo.GetReduceGroupId(group_id).value();
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    写 C++的时候-Wall -Werror 是个好习惯
    rabbbit
        11
    rabbbit  
       107 天前
    visual studio 输出的都是 1
    这么写行吗?
    const ReducedGroupId&& reduced_group_id = std::move(memo.GetReduceGroupId(group_id).value());
    rabbbit
        12
    rabbbit  
       107 天前
    额数值有必要用引用吗?
    fighterhit
        13
    fighterhit  
       107 天前
    人生苦短,学点别的吧
    MoYi123
        14
    MoYi123  
       107 天前
    编译有警告
    main.cpp:43:46: warning: object backing the pointer will be destroyed at the end of the full-expression [-Wdangling-gsl]
    const ReducedGroupId& reduced_group_id = memo.GetReduceGroupId(group_id).value();

    运行有 asan
    ==7760==ERROR: AddressSanitizer: stack-use-after-scope on address 0x00016ba93320 at pc 0x00010436df8c bp 0x00016ba93250 sp 0x00016ba93248
    READ of size 8 at 0x00016ba93320 thread T0
    #0 0x10436df88 in main+0x610 (a.out:arm64+0x100001f88)
    #1 0x187dbe0dc (<unknown module>)

    Address 0x00016ba93320 is located in stack of thread T0 at offset 192 in frame
    #0 0x10436d984 in main+0xc (a.out:arm64+0x100001984)

    This frame has 7 object(s):
    [32, 40) 'ref.tmp.i.i124'
    [64, 72) 'ref.tmp.i.i103'
    [96, 104) 'ref.tmp.i.i'
    [128, 152) 'tmp'
    [192, 208) 'ref.tmp' <== Memory access at offset 192 is inside this variable
    [224, 232) 'ref.tmp15'
    [256, 264) 'ref.tmp28'
    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-scope (a.out:arm64+0x100001f88) in main+0x610
    sanbuks
        15
    sanbuks  
       107 天前
    @rabbbit 没有必要,直接复制
    sanbuks
        16
    sanbuks  
       107 天前
    template< class T >constexpr std::optional<std::decay_t<T>> make_optional( T&& value );

    引用被 decay 了,直接赋值接收就行了
    araraloren
        17
    araraloren  
       107 天前
    Very cool feature, thanks for share this.
    a554340466
        18
    a554340466  
       107 天前
    读一下 C++17 The complete guide 的 optional 那一章
    a554340466
        19
    a554340466  
       107 天前
    it is always safe to assign any optional return value to a new object:
    auto a = getString().value(); // OK: copy of contained object or exception
    However, using the returned value directly (other than passing it as an argument) is a source of trouble:
    ```cpp
    auto b = *getString(); // ERROR: undefined behavior if std::nullopt
    const auto& r1 = getString().value(); // ERROR: reference to deleted contained object
    auto&& r2 = getString().value(); // ERROR: reference to deleted contained object
    ```
    The problem with the references is that by rule, they extend the lifetime of the return value of value() but
    not the lifetime of the optional object returned by getString(). Thus, r1 and r2 refer to values that no
    longer exist and using them results in undefined behavior.
    ElevenQAQ
        20
    ElevenQAQ  
       107 天前
    @Donaldo 哥们儿的解释简洁清晰
    ipwx
        21
    ipwx  
       107 天前
    额,楼主你这

    std::optional<ReducedGroupId> GetReduceGroupId(const GroupId& group_id) {
    // omit
    return std::make_optional<ReducedGroupId>(group_id);
    }

    不是取了 group_id 的地址塞到 optional 里面。optional 本来就是个完整的对象,所以你是复制了一份 group_id 塞到了 optional 里面。

    然后

    const ReducedGroupId& reduced_group_id

    取的就是这个临时的 optional 内部的 int64 的地址,当然这句话执行完就被 “销毁” 了。后面的代码都是错的。
    ipwx
        22
    ipwx  
       107 天前   ❤️ 1
    optional 类似于


    template <typename T>
    struct Optional {
    T* myObject;

    Optional() : myObject(nullptr) {}
    Optional(const T& value) : myObject(new T(value)) {}
    ~Optional() { delete myObject; }

    bool has_value() { return myObject != nullptr; }
    }
    Betsy
        23
    Betsy  
    OP
       106 天前
    @InkStone 不是哈,g++ 11.4.0 亲测没有报错的。

    ```bash
    g++ a.cc -o a.out -std=c++20 -g -Wall -Werror
    ```
    Betsy
        24
    Betsy  
    OP
       106 天前
    @a554340466 这个地址可以发我不?原始出处没找到
    Betsy
        25
    Betsy  
    OP
       106 天前
    @rabbbit 你这么写肯定是不行的。数值类型为啥没有必要引用呢?
    realJamespond
        26
    realJamespond  
       106 天前
    右值是个很抽象的东西一直没搞清楚
    rabbbit
        27
    rabbbit  
       106 天前
    摘自 C++Primer plus

    使用引用参数的主要原因有两个。
    程序员能够修改调用函数中的数据对象。
    通过传递引用而不是整个数据对象,可以提高程序的运行速度。
    当数据对象较大时(如结构和类对象),第二个原因最重要。这些
    也是使用指针参数的原因。这是有道理的,因为引用参数实际上是基于
    指针的代码的另一个接口。那么,什么时候应使用引用、什么时候应使
    用指针呢?什么时候应按值传递呢?下面是一些指导原则:
    对于使用传递的值而不作修改的函数。
    如果数据对象很小,如内置数据类型或小型结构,则按值传递。

    既然 std::move 是不行的。
    根据我的理解,你是希望修改 group_id 时,跟着变动 reduced_group_id ,对吗?
    blinue
        28
    blinue  
       106 天前   ❤️ 1
    我在 godbolt 里复现这个问题: https://godbolt.org/z/h45896sM5

    只会在 O1 优化下出现,是一个悬垂引用导致的巧合。

    1. memo.GetReduceGroupId(group_id) 返回的临时的 std::optional<ReducedGroupId> 存储在 [rsp + 16] 到 [rsp + 24]。optional 本身共 9 个字节,前 8 个字节是 ReducedGroupId ,后跟一个 bool 。

    2. reduced_group_id 为 .value() 返回的地址,即 rsp + 16 ,注意这是一个栈上的临时空间,reduced_group_id 为悬垂引用。后续用 rbx 存储 reduced_group_id 引用的地址。

    3. tmp.push_back(4) 恰好将 4 ( 8 个字节)存储到 [rsp + 16],覆盖了 reduced_group_id 指向的内存。编译器认为这是安全的,因为临时的 std::optional<ReducedGroupId> 已经析构,这导致后续读取 reduced_group_id 的值为 4 。
    a554340466
        29
    a554340466  
       106 天前   ❤️ 1
    @Betsy https://dokumen.pub/c17-the-complete-guide-396730017x-9783967300178.html
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5151 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 09:16 · PVG 17:16 · LAX 01:16 · JFK 04:16
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.