传递类 by-value 时,调用方或被调用方是否调用析构函数



>假设我有以下(精简(代码:

class P { P(); P(const P&); ~P(); }
void foo(P x) {
  ...
}
void bar() {
  P p{};
  foo(p); // compiler uses P::(const P&) to construct the value for x
  ...
  // compiler calls P::~P() on p
}

编译器必须创建p的副本才能调用foo,因此调用者在调用之前调用复制构造函数。我的问题是,谁负责销毁这个创建的对象?似乎有两个有效的选择:

  1. 被叫方(即 foo ( 在返回之前对其所有按值参数调用析构函数,然后调用方解除分配内存(通过将其从堆栈中弹出(。
  2. 叫方不做任何事情,而被叫方(即 bar ( 在foo(p)调用结束时的序列点之前对所有临时变量调用析构函数。
标准在

[expr.call]/4 中回答了这个问题,并进行了令人惊讶的详细说明:

。每个参数的初始化和销毁发生在 调用函数。[ 示例:检查构造函数、转换函数或析构函数的访问 在调用函数中的调用点。如果函数参数的构造函数或析构函数引发 例外,对处理程序的搜索从调用函数的范围开始;特别是,如果函数 call 有一个函数 try-block(条款 18(,带有可以处理异常的处理程序,此处理程序不是 考虑。—结束示例 ]

换句话说,析构函数由调用函数调用。

调用方销毁它。请参阅 https://en.cppreference.com/w/cpp/language/lifetime。引用:

所有临时对象都将被销毁,作为评估 (在词法上(包含它们所在的点的完整表达式 已创建,如果创建了多个临时对象,则它们是 以与创造顺序相反的顺序销毁。

也保持这一点作为一般规则 - 一个,谁创造,谁破坏。通常顺序相反。

每当对象的生存期结束时都会调用析构函数,其中包括

范围结束,对于具有自动存储持续时间的对象和 通过绑定到参考文献而延长寿命的临时人员

因此,bar复制对象的所有者将在复制的对象上调用dtor。CPP首选项

调用者和被调用者的想法对我来说似乎是错误的。你应该在这里想到scopes

在创建函数堆栈的那一刻,P x foo的对象存在,该对象将被"创建"。这样,对象最终将通过离开范围来删除,在您的情况下,通过离开函数。

因此,在

引入新对象的函数中有一个局部作用域,然后又将该作用域保留在同一函数中,在理论上没有区别。

编译器能够"看到"你的对象是如何被使用的,特别是修改的,并且可以通过内联函数跳过"临时"对象的创建,只要代码的行为"好像"写了。

我的简短回答是,这取决于您使用的 ABI。

按照 Eljay 的建议,我在 Linux x86-64 和 Windows 64 上做了一个简单的 clang 实验,以了解为什么我无法生成代码来调用两个 ABI 上的 extern C++ 函数。

下面是代码示例:

class A
{
public:
  ~A();
};
void foo(A a)
{}

以下是 Linux x86-64 上 foo 函数的 llvm IR

; Function Attrs: noinline nounwind optnone uwtable mustprogress
define dso_local void @_Z3foo1A(%class.A* %0) #0 {
  ret void
}

在Windows 64上也是如此:

; Function Attrs: noinline nounwind optnone uwtable mustprogress
define dso_local void @"?foo@@YAXVA@@@Z"(i8 %0) #0 {
  %2 = alloca %class.A, align 1
  %3 = getelementptr inbounds %class.A, %class.A* %2, i32 0, i32 0
  store i8 %0, i8* %3, align 1
  call void @"??1A@@QEAA@XZ"(%class.A* nonnull dereferenceable(1) %2) #2
  ret void
}

析构函数显然是由 Windows 64 上的被调用方函数调用的,但在 Linux x86-64 上不是

最新更新