انتقل إلى المحتوى

إعادة توجيه مثالية و std::forward

افهم سبب اعتبار مراجع rvalue المُسماة قيمًا، وتعرف على نمط إعادة التوجيه الأساسي باستخدام std::forward، وانظر كيف يستخدم emplace_back إعادة التوجيه المتغير لإنشاء كائنات في مكانها.

المشكلة: مراجع Rvalue المسماة هي قيم L

هذه هي المشكلة الأساسية التي تحلها عملية إعادة التوجيه المثالية: عندما تقبل وسيطة كـT&&وتمررها إلى دالة أخرى، فإن name يجعلها قيمة:

void inner(int& x)  { std::cout << "lvalue\n"; }
void inner(int&& x) { std::cout << "rvalue\n"; }

void outer(int&& x) {
    inner(x);  // calls inner(int&) — x is an lvalue!
}

The value category is lost at every function boundary. If you are writing a wrapper function (logging, timing, factory), you need to forward the argument with its original value category preserved. Simply passing x always passes an lvalue. Using std::move(x) يمرر قيمة r دائمًا. ولا يصح أي منهما بالنسبة للمجمع العام.

std::forward: طاقم الممثلين الشرطي

std::forward(x)عبارة عن طاقم مشروط:

- إذا كانTمرجع lvalue (تم استنتاجه لأنه تم تمرير قيمة l)، فإنstd::forward(x)يُرجع مرجع lvalue — بدون تحويل.
- إذا كانTغير مرجعي (تم استنتاجه لأنه تم تمرير قيمة r)، فسيُرجعstd::forward(x)مرجع قيمة r — يلقي إلىT&&.

الفرق الرئيسي منstd::move:
-std::move دائمًا يُلقي على قيمة r (غير مشروطة).
- يتم تحويلstd::forwardإلى rvalue فقط إذا كانت الوسيطة الأصلية عبارة عن rvalue (مشروطة).

يجب استخدامstd::forwardفقط مع مراجع إعادة التوجيه (T&&حيث يتم استنتاجT). استخدامه في مكان آخر هو خطأ.

نمط إعادة التوجيه الكنسي

النمط: اقبل مرجع إعادة التوجيهT&&، ثم استخدمstd::forward(arg)للحفاظ على فئة القيمة عند التمرير إلى وظيفة أخرى.

perfect_forwarding.cpp
#include <iostream>
#include <string>
#include <utility>
#include <chrono>

// The function we want to wrap
void process(const std::string& s) {
    std::cout << "process(lvalue): " << s << '\n';
}
void process(std::string&& s) {
    std::cout << "process(rvalue): " << s << '\n';
    // Can steal s's resources since it's an rvalue
}

// Generic wrapper that perfectly forwards
template <typename T>
void timed_process(T&& arg) {
    auto start = std::chrono::steady_clock::now();

    // std::forward<T>(arg) preserves the original value category
    process(std::forward<T>(arg));

    auto elapsed = std::chrono::steady_clock::now() - start;
    std::cout << "Took: " << elapsed.count() << " ns\n";
}

int main() {
    std::string name = "Alice";
    timed_process(name);              // T = string&,  forwards as lvalue
    timed_process(std::string("Bob")); // T = string, forwards as rvalue
    timed_process("Charlie");          // T = const char(&)[8], forwards as lvalue
}

إعادة توجيه متغيرة وemplace_back

إن إعادة التوجيه المثالية تتألق حقًا مع القوالب المتنوعة. يقومemplace_backبإعادة توجيه وسيطات المُنشئ مباشرة إلى الحاوية، مما يؤدي إلى إنشاء الكائن في مكانه دون أي نسخ أو نقل.

variadic_forwarding.cpp
#include <iostream>
#include <vector>
#include <string>
#include <utility>

struct Employee {
    std::string name;
    int age;
    Employee(std::string n, int a) : name(std::move(n)), age(a) {
        std::cout << "Constructed: " << name << '\n';
    }
    Employee(const Employee&) { std::cout << "Copied\n"; }
    Employee(Employee&&) noexcept { std::cout << "Moved\n"; }
};

// Custom factory using variadic forwarding
template <typename T, typename... Args>
std::vector<T> make_vector_of(Args&&... args) {
    std::vector<T> v;
    // Fold expression (C++17) + emplace_back
    (v.emplace_back(std::forward<Args>(args)), ...);
    return v;
}

int main() {
    std::vector<Employee> team;

    // push_back: creates temporary, then MOVES it into vector
    team.push_back(Employee("Alice", 30));
    // Output: Constructed: Alice, Moved

    // emplace_back: constructs IN-PLACE — no copy, no move
    team.emplace_back("Bob", 25);
    // Output: Constructed: Bob

    std::cout << "---\n";
    // Variadic forwarding
    auto v = make_vector_of<std::string>("hello", std::string("world"), "!");
    for (const auto& s : v) std::cout << s << ' ';
    std::cout << '\n';
}

move vs forward: Know the Difference

| |std::move|std::forward|
|---|---|---|
| نوع المصبوب | إرسال غير مشروط إلىT&&| شرطي — القيمة l إذا كانTهوT&، والقيمة r إذا كانTهوT|
| حالة الاستخدام | "لقد انتهيت من هذا، اسرقه" | "مرر هذا مع فئة القيمة الأصلية" |
| السياق | أنواع الخرسانة، آخر استخدام للمتغير | مراجع إعادة توجيه القالب فقط |
| خطر | نقل قيمة لا تزال بحاجة إليها عن طريق الخطأ | الاستخدام بدون مرجع إعادة توجيه (لا معنى له) |

القاعدة الأساسية: استخدمstd::moveعندما تملك القيمة وتريد التخلي عنها. استخدمstd::forwardعندما تقوم بإعادة توجيه قيمة شخص آخر.

خطأ شائع

1. إعادة التوجيه أكثر من مرة: قد يتم إرسالstd::forward(arg)إلى rvalue. إذا قمت بإعادة توجيه نفس الوسيطة إلى وظيفتين، فقد تسرق الأولى مواردها، وتترك البيانات المهملة للثانية. قم بإعادة توجيه كل وسيطة مرة واحدة على الأكثر.

2.auto&&هو مرجع إعادة التوجيه: يستنتجfor (auto&& elem : container)فئة القيمة. يعد هذا أمرًا جيدًا بالنسبة للنطاق ولكن كن حذرًا عند استخدامauto&&في سياقات أخرى.

3.std::forwardبدون خصم:std::forward(s)هو مجردstd::move(s)مقنع. استخدمstd::forwardفقط مع معلمة قالب مستنتجة.

4. قوائم Braced-init:{1, 2, 3}ليس له نوع ولا يمكن إعادة توجيهه. تعيين لمتغير أولا.

أفضل ممارسة

1. استخدمstd::forwardفقط مع مراجع إعادة التوجيه (T&&حيث يتم استنتاجT).
2. إعادة توجيه كل وسيطة مرة واحدة على الأكثر — قد يتم نقل إعادة التوجيه، مما يترك المصدر فارغًا.
3. تفضلemplace_backعلىpush_back عند إنشاء الكائنات في الحاويات.
4. لا تقم بإعادة التوجيه في حلقة — قد يؤدي التكرار الأول إلى سرقة الوسيطة.
5. استخدمif constexprمعstd::is_lvalue_reference_v عندما تحتاج إلى سلوك مختلف للقيم l والقيم r.

النقاط الرئيسية
  • مراجع rvalue المُسماة هي lvalues ​​— تُفقد فئة القيمة عند حدود الوظيفة
  • std::forwardيُلقي بشكل مشروط: تظل القيمة lvalue lvalue، وتظل rvalue rvalue
  • std::moveغير مشروط (قيمة دائمًا)؛std::forwardمشروط (يحتفظ بالأصل)
  • يستخدمemplace_backإعادة التوجيه المتغير لإنشاء كائنات في مكانها بدون نسخ
  • قم بإعادة توجيه كل وسيطة مرة واحدة على الأكثر - قد تؤدي إعادة التوجيه إلى سرقة الموارد

اختبار — اختبر معرفتك

(20 نقطة خبرة)

1. ما الفرق بين`std::move`و`std::forward`؟

2. لماذا يعتبر`emplace_back`أكثر كفاءة من`push_back`في بناء العناصر؟

3. ماذا يحدث إذا قمت`std::forward`بنفس الوسيطة لوظيفتين مختلفتين؟