默认复制/移动构造函数时 GDB 中的奇怪行为

Posted

技术标签:

【中文标题】默认复制/移动构造函数时 GDB 中的奇怪行为【英文标题】:Weird behavior in GDB when defaulting copy/move constructors 【发布时间】:2018-06-29 12:28:31 【问题描述】:

我有以下代码,它在 GDB 中的行为似乎很奇怪,具体取决于复制/移动构造函数是否默认。

#include <iostream>

#define CUSTOM 0

class Percentage

public:
    using value_t = double;

    Percentage()    = default;
    ~Percentage()   = default;

    template <typename T>
    Percentage(T) = delete;

    Percentage(value_t value):
        m_value(value)
    

    #if CUSTOM == 1
    Percentage(const Percentage& p):
        m_value(p.m_value)
    

    Percentage& operator=(const Percentage& p)
    
        m_value = p.m_value;
        return *this;
    

    Percentage(Percentage&& p):
        m_value(std::move(p.m_value))
    

    Percentage& operator=(Percentage&& p)
    
        m_value = std::move(p.m_value);
        return *this;
    
    #else
    Percentage(const Percentage&) = default;
    Percentage& operator=(const Percentage&) = default;
    Percentage(Percentage&&) = default;
    Percentage& operator=(Percentage&&) = default;
    #endif

    friend std::ostream& operator<<(std::ostream& os, const Percentage& p)
    
        return os << (p.m_value * 100.0) << '%';
    
private:
    value_t m_value = 0.0;
;

struct test

    Percentage m_p;

    void set(const Percentage& v)  m_p = v; 
    Percentage get() const  return m_p; 
;

int main()

    test t;

    std::cout << "Value 1: " << t.get() << std::endl;
    t.set(42.0);
    std::cout << "Value 2: " << t.get() << std::endl;

    std::cout << "Breakpoint here" << std::endl;

我启动 GDB,在 main 的最后一个 cout 上添加一个断点并运行“p t.get()”,我希望它是 42,但取决于宏 CUSTOM 的值,我得到 42(当 CUSTOM为 1) 或 0(当 CUSTOM 为 0 时)。

发生了什么?这是编译器gdb中的错误吗?

OS: Fedora 26
Compiler: gcc 7.3.1
Flags: -fsanitize=address,leak -O0 -g3 -std=c++17
GDB 8.0.1-36

【问题讨论】:

请注意,p t 显示了对象的正确内容。 p t.get() 需要调试器执行应用代码,gdb 显然搞错了。 我怀疑问题是 p.get() 返回一个临时的。您可以尝试相同的方法,但将 get 的签名从“Percentage get() const”更改为“Percentage const & get() const”并查看报告的值是否正常? @SamVarshavchik 是吗?此处不是强制使用 RVO,即没有返回任何内容。 @Walter 我观察到与 Sam 相同的行为 被删除的模板构造函数是怎么回事??? 【参考方案1】:

一般来说,由于 test::get 的结果是一个“纯右值”,如果它没有绑定到一个左值(如百分比&&),编译器可以跳过它的初始化。因此,要查看纯右值的内容,您应该将其存储在 Percentage&& 变量中,该变量“物化”纯右值并延长临时返回的生命周期。

这两种情况的区别似乎存在于可执行文件中(与GDB无关):从两种情况下的可执行文件的反汇编中可以看出“test::get”不同,而如果我们用优化编译在 (-O3) 上生成的程序集是相同的。

案例 0:

    Percentage get()  return m_p; 
  4009f0:       55                      push   %rbp
  4009f1:       48 89 e5                mov    %rsp,%rbp
  4009f4:       48 89 7d f0             mov    %rdi,-0x10(%rbp)
  4009f8:       48 8b 7d f0             mov    -0x10(%rbp),%rdi
  4009fc:       48 8b 3f                mov    (%rdi),%rdi
  4009ff:       48 89 7d f8             mov    %rdi,-0x8(%rbp)
  400a03:       f2 0f 10 45 f8          movsd  -0x8(%rbp),%xmm0
  400a08:       5d                      pop    %rbp
  400a09:       c3                      retq   
  400a0a:       66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)

案例一:

    Percentage get()  return m_p; 
  4009f0:       55                      push   %rbp
  4009f1:       48 89 e5                mov    %rsp,%rbp
  4009f4:       48 83 ec 10             sub    $0x10,%rsp
  4009f8:       48 89 f8                mov    %rdi,%rax
  4009fb:       48 89 75 f8             mov    %rsi,-0x8(%rbp)
  4009ff:       48 8b 75 f8             mov    -0x8(%rbp),%rsi
  400a03:       48 89 45 f0             mov    %rax,-0x10(%rbp)
  400a07:       e8 54 00 00 00          callq  400a60 <_ZN10PercentageC2ERKS_>
  400a0c:       48 8b 45 f0             mov    -0x10(%rbp),%rax
  400a10:       48 83 c4 10             add    $0x10,%rsp
  400a14:       5d                      pop    %rbp
  400a15:       c3                      retq   
  400a16:       66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  400a1d:       00 00 00 

在这种情况下,由于您从 GDB 调用 test::get,因此您不仅仅是在内存中打印一个值,而是在执行上面的行。您会看到对 Percentage 的复制构造函数的调用,并且 test::get 的返回似乎在 rax 寄存器中,而在第一个 sn-p 中似乎内联了隐式构造函数,并存储了返回值在浮点寄存器 xmm0 中。 我不知道为什么会有这种差异(也许汇编专家可以补充一些见解),但我怀疑这就是 GDB 感到困惑的原因。

【讨论】:

在我看来 gcc 实际上使用的 ABI 略有不同,具体取决于编译的优化级别。因此,使用不同优化标志编译的不同翻译单元将与 ABI 不兼容。 我相信编译器必须坚持使用特定的 ABI 来导出库中的符号。对于单个项目中的编译单元,它可以随心所欲。例如,它可能会忽略调用约定。

以上是关于默认复制/移动构造函数时 GDB 中的奇怪行为的主要内容,如果未能解决你的问题,请参考以下文章

按值传递参数时的奇怪行为

使用 requires 的可选非平凡析构函数

避免调用默认、移动和复制构造函数

父类的复制构造函数中的嵌套类缺少默认构造函数

如何在不破坏移动和复制构造函数的情况下声明虚拟析构函数

了解 Lambda 闭包类型如何删除默认构造函数