C++ "Best"参数传递方法

C++ "Best" Parameter Passing Method

本文关键字:方法 参数传递 Best C++      更新时间:2023-10-16

我今天正在编写一个c++类,我写了一个函数,它将参数作为引用而不是指针,这是我很少做的事情。我总是通过指针传递。所以我正要把它改回来,然后我意识到——我不知道我是否应该,或者这是否重要。

所以我转向你们。我有三种传递参数的方法:

//1: By pointer
Object* foo(Object* bar) {…}
//2: By reference
Object& foo(Object& bar) {…}
//3: By value (just for completeness)
Object foo(Object bar) {…}

假设#3是出于性能原因(是的,我知道编译器在这方面已经很好了,但仍然),其他两个或多或少是相等的。

那么:什么是"最好的"?方法?指针?引用?两者的结合?或者这有什么关系吗?技术上的理由是最好的,但文体上的理由也一样好。

更新:我已经接受了YeenFei的答案,因为它处理了让我确定它的差异(即使我后来故意忽略了他的建议——我喜欢把NULL作为一个选项…)。但是每个人都提出了很好的观点——尤其是GMan(在评论中)和Nemo,在回答中处理了性能和价值传递。如果你是来找答案的,那就全部检查一下!

如果期望有效,我建议通过引用传递您的参数。这将是一个设计优化,并将您从防御性编程中拯救出来。

引用不能为空,而指针可以为空。如果你正在处理指针,你需要验证是否给定的指针是有效的(非空),不管它是在原始形式或包装在托管容器(shared_ptr),在使用它们之前。

所以我要做的是选择#3。考虑下面的代码:

struct Foo {
    int x;
    int y;
};
Foo
add(Foo a, Foo b)
{
    Foo result;
    result.x = a.x + b.x;
    result.y = a.y + b.y;
    return result;
}
Foo
add2(Foo &a, Foo &b)
{
    Foo result;
    result.x = a.x + b.x;
    result.y = a.y + b.y;
    return result;
}

尝试检查生成的程序集。注意add几乎完全是寄存器操作,调度得很好。注意add2是如何在没有任何重新排序的情况下进行大量内存访问的。

我写了一个main,它调用这些函数中的每个函数100亿次。结果呢?add耗时22秒,add2耗时26秒。即使对于这个微不足道的例子,按值传递版本的性能也比好10-20%。

这个结构很简单。函数也是如此。函数越复杂,按值传递的版本越可能更快,因为编译器知道这两个参数不会"重叠"。这对优化是一个巨大的好处。

当然,这个决定应该主要基于函数的语义:你需要NULL是一个合法值吗?如果是这样,显然你需要一个指针。你需要修改对象吗?然后使用指针或引用。

但是如果你不需要修改对象,最好按值传递它们,除非对象很大并且/或者有一个重要的复制构造函数(例如std::string)。如果按值传递确实太慢,则通过const引用或指向const的指针传递。

但是不要低估按值传递的潜在速度优势,这源于寄存器相对于内存和指令重排序的优势。请注意,这些优势在每一代CPU中变得更加明显。

通过指针传递和通过引用传递实际上是相同的,只是语法不同。我更喜欢通过指针传递,因为它使事情显式:

Object bar;
ptr_foo(&bar); // bar may change
ref_foo(bar); // can bar change? Now I need to go look at the prototype...
val_foo(bar); // bar cannot change. (Unless you use references here and there)
传递值和指针之间的唯一技术偏好,正如您所触及的,是如果类足够大,使其传递缓慢。

如果一切都是自己设计的,那么任何一天都可以引用。按照惯例,现代c++几乎不应该在任何地方有原始指针。动态分配的对象应该在资源管理容器(shared_ptrunique_ptr,或weak_ptr)中传递,但对于大多数操作来说,通过(const)引用传递需要修改的参数或重量级类型的参数是主要方式。不要忘记,如果你有可移动的类型,按值传递可能是一个可行的选择。

使用说明:

  1. const引用,如果对象未被修改
  2. 指针,如果对象被修改,或者可以为空
  3. 值,如果对象很小并且您关心性能,或者如果您需要在函数内复制对象。这允许编译器选择复制/移动参数的最佳方式。
  4. std::unique_ptr如果所有权转移到函数。

您可以查看https://www.boost.org/doc/libs/1_51_0/libs/utility/call_traits.htm库,它会自动将类型转换为最佳参数类型