将文字作为 const ref 参数传递

Passing literal as a const ref parameter

想象一下下面的简化代码:

#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; }

它不会 link,除非我定义 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里面发生的事情,因为它与这个问题无关。

在 C++17 中,考虑到 bar::baz 作为内联的使用,代码可以完美编译,在 C++14 中,模板需要 prvalue 作为参数,因此编译器在 bar::baz 中保留了一个符号目标代码。这不会得到解决,因为你没有那个声明。 constexpr 应该被编译器视为 constprvalue 或 rvalues,在可能导致不同方法的代码生成中。例如。如果被调用的函数是内联的,编译器可能会生成使用该特定值作为处理器指令常量参数的代码。这里的关键字是 "should be" 和 "may",它们与一般标准文档状态中通常的免责声明条款 "must" 不同。

对于原始类型,对于时间值和 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

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。常量位置完全取决于编译器,并没有在标准中定义。 Linux 上的 GCC 可能会在静态只读内存部分分配此类常量。优化可能会将其全部删除。

您的 示例 #2 将无法编译(在 link 之前)。由于范围规则。所以你需要bar::baz

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

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

Does the compiler stick 42 somewhere (on the stack?) and pass its address to foo?

创建类型 const int 的临时对象,用纯右值表达式 42 初始化,并绑定到引用。

在实践中,如果 foo 不是内联的,那需要在堆栈上分配 space,将 42 存储到其中,并传递地址。

Is there anything in the standard that dictates what is to be done in this situation (or is it strictly up to the compiler)?

[dcl.init.ref].

Besides ODR, why can't the compiler do whatever it did with 42 above?

因为根据语言,引用绑定到对象 bar::baz,除非编译器在编译调用时确切地知道 foo 在做什么,否则它必须假设这是重要的。例如,如果 foo 包含 assert(&x == &bar::baz);,则 不得 foo(bar::baz) 一起触发。

(在C++17中,bazimplicitly inline as a constexpr static data member;无需单独定义。)

Is there an elegant way to tell foo to accept x by value for primitive types?

在没有分析数据表明传递引用实际上导致问题的情况下,这样做通常没有多大意义,但如果您出于某种原因确实需要这样做,请添加(可能是 SFINAE 约束) 重载将是解决之道。