16

想象以下简化的代码:传递文字作为一个const ref参数

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

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

(1)优化之外,当42被传递到foo会发生什么?

编译器是否在某个地方(在堆栈上?)将其地址传递给foo

(1a)标准中是否有任何规定在这种情况下要做什么(或严格遵守编译器)?


试想一下,略有不同的代码:(?由于ODR)

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

+0

可能为T &&和T生成的代码与42相同,它只是函数中的寄存器中的值,而不管它如何传递到函数? –

+0

如果这个关于编译器的实现的问题,那真的没有定义,作为一个constexpr值,编译器可能会把'movl'那42个注册到代码中。 – Swift

回答

11

该编译器棒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,bazimplicitly inline as a constexpr static data member;不需要单独的定义。)

有一种优雅的方法告诉foo由对于基本类型值接受x

有在缺乏分析数据显示,通过按引用的这样做实际上是造成问题一般没有多大意义,但如果你真的需要做的是出于某种原因,加上(可能SFINAE-受限制的)超负荷将是一条路。

+0

“在C++ 17中,baz隐式内联为一个constexpr静态数据成员...”您可以添加一个对该标准的引用吗?如果'bar'的类型是例如'std :: chrono :: milliseconds'呢?它还会被内联吗? –

+0

“在没有分析数据的情况下做这件事通常没有太多意义,表明通过引用实际上导致了问题......”我正在考虑复制的代价非常高的类。如果我理解正确,在这种情况下必须使用模板重载?即使'foo'是一个冗长的函数? –

+0

@InnocentBystander如果您的模板可以接受昂贵的副本类,请按引用传递。然后,如果某些使用原始类型的调用实际上会导致性能问题,请添加重载以按价值传递这些便宜到可复制类型。 –

2

考虑到将bar :: baz作为内联使用的C++ 17,使用C++ 14时,该模板需要使用prvalue作为参数,因此编译器会在目标代码中保留bar::baz的符号。由于您没有该声明,所以无法解决。编译器应该将constexpr视为constprvalue或rvalue,在代码生成中可能会导致不同的方法。例如。如果调用的函数是内联的,编译器可能会生成使用该特定值作为处理器指令的常量参数的代码。这里的关键词是“应该是”和“可能”,它们与通常标准文档状态中的“必须”不同于通常的免责声明条款。

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

如果你的编译器足够现代,你根本不需要引用,copy elision会为你节省额外的拷贝操作。为了证明:

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

可以避免转发问题,虽然这个过程也会涉及复制elision。编译器推断模板参数如下从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 

一个转发参考是一个rvalue参照CV-不合格 模板参数。如果P是转发参考,并且参数是 左值,则类型“左值参考A”用于代替 类型推导的A.

+1

*“使用移动语义实际上很危险”* - [这不是移动语义](https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers)。我认为这个问题比你意识到的更复杂。 – WhozCraig

+0

你错过了*我的*点。这不一定是一个右值参考,在OP代码的情况下,它绝对不是。 [见示例](http://ideone.com/cK4PUt)。 – WhozCraig

+1

'bar :: baz'中缺少范围是一个错字。我已经修复了它 –

2

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

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

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

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

'bar :: baz'中缺少范围是一个错字。我修复了它。 –