C ++ 11 rvalues ​​और चाल अर्थ विज्ञान भ्रम (वापसी विवरण)


435

मैं rvalue संदर्भों को समझने और C ++ 11 के शब्दार्थ को स्थानांतरित करने की कोशिश कर रहा हूं।

इन उदाहरणों में क्या अंतर है, और उनमें से कौन सी कोई वेक्टर कॉपी नहीं करने जा रहा है?

पहला उदाहरण

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> &&rval_ref = return_vector();

दूसरा उदाहरण

std::vector<int>&& return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

तीसरा उदाहरण

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

50
कृपया संदर्भ द्वारा कभी भी स्थानीय चर वापस न करें। एक संदर्भ संदर्भ अभी भी एक संदर्भ है।
fredoverflow

63
यह स्पष्ट रूप से जानबूझकर जानबूझकर किया गया था ताकि उदाहरणों के बीच शब्दार्थ अंतर को समझा जा सके
टारेंटयुला

@FredOverflow पुराना सवाल है, लेकिन आपकी टिप्पणी को समझने में मुझे एक सेकंड का समय लगा। मुझे लगता है कि # 2 के साथ सवाल यह था कि क्या std::move()एक सतत "प्रति" बनाई गई थी ।
डीव्यू

5
@DavidLively std::move(expression)कुछ भी नहीं बनाता है, यह बस एक xvalue को अभिव्यक्ति देता है। मूल्यांकन की प्रक्रिया में किसी भी वस्तु को कॉपी या स्थानांतरित नहीं किया जाता है std::move(expression)
fredoverflow

जवाबों:


562

पहला उदाहरण

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> &&rval_ref = return_vector();

पहला उदाहरण एक अस्थायी लौटाता है जिसे पकड़ा जाता है rval_ref। उस अस्थायी की rval_refपरिभाषा से परे उसका जीवन होगा और आप इसका उपयोग कर सकते हैं जैसे कि आपने इसे मूल्य से पकड़ा था। यह निम्नलिखित के समान है:

const std::vector<int>& rval_ref = return_vector();

सिवाय इसके कि मेरे पुनर्लेखन में आप स्पष्ट रूप rval_refसे एक गैर-कास्ट तरीके से उपयोग नहीं कर सकते ।

दूसरा उदाहरण

std::vector<int>&& return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

दूसरे उदाहरण में आपने रन टाइम एरर बनाया है। rval_refअब tmpफ़ंक्शन के अंदर नष्ट होने का संदर्भ रखता है । किसी भी भाग्य के साथ, यह कोड तुरंत दुर्घटनाग्रस्त हो जाएगा।

तीसरा उदाहरण

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

आपका तीसरा उदाहरण मोटे तौर पर आपके पहले के बराबर है। std::moveपर tmpअनावश्यक है और वास्तव में एक प्रदर्शन pessimization के रूप में यह वापसी मान अनुकूलन से रोक सकता है हो सकता है।

कोड का सबसे अच्छा तरीका है कि आप क्या कर रहे हैं:

सर्वश्रेष्ठ प्रणालियां

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

जैसा कि आप C ++ 03 में करेंगे। tmpलौकिक रूप से वापसी विवरण में एक प्रतिद्वंद्विता के रूप में माना जाता है। इसे या तो रिटर्न-वैल्यू-ऑप्टिमाइज़ेशन (कोई कॉपी, कोई चाल नहीं) के माध्यम से लौटाया जाएगा, या यदि कंपाइलर तय करता है कि यह आरवीओ प्रदर्शन नहीं कर सकता है, तो यह रिटर्न करने के लिए वेक्टर के मूव कंस्ट्रक्टर का उपयोग करेगा । केवल अगर आरवीओ का प्रदर्शन नहीं किया जाता है, और यदि लौटे हुए प्रकार में मूव कंस्ट्रक्टर नहीं होता है, तो वापसी के लिए कॉपी कंस्ट्रक्टर का उपयोग किया जाएगा।


64
जब आप किसी स्थानीय ऑब्जेक्ट को वैल्यू द्वारा लौटाते हैं तो कंपाइलर्स RVO होंगे, और फंक्शन का लोकल और रिटर्न एक ही होता है, और न ही cv- क्वॉलिफाइड होता है (न ही कांस्ट टाइप वापस करें)। स्थिति (:?) कथन के साथ लौटने से दूर रहें क्योंकि यह आरवीओ को बाधित कर सकता है। स्थानीय को किसी अन्य फ़ंक्शन में न लपेटें जो स्थानीय का संदर्भ देता है। बस return my_local;। एकाधिक रिटर्न स्टेटमेंट ठीक हैं और आरवीओ को बाधित नहीं करेंगे।
हावर्ड हिनांट

27
एक चेतावनी है: जब किसी स्थानीय वस्तु के सदस्य को लौटाया जाता है , तो चाल स्पष्ट होनी चाहिए।
बॉयसी

5
@NoSenseEtAl: रिटर्न लाइन पर कोई अस्थायी निर्माण नहीं किया गया है। moveएक अस्थायी नहीं बनाता है। यह एक xvalue के लिए एक अंतराल बनाता है, कोई प्रतिलिपि नहीं बनाता है, कुछ भी नहीं बनाता है, कुछ भी नष्ट नहीं करता है। यह उदाहरण ठीक वैसी ही स्थिति है जैसे कि आप lvalue-reference से लौटे हैं और moveरिटर्न लाइन से हटा दिया गया है: किसी भी तरह से आपको फ़ंक्शन के अंदर एक स्थानीय चर के लिए झूलने का संदर्भ मिला है और जिसे नष्ट कर दिया गया है।
हावर्ड हिनांट

15
"एकाधिक रिटर्न स्टेटमेंट ठीक हैं और आरवीओ को बाधित नहीं करेंगे": केवल तभी जब वे एक ही चर लौटाते हैं ।
डेडुप्लिकेटर

5
@ डेडप्लिकेटर: आप सही हैं। मैं उतना सही नहीं बोल रहा था, जितना मेरा इरादा था। मेरा मतलब था कि कई रिटर्न स्टेटमेंट RVO से कंपाइलर को मना नहीं करते (भले ही इसे लागू करना असंभव हो जाए), और इसलिए रिटर्न एक्सप्रेशन को अभी भी एक प्रतिद्वंद्विता माना जाता है।
हावर्ड हिनान्ट

42

उनमें से कोई भी नकल नहीं करेगा, लेकिन दूसरा एक नष्ट वेक्टर का उल्लेख करेगा। नामांकित रूवल संदर्भ लगभग नियमित कोड में कभी नहीं होते हैं। आप इसे केवल उसी तरह लिखते हैं जैसे आपने C ++ 03 में एक प्रति लिखी होगी।

std::vector<int> return_vector()
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

अब छोड़कर, वेक्टर को स्थानांतरित कर दिया गया है। उपयोगकर्ता एक वर्ग के अधिकांश मामलों में यह rvalue संदर्भ से निपटने नहीं है।


क्या आप वास्तव में सुनिश्चित हैं कि तीसरा उदाहरण वेक्टर कॉपी करने जा रहा है?
टारेंटयुला

@ टारेंटयुला: यह आपके वेक्टर का भंडाफोड़ करने वाला है। टूटने से पहले इसे किया या नहीं किया या नहीं, वास्तव में कोई फर्क नहीं पड़ता।
पिल्ला

4
मैं आपको प्रपोज करने का कोई कारण नहीं देखता। एक स्थानीय रैवेले रेफरेंस वैरिएबल को एक रैल्यू से बांधना पूरी तरह से ठीक है। उस स्थिति में, अस्थायी ऑब्जेक्ट के जीवनकाल को रेवल्यू रेफरेंस वेरिएबल के जीवनकाल तक बढ़ाया जाता है।
22

1
केवल स्पष्टीकरण का एक बिंदु, जब से मैं यह सीख रहा हूं। इस नए उदाहरण में, वेक्टर tmpको स्थानांतरित नहीं किया गया है rval_ref, लेकिन सीधे rval_refआरवीओ (यानी प्रतिलिपि चीरा) का उपयोग करते हुए लिखा गया है । के बीच अंतर है std::moveऔर नकल elision। A में std::moveअभी भी कॉपी किए जाने वाले कुछ डेटा शामिल हो सकते हैं; वेक्टर के मामले में, एक नया वेक्टर वास्तव में कॉपी कंस्ट्रक्टर में बनाया गया है और डेटा आवंटित किया गया है, लेकिन डेटा सरणी के थोक को केवल पॉइंटर (अनिवार्य रूप से) की नकल करके कॉपी किया जाता है। कॉपी एलिज़न सभी प्रतियों के 100% से बचा जाता है।
मार्क लकाटा

@MarkLakata यह NRVO है, RVO नहीं। NRVO वैकल्पिक है, यहां तक ​​कि C ++ 17 में भी। यदि इसे लागू नहीं किया जाता है, तो rval_refमूव कंस्ट्रक्टर के उपयोग से रिटर्न वैल्यू और वैरिएबल दोनों का निर्माण किया जाता है std::vector। कोई भी कॉपी कंस्ट्रक्टर इसमें शामिल नहीं है / दोनों के बिना std::movetmpएक के रूप में व्यवहार किया जाता है rvalue में returnइस मामले में बयान।
डैनियल लैंगर

16

इसका सरल उत्तर यह है कि आपको रवैल्यू रेफरेंस के लिए कोड लिखना चाहिए, जैसे कि आप रेगुलर रेफरेंस कोड चाहते हैं, और आपको समय के समान मानसिक रूप से 99% व्यवहार करना चाहिए। इसमें रिटर्निंग रेफरेंस के बारे में सभी पुराने नियम शामिल हैं (यानी किसी स्थानीय वैरिएबल का संदर्भ कभी नहीं लौटाते)।

जब तक आप एक टेम्प्लेट कंटेनर क्लास नहीं लिख रहे हैं, जिसे std :: फॉरवर्ड का लाभ उठाने की आवश्यकता है और एक जेनेरिक फ़ंक्शन लिखने में सक्षम है जो या तो लेवल्यू या रिवेल्यू रेफरेंस लेता है, यह कमोबेश सही है।

मूव कंस्ट्रक्टर और मूव असाइनमेंट के बड़े फायदों में से एक यह है कि यदि आप उन्हें परिभाषित करते हैं, तो कंपाइलर उनका उपयोग उन मामलों में कर सकते हैं जो आरवीओ (रिटर्न वैल्यू ऑप्टिमाइज़ेशन) और एनआरवीओ (रिटर्न वैल्यू ऑप्टिमाइज़ेशन) नामांकित होने में विफल हैं। कंटेनरों और तारों जैसी महंगी वस्तुओं को कुशलता से मूल्य से लौटाने के लिए यह बहुत बड़ा है।

अब जहां चीजें रूवल संदर्भों के साथ दिलचस्प हो जाती हैं, वह यह है कि आप उन्हें सामान्य कार्यों के तर्क के रूप में भी उपयोग कर सकते हैं। यह आपको उन कंटेनरों को लिखने की अनुमति देता है जिनके पास कॉन्स्ट रेफरेंस (कॉन्स्ट फू और अन्य) और रवैल्यू रेफरेंस (foo && अन्य) दोनों के लिए ओवरलोड हैं। यहां तक ​​कि अगर तर्क भी एक मात्र निर्माता के साथ पारित करने के लिए बहुत बुरा है कॉल यह अभी भी किया जा सकता है:

std::vector vec;
for(int x=0; x<10; ++x)
{
    // automatically uses rvalue reference constructor if available
    // because MyCheapType is an unamed temporary variable
    vec.push_back(MyCheapType(0.f));
}


std::vector vec;
for(int x=0; x<10; ++x)
{
    MyExpensiveType temp(1.0, 3.0);
    temp.initSomeOtherFields(malloc(5000));

    // old way, passed via const reference, expensive copy
    vec.push_back(temp);

    // new way, passed via rvalue reference, cheap move
    // just don't use temp again,  not difficult in a loop like this though . . .
    vec.push_back(std::move(temp));
}

एसटीएल कंटेनरों को लगभग किसी भी चीज़ के लिए ओवरलोड ले जाने के लिए अपडेट किया गया है (हैश कुंजी और मान, वेक्टर सम्मिलन, आदि), और वह है जहां आप उन्हें सबसे अधिक देखेंगे।

आप उन्हें सामान्य कार्यों के लिए भी उपयोग कर सकते हैं, और यदि आप केवल एक संदर्भ संदर्भ तर्क प्रदान करते हैं, तो आप कॉल करने वाले को ऑब्जेक्ट बनाने और फ़ंक्शन को स्थानांतरित करने के लिए बाध्य कर सकते हैं। यह वास्तव में अच्छे उपयोग की तुलना में एक उदाहरण के रूप में अधिक है, लेकिन मेरे प्रतिपादन पुस्तकालय में, मैंने सभी लोड किए गए संसाधनों को एक स्ट्रिंग सौंपी है, ताकि यह देखना आसान हो कि प्रत्येक ऑब्जेक्ट डीबगर में क्या दर्शाता है। इंटरफ़ेस कुछ इस तरह है:

TextureHandle CreateTexture(int width, int height, ETextureFormat fmt, string&& friendlyName)
{
    std::unique_ptr<TextureObject> tex = D3DCreateTexture(width, height, fmt);
    tex->friendlyName = std::move(friendlyName);
    return tex;
}

यह एक 'टपका हुआ अमूर्त' का एक रूप है, लेकिन मुझे इस तथ्य का लाभ उठाने की अनुमति देता है कि मुझे पहले से ही ज्यादातर समय स्ट्रिंग बनाना था, और अभी तक इसकी एक और प्रतिलिपि बनाने से बचें। यह बिल्कुल उच्च-प्रदर्शन कोड नहीं है, लेकिन संभावनाओं का एक अच्छा उदाहरण है क्योंकि लोगों को इस सुविधा का हैंग हो जाता है। इस कोड को वास्तव में आवश्यकता होती है कि चर या तो कॉल के लिए अस्थायी हो, या std :: Move invoked:

// move from temporary
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string("Checkerboard"));

या

// explicit move (not going to use the variable 'str' after the create call)
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, std::move(str));

या

// explicitly make a copy and pass the temporary of the copy down
// since we need to use str again for some reason
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string(str));

लेकिन यह संकलन नहीं होगा!

string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, str);

3

प्रति उत्तर नहीं , बल्कि एक दिशानिर्देश। अधिकांश समय स्थानीय T&&चर घोषित करने में बहुत समझदारी नहीं होती है (जैसा आपने किया था std::vector<int>&& rval_ref)। आपको अभी भी std::move()उन्हें foo(T&&)टाइप विधियों में उपयोग करना होगा । वहाँ भी समस्या है जो पहले ही उल्लेख किया गया था कि जब आप ऐसे rval_refफ़ंक्शन से लौटने की कोशिश करते हैं तो आपको मानक संदर्भ-से-नष्ट-अस्थायी-फ़ाइस्को मिलेगा।

ज्यादातर समय मैं निम्नलिखित पैटर्न के साथ जाऊंगा:

// Declarations
A a(B&&, C&&);
B b();
C c();

auto ret = a(b(), c());

आप अस्थायी वस्तुओं को लौटाने के लिए कोई भी रेफरी नहीं रखते हैं, इस प्रकार आप (अनुभवहीन) प्रोग्रामर की त्रुटि से बचते हैं जो एक स्थानांतरित वस्तु का उपयोग करना चाहते हैं।

auto bRet = b();
auto cRet = c();
auto aRet = a(std::move(b), std::move(c));

// Either these just fail (assert/exception), or you won't get 
// your expected results due to their clean state.
bRet.foo();
cRet.bar();

स्पष्ट रूप से ऐसे (हालांकि दुर्लभ) मामले हैं जहां एक फ़ंक्शन सही मायने में रिटर्न करता है T&&जो एक गैर-अस्थायी ऑब्जेक्ट का संदर्भ है जिसे आप अपनी वस्तु में स्थानांतरित कर सकते हैं।

आरवीओ के बारे में: ये तंत्र आम तौर पर काम करते हैं और संकलक अच्छी तरह से नकल करने से बच सकते हैं, लेकिन ऐसे मामलों में जहां वापसी का रास्ता स्पष्ट नहीं है (अपवाद, ifनाम वस्तु को निर्धारित करने वाले सशर्त आप वापस आ जाएंगे, और शायद कुछ अन्य) rrefs आपके उद्धारकर्ता हैं (भले ही संभावित रूप से अधिक महंगा)।


2

उनमें से कोई भी कोई अतिरिक्त नकल नहीं करेगा। यहां तक ​​कि अगर आरवीओ का उपयोग नहीं किया जाता है, तो नया मानक कहता है कि जब मैं विश्वास करता हूं कि रिटर्न बनाने के लिए कदम निर्माण को कॉपी करना पसंद किया जाता है।

मेरा मानना ​​है कि आपका दूसरा उदाहरण अपरिभाषित व्यवहार का कारण बनता है, क्योंकि आप एक स्थानीय चर का संदर्भ दे रहे हैं।


1

जैसा कि पहले उत्तर में टिप्पणियों में पहले ही उल्लेख किया गया है, return std::move(...);निर्माण स्थानीय चर की वापसी के अलावा अन्य मामलों में अंतर कर सकता है। यहां एक उदाहरणनीय उदाहरण है कि जब आप किसी सदस्य के साथ और उसके बिना लौटते हैं तो क्या दस्तावेज होते हैं std::move():

#include <iostream>
#include <utility>

struct A {
  A() = default;
  A(const A&) { std::cout << "A copied\n"; }
  A(A&&) { std::cout << "A moved\n"; }
};

class B {
  A a;
 public:
  operator A() const & { std::cout << "B C-value: "; return a; }
  operator A() & { std::cout << "B L-value: "; return a; }
  operator A() && { std::cout << "B R-value: "; return a; }
};

class C {
  A a;
 public:
  operator A() const & { std::cout << "C C-value: "; return std::move(a); }
  operator A() & { std::cout << "C L-value: "; return std::move(a); }
  operator A() && { std::cout << "C R-value: "; return std::move(a); }
};

int main() {
  // Non-constant L-values
  B b;
  C c;
  A{b};    // B L-value: A copied
  A{c};    // C L-value: A moved

  // R-values
  A{B{}};  // B R-value: A copied
  A{C{}};  // C R-value: A moved

  // Constant L-values
  const B bc;
  const C cc;
  A{bc};   // B C-value: A copied
  A{cc};   // C C-value: A copied

  return 0;
}

संभवतया, return std::move(some_member);केवल तभी समझ में आता है जब आप वास्तव में विशेष वर्ग के सदस्य को स्थानांतरित करना चाहते हैं, उदाहरण के लिए एक ऐसे मामले में, जहां के class Cउदाहरणों को बनाने के एकमात्र उद्देश्य के साथ अल्पकालिक एडाप्टर वस्तुओं का प्रतिनिधित्व करता है struct A

ध्यान दें कि struct Aहमेशा वस्तु की R- मान होने पर भी कैसे नकल की जाती है । ऐसा इसलिए है क्योंकि संकलक के पास यह बताने का कोई तरीका नहीं है कि किसी भी प्रकार के उदाहरण का उपयोग नहीं किया जाएगा। में , संकलक के पास यह जानकारी होती है , जिसके कारण इसे स्थानांतरित किया जाता है , जब तक कि इसका उदाहरण स्थिर न हो।class Bclass Bclass Bstruct Aclass Cstd::move()struct Aclass C

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.