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'中缺少範圍是一個錯字。我修復了它。 –