17

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

#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的事情,因为它与这个问题无关。

4

3 回答 3

13

编译器是否将 42 粘贴在某处(在堆栈上?)并将其地址传递给foo?

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

实际上,如果foo没有内联,则需要在堆栈上分配空间,存储42到其中并传递地址。

标准中是否有任何规定在这种情况下要做什么(或者它是否严格取决于编译器)?

[dcl.init.ref]

除了 ODR,为什么编译器不能像上面的 42 那样做呢?

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

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

有没有一种优雅的方式来告诉foo接受x原始类型的值?

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

于 2017-08-12T01:29:46.227 回答
3

考虑到 bar::baz 作为内联的使用,C++17 的代码编译完美,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

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

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

转发引用是对 cv 非限定模板参数的右值引用。如果 P 是转发引用并且参数是左值,则使用类型“对 A 的左值引用”代替 A 进行类型推导。

于 2017-08-12T00:41:48.097 回答
2

你的例子 #1。常量位置完全取决于编译器,并且没有在标准中定义。Linux 上的 GCC 可能会在静态只读内存部分中分配此类常量。优化可能会一起删除它。

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

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

template<typename T>
    void foo(const T& x) { std::cout << x << std::endl; }
于 2017-08-12T01:07:27.390 回答