将文字作为const-ref参数传递

Passing literal as a const ref parameter

本文关键字:const-ref 参数传递 文字      更新时间:2023-10-16

想象一下以下简化代码:

#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内部发生的内容,因为它与此问题无关。

编译器是否将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中,代码编译完美,考虑到bar::baz作为内联的使用,在C++14中,模板需要prvalue作为参数,因此编译器在对象代码中保留bar::baz的符号。这不会得到解决,因为你没有声明。在代码生成中,编译器应将constexpr视为常量值或右值,这可能会导致不同的方法。例如,如果被调用的函数是内联的,编译器可能会生成使用该特定值作为处理器指令常量参数的代码。这里的关键词是"应该是"answers"可能",这与一般标准文档中常见的免责条款中的"必须"一样不同。

对于基元类型,对于时态值和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

如果它是一个左值,例如,那么非常量引用将不允许我们转发参数

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

转发引用是对不合格简历的右值引用模板参数。如果P是转发引用,并且参数为对于类型推导。

您的示例#1。常量位置完全取决于编译器,并且没有在标准中定义。Linux上的GCC可能会在静态只读内存部分中分配这样的常量。优化可能会将其全部删除。

您的示例#2将不会编译(在链接之前)。由于作用域规则。所以你需要bar::baz

示例#3,我通常这样做:

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