在函数调用过程中,是否实际传递了一个未命名的参数?

18
template <typename TAG>
fn(int left, TAG, int right)
{
}

fn(0, some_type_tag(), 1);
/* or */
fn(0,int(), 1); // where the primitive, int, is not empty.

编辑:这个问题有两个不同的角度。

  1. 函数声明与定义。声明可能没有为参数命名,但声明可能会这样做。这不是我们感兴趣的角度。
  2. 模板方面,特别是在元编程中。问题中的参数是用于从trait中提取元结构的标签。这就是为什么该参数未命名的原因,我只关心编译时信息 - 标签类型。

/EDIT

我的标签通常是空结构体,但在代码的某些部分中它们是原始类型的typedefs。因此,我想知道现代编译器是否实际传递参数。这涉及到两个方面:

  1. 调整堆栈大小,考虑未命名参数类型的大小。
  2. 以传递的值构造堆栈。

让我们把重点放在gcc 4.5和msvc 2008+上。


1
结果会根据编译器和优化级别而有所不同。你需要尝试一下。 - Peter G.
我每天有10个类似的问题,如果我对所有问题都进行实验,我就无法完成任何工作;)我已经用我感兴趣的两个编译器限定了这个问题。 - Hassan Syed
2
@Hassan:我认为你应该在写整个问题之前先写一个简单的测试。 :) - Diego Sevilla
2
@diego,这可能是真的(但我怀疑),即使是这样,当我忘记答案时,将来我也不会有一个好的参考点可以返回。 - Hassan Syed
@sehe,这很重要,我会被迫再次运行实验……我不记得gcc手册,也不记得Intel x86汇编参考手册。虽然我完全有能力进行实验,但我正在询问那些可以脱口而出回答这个问题的人。 - Hassan Syed
显示剩余3条评论
4个回答

12

C++有独立的翻译过程,因为参数可以在声明中命名,但在函数定义中可能没有被命名,反之亦然,所以通常无法确定编译器是否知道省略函数参数是安全的。当所有内容都在同一个翻译单元中时,所有东西都可以被内联,并且参数名称对于优化是完全无关紧要的。

[添加]

对于这个特定情况,独立翻译可能并不重要,但是如果编译器构建者添加了这样的优化,他们必须要考虑。如果会破坏完全有效的代码,他们不会添加这样的优化。

至于模板,模板函数的类型必须等于非模板函数的类型,否则不可能将其地址取出并分配给函数指针。同样,您必须考虑独立翻译。在此TU中不获取foo<int>的地址并不意味着在另一个TU中也不会获取。


编译器在获取地址时会生成一个函数调用。但是,除非必须使用这个新函数,否则它不会(或者不应该)使用它。这仍然留下了一类函数,即我的用例正在审查的函数 =D。因此,也许我们可以假设,如果获取了地址,编译器将考虑标记参数并创建一个函数调用。 - Hassan Syed
这仍然存在一个问题:如果一个调用者考虑了参数,那么被调用者必须考虑,这又意味着所有的调用者都必须考虑。 - MSalters

8

实际上,这是一个非常有趣的问题。

首先,需要注意的是我们处于一种命令式语言中,意味着当你请求某些东西时(即使无用,例如构造一个未使用的对象),编译器需要遵守,除非它能提出等效形式。基本上,如果编译器能够证明这样做不会改变程序的含义,那么它可以省略参数。

当你编写函数调用时,最终会发生以下两件事情:

  • 要么内联
  • 要么实际发出call

如果是内联,则不传递任何参数,这有效地意味着如果编译器能够证明所涉及的构造函数和析构函数不执行任何重要工作,则可以删除未使用的对象(甚至不需要构建)。对于标记结构,它表现良好。

当发出调用时,它会按照特定的调用约定进行发出。每个编译器都有自己的调用约定集,这些调用约定指定如何传递各种参数(this指针等),通常试图利用可用寄存器。

由于仅使用函数的声明来确定调用约定(分离编译模型),因此实际上需要传递对象...

但是,如果我们谈论的是一个空结构,没有方法和状态,则这只是一些未初始化的内存。它不应该花费太多,但确实需要堆栈空间(至少需要保留它)。

使用llvm tryout演示:

struct tag {};

inline int useless(int i, tag) { return i; }

void use(tag);

int main() {
  use(tag());
  return useless(0, tag());
}

提供:

%struct.tag = type <{ i8 }>

define i32 @main() {
entry:
  ; allocate space on the stack for `tag`
  %0 = alloca %struct.tag, align 8                ; <%struct.tag*> [#uses=2]

  ; get %0 address
  %1 = getelementptr inbounds %struct.tag* %0, i64 0, i32 0 ; <i8*> [#uses=1]

  ; 0 initialize the space used for %0
  store i8 0, i8* %1, align 8

  ; call the use function and pass %0 by value
  call void @_Z3use3tag(%struct.tag* byval %0)
  ret i32 0
}

declare void @_Z3use3tag(%struct.tag* byval)

注意:

  • 如何删除对useless的调用,并且不为其构建参数
  • 无法删除对use的调用,因此需要为临时变量分配空间(希望新版本不会将内存初始化为0)

我正在寻找的恰到好处的风格、权威和语气:D - Hassan Syed
@Hassan:我正在跟你一样学习,而且我经常会注意力不集中,所以这绝对是权威的^^ - Matthieu M.

7
无论参数是否有名称,都不会影响函数签名,编译器应该传递它。考虑一个函数声明中的未命名参数可能在定义中被命名的情况。
现在,在像上面那样的模板特定情况下,编译器很可能会内联代码,这种情况下不会传递任何参数,未命名参数将没有效果。
如果你想要标记以解决不同的重载,你总是可以退回到指针,这样即使它被传递进来,成本也会很小。

谢谢,我确实对模板角度感兴趣。我已经进一步明确了问题以反映这一点。通常我可能使用原始类型作为标记,而不是空结构体,如果我使用指针,在64位代码中我将始终支付8字节的开销:D。尽管8个字节的调用堆栈空间对性能几乎没有影响,我想。 - Hassan Syed
如果您使用64位,很有可能调用约定会广泛地使用寄存器,这将意味着对于任何适合寄存器的内容,成本将是重置它的成本(在非内联情况下)。 - David Rodríguez - dribeas
我并不完全同意指针参数的观点,因为空结构体(大小为1)比指针(大小为4或8)要小。即使是基本类型的typedef也应该更小。 - Xeo
@Xeo:好的,如果你要使用空结构体(或其他小型类型),那么指针将不会提供任何优势,它只有在对象大小较大时才有帮助,而这并不是我们正在讨论的情况,因此我应该保持沉默 :) - David Rodríguez - dribeas

2

好问题,但您需要在编译器上尝试。理论上,如果一个参数没有被使用,它就不必在堆栈中分配。然而,调用者必须知道如何调用它,所以我的猜测是该元素实际上被分配在堆栈中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接