为什么 struct 的析构函数是否运行取决于成员变量的类型?

Thi*_*ran 25 c++

我对 C++ 相当陌生,当我在构造函数和析构函数的行为中徘徊时,我发现了这个问题:

#include <iostream>

struct Student {
    std::string a;
    ~Student() {
        std::cout << "Destructor called\n";
    }
} S;

int main() {
    std::cout << "Before assigning to S\n";
    S = {""};
    std::cout << "After assigning to S\n";
}
Run Code Online (Sandbox Code Playgroud)

当我编译上面的代码g++并运行它时,它会打印:

Before assigning to S
Destructor called
After assigning to S
Destructor called
Run Code Online (Sandbox Code Playgroud)

但是当我更改std::string a;为 时const char *a;,它会打印:

Before assigning to S
After assigning to S
Destructor called
Run Code Online (Sandbox Code Playgroud)

谁能解释为什么这一变化使析构函数少运行一次?

pad*_*ddy 16

您正在看到Copy Elision。这是一种优化,在某些情况下将绕过临时对象的构造。

在 GCC 中,可以通过传递以下编译器标志来禁用它(尽管通常不推荐这样做):

-fno-elide-constructors
Run Code Online (Sandbox Code Playgroud)

在您的情况下,我认为优化是由于结构成为微不足道的类型而触发的,因此它被视为 POD(纯旧数据),并且在不构造对象的情况下简单地修改了内存。

显示效果的现场演示-fno-elide-constructorshttps : //godbolt.org/z/c3qrMEr9s

  • 这真的是复制初始化吗?那不是像 `Student a={2};` 这样的东西吗,而示例中的 `a` 已经声明为全局变量,只是 `a = {2};` 看起来更像是一个赋值运算符,其中复制初始化是绑定到赋值运算符引用的临时值。据说,当绑定到引用时,它不应该被忽略。 (10认同)
  • 复制省略仅适用于初始化期间,而不适用于赋值期间。对我来说这看起来像是一个 GCC 错误。 (4认同)

jac*_*k X 7

首先,在任何一种情况下,Student都是聚合。现在,我们检查相关规则S = {""};

一个大括号初始化列表可能会出现在右侧

  • 对类类型对象的赋值,在这种情况下,初始化列表作为参数传递给重载决议选择的赋值运算符函数

由于没有适合这种情况的内置候选者,因此唯一可行的候选者是隐式声明的复制赋值运算符,其形式为:

Student&Student::operator=(Student const&)

对应的参数是{""}。参数传递将启动复制初始化,这意味着参数将从{""}; 这是一个列表初始化,下面的规则将适用于此

否则,如果 T 是引用类型,则生成一个纯右值。纯右值通过复制列表初始化来初始化其结果对象。然后使用纯右值直接初始化引用。临时的类型是 T 引用的类型,除非 T 是“对 U 的未知边界数组的引用”,在这种情况下,临时的类型是声明 U x[] H 中 x 的类型,其中 H是初始化列表。

这意味着引用将绑定到由 复制初始化的临时对象{""}。这是一个聚合初始化,因为它Student是一个聚合。并且临时的生命周期将在 full-expression 处结束,由以下规则决定:

临时对象被销毁作为评估完整表达式 ([intro.execution]) 的最后一步,该表达式(词法上)包含它们的创建点。

因此,正确的输出应该是

  Before assigning to S
  Destructor called // for the temporary 
  After assigning to S
  Destructor called  // for the object S
Run Code Online (Sandbox Code Playgroud)

因此,GCC 在这里是错误的。由于其行为不符合标准规定。相比之下,Clang实现了正确的行为。