将文字字面量作为常量引用参数传递

18

想象以下简化的代码:

#include <iostream>
void foo(const int& x) { do_something_with(x); }

int main() { foo(42); return 0; }

(1) 优化除外,当42被传递给foo时会发生什么?

编译器会将42放在某个地方(栈上?),并将其地址传递给foo吗?

(1a) 标准中是否有规定在这种情况下应该执行什么操作(还是完全由编译器决定)?


现在,想象一下略微不同的代码:

#include <iostream>
void foo(const int& x) { do_something_with(x); }

struct bar { static constexpr int baz = 42; };

int main() { foo(bar::baz); return 0; }

除非我定义 int bar::baz;(由于ODR?),否则它不会链接。

(2) 除了ODR,为什么编译器不能像上面的42一样做任何事情?


简化事情的一个明显方法是将foo定义为:

void foo(int x) { do_something_with(x); }

然而,如果涉及到模板,该怎么办呢?例如:

template<typename T>
void foo(T&& x) { do_something_with(std::forward<T>(x)); }

(3) 有没有一种优雅的方法告诉foo对于原始类型按值接受x?还是我需要用SFINAE或类似的方式进行特化?

编辑:修改了foo中发生的事情,因为它与这个问题无关。


也许对于 T&& 和 T 生成的代码是相同的,对于 42 来说它只是一个寄存器中的值,在函数中无论如何传递它都是如此? - huseyin tugrul buyukisik
如果这个问题涉及到编译器的实现,那么它并没有明确定义。对于一个constexpr值,编译器可以在代码中将42移动到寄存器中。 - Swift - Friday Pie
3个回答

13
编译器是否将42粘贴在某个地方(堆栈上?),并将其地址传递给foo
创建一个临时对象,类型为const int,用prvalue表达式42初始化,并绑定到引用。
实际上,如果foo没有内联,那就需要在堆栈上分配空间,将42存储在其中,并传递地址。
在标准中有规定要在这种情况下执行什么操作吗(还是完全由编译器决定)? [dcl.init.ref]
除了ODR之外,为什么编译器不能像上面的42一样做任何事情?
因为根据语言,该引用绑定到对象bar::baz,除非编译器在编译调用时确切知道foo正在做什么,否则它必须认为这很重要。例如,如果foo包含assert(&x == &bar::baz);,那么不得使用foo(bar::baz)触发。

(在C++17中,baz作为一个隐式的constexpr静态数据成员;不需要单独定义.)

有没有一种优雅的方式告诉foo对于基本类型按值接受x?

通常情况下,在缺乏证明按引用传递实际上会导致问题的性能分析数据的情况下,这样做没有太大意义,但如果您真的因某些原因需要这样做,则添加(可能是SFINAE约束的)重载将是正确的方法。


在C++17中,baz作为constexpr静态数据成员,默认情况下是内联的...参考标准是哪个?如果bar的类型是std::chrono::milliseconds,它仍然会被内联吗?参考标准:ISO/IEC 14882:2017(E)对于bar的类型是std::chrono::milliseconds的情况,只要满足内联函数的条件,它也可以被内联。 - Super-intelligent Shade
在没有性能分析数据显示传递引用实际上会引起问题的情况下,这样做通常没有太大意义......我想的是一个非常昂贵的复制类。如果我理解你的意思正确,那么在这种情况下必须使用模板重载吗?即使 foo 是一个冗长的函数? - Super-intelligent Shade
@InnocentBystander 如果您的模板可以接受昂贵的复制类,请通过引用传递。然后,如果某些使用原始类型的调用实际上会导致性能问题,请添加重载以按值传递那些廉价的复制类型。 - T.C.
好的。我希望有一种避免这种情况的方法,但我可能会单独提出这个问题。 - Super-intelligent Shade
如果有人感兴趣,这里是涵盖C++17中内联变量的P0386R2 - Super-intelligent Shade

3

C++17版本中,如果将bar::baz用作内联,则该代码可以完美编译。而使用C++14时,该模板需要将prvalue作为参数,所以编译器会在目标代码中保留一个bar::baz符号。但由于你没有进行声明,因此它不会被解决。在代码生成中,编译器应将constexpr视为constprvalue或rvalues,这可能会导致不同的方法。例如,如果调用函数是内联的,则编译器可能会生成代码,将该特定值用作处理器指令的常量参数。这里的关键词是“应该”和“可能”,它们与通常免责声明条款在一般标准文档中的含义一样不同。

对于原始类型、临时值和constexpr,无论您使用哪种模板签名,都不会有区别。实际上,编译器如何实现它取决于平台和编译器...以及所使用的调用约定。我们甚至无法确定某些平台是否在堆栈上,因为某些平台没有堆栈,或者与x86平台上的堆栈实现不同。多种现代调用约定都使用CPU寄存器传递参数。

如果您的编译器足够现代,您根本不需要引用,复制省略将使您免除额外的复制操作。为了证明这一点:

#include <iostream>

template<typename T>
void foo(T x) { std::cout << x.baz << std::endl; }


#include <iostream>
using namespace std;

struct bar
{
    int baz;

    bar(const int b = 0): baz(b)
    {
        cout << "Constructor called" << endl;
    }    

    bar(const bar &b): baz(b.baz)  //copy constructor
    {
        cout << "Copy constructor called" << endl;
    } 
};

int main() 
{ 
    foo(bar(42)); 
}

这将导致输出:

Constructor called
42

通过引用传递参数,使用const引用不会比值传递更耗费资源,特别是对于模板而言。如果你需要不同的语义,你需要显式地特化模板。一些较旧的编译器可能无法正确支持后者。

template<typename T>
void foo(const T& x) { std::cout << x.baz << std::endl; }

// ...

bar b(42);
foo(b); 

输出:

Constructor called
42

非 const 引用不允许我们转发参数,如果它是左值,例如:

template<typename T>
void foo(T& x) { std::cout << x.baz << std::endl; }
// ...
foo(bar(42)); 

通过调用这个模板(称为完美转发),
template<typename T>
void foo(T&& x) { std::cout << x << std::endl; }

通过避免转发问题,尽管这个过程也涉及到复制省略,但是可以实现。从C++17开始,编译器将推断模板参数如下:

template <class T> int f(T&& heisenreference);
template <class T> int g(const T&&);
int i;
int n1 = f(i); // calls f<int&>(int&)
int n2 = f(0); // calls f<int>(int&&)
int n3 = g(i); // error: would call g<int>(const int&&), which
               // would bind an rvalue reference to an lvalue

A forwarding reference is an rvalue reference to a cv-unqualified template parameter. If P is a forwarding reference and the argument is an lvalue, the type “lvalue reference to A” is used in place of A for type deduction.


1
“使用移动语义实际上是危险的” - 那不是移动语义。我认为这个问题比你想象的要复杂。 - WhozCraig
1
bar::baz 缺少作用域是个笔误,我已经修复了。 - Super-intelligent Shade
1
@Swift 那么,你希望注意到的是,在折叠后,它不会将这些参数之一推导为右值引用,这在标准中是*定义的。也没有改变我最初说的话。没有移动语义,这个主题已经从你的答案中删除了。在指责袭击之前,不,我不是下调此答案的人。我的唯一真正问题已经通过您的编辑解决,而且它足够小,以至于我一开始就不会打下调。 - WhozCraig
1
@Swift 之所以编译成功是因为进行了优化。我会在我的问题中更加明确。 - Super-intelligent Shade
1
@Swift clang 也支持它,但我同意。它真的很棒,在模板编程时组合推断测试非常方便,当你对发生了什么感到困惑时。它为我节省了许多令人沮丧的推断谜团。 - WhozCraig
显示剩余10条评论

2

你的示例1。常量位置完全取决于编译器,没有标准定义。Linux上的GCC可能会将这些常量分配在静态只读内存段中。优化可能会彻底删除它。

你的示例2(链接之前)无法编译,由于作用域规则。所以你需要在那里使用bar :: baz

示例3, 我通常这样做:

template<typename T>
    void foo(const T& x) { std::cout << x << std::endl; }

bar::baz 中缺少作用域是一个笔误。我已经修复了它。 - Super-intelligent Shade

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