วิธีแก้ปัญหาการอ้างอิง const ที่ค้าง


18

โปรแกรมย่อต่อไปนี้

#include <vector>
#include <iostream>

std::vector<int> someNums()
{
    return {3, 5, 7, 11};
}

class Woop
{
public:
    Woop(const std::vector<int>& nums) : numbers(nums) {}
    void report()
    {
        for (int i : numbers)
            std::cout << i << ' ';
        std::cout << '\n';
    }
private:
    const std::vector<int>& numbers;
};

int main()
{
    Woop woop(someNums());
    woop.report();
}

มีปัญหาในการอ้างอิงห้อยซึ่งดูเหมือนว่าไม่มีคอมไพเลอร์เตือน ปัญหาคือขมับสามารถผูกมัดกับกลุ่มอ้างอิงซึ่งคุณอาจจะต้องระวังไว้ คำถามคือ; มีวิธีการหลีกเลี่ยงปัญหานี้หรือไม่? โดยเฉพาะอย่างยิ่งที่ไม่เกี่ยวข้องกับการเสียสละความถูกต้อง const หรือทำสำเนาของวัตถุขนาดใหญ่


4
นั่นเป็นเรื่องยุ่งยาก ฉันสามารถมั่นใจได้ว่าฉันคิดว่าสองครั้งก่อนที่ฉันจะทำการอ้างอิงตัวแปรสมาชิก หากมีข้อสงสัยผมจะพิจารณาในการจำลองข้อมูลนี้อย่างใดที่ชี้สมาร์ทสามารถมีส่วนร่วม (ทั้งstd::unique_ptrเป็นเจ้าของ แต่เพียงผู้เดียวหรือstd::shared_ptrหรือความเป็นเจ้าของร่วมกันหรือstd::weak_ptrเพื่ออย่างน้อยรับรู้ข้อมูลที่หายไป)
Scheff

ใน C ++ คุณไม่ต้องจ่ายเงินสำหรับสิ่งที่คุณไม่ต้องการ / ใช้ ขึ้นอยู่กับโปรแกรมเมอร์ที่จะดูแลว่าอายุการใช้งานของออบเจ็กต์ที่อ้างอิงไม่สิ้นสุดในขณะที่การอ้างอิงยังคงใช้งานอยู่ / ที่มีอยู่ สิ่งเดียวกันสำหรับตัวชี้แบบดิบ ... มีตัวชี้อัจฉริยะที่นำคุณสมบัติที่คุณขอมาให้ :)
Fareanor

2
สมาชิกอ้างอิงเป็นความผิดพลาดเสมอ: herbutter.com/2020/02/23/references-simply
Maxim Egorushkin

แม้ว่าคอมไพเลอร์ไม่ได้เตือนข้อผิดพลาดนี้เป็น catchable โดย Valgrind -fsanitize=addressและ ฉันไม่คิดว่าจะมีวิธีปฏิบัติที่ดีที่สุดที่จะหลีกเลี่ยงได้โดยไม่ต้องเสียสละประสิทธิภาพ
ks1322

คำตอบ:


8

ในสถานการณ์ที่บางวิธีเก็บการอ้างอิงหลังจากส่งคืนเป็นความคิดที่ดีที่จะใช้std::reference_wrapperแทนการอ้างอิงปกติ:

#include <functional>

class Woop
{
public:
    using NumsRef = ::std::reference_wrapper<const std::vector<int>>;
    Woop(NumsRef nums) : numbers_ref{nums} {}
    void report()
    {
        for (int i : numbers_ref.get())
            std::cout << i << ' ';
        std::cout << '\n';
    }
private:
    NumsRef numbers_ref;
};
  1. มันมาพร้อมกับชุดของการโอเวอร์โหลดที่ป้องกันการรวมตัวกันของค่าและการส่งผ่านของทางขมับโดยไม่ตั้งใจดังนั้นจึงไม่จำเป็นต้องกังวลกับการโอเวอร์โหลดที่ไม่ได้รับอนุญาตเป็นพิเศษที่ใช้Woop (std::vector<int> const &&) = delete;วิธีการของคุณ:
Woop woop{someNums()}; // error
woop.report();
  1. มันอนุญาตให้มีผลผูกพันโดยปริยายของ lvalues ​​ดังนั้นมันจะไม่ทำลายการร้องขอที่ถูกต้องที่มีอยู่:
auto nums{someNums()};
Woop woop{nums}; // ok
woop.report();
  1. อนุญาตให้มีการรวมค่า lvalues ​​อย่างชัดเจนซึ่งเป็นแนวปฏิบัติที่ดีเพื่อระบุว่าผู้เรียกจะเก็บการอ้างอิงไว้หลังจากส่งคืน:
auto nums{someNums()};
Woop woop{::std::ref(nums)}; // even better because explicit
woop.report();

10

วิธีหนึ่งที่จะทำให้ชั้นเรียนของคุณมีความเสี่ยงน้อยลงคือการเพิ่มตัวสร้างที่ถูกลบซึ่งใช้การอ้างอิงที่ถูกต้อง สิ่งนี้จะหยุดอินสแตนซ์ในชั้นเรียนของคุณจากการผูกเข้ากับขมับ

Woop(std::vector<int>&& nums)  =delete;

ตัวสร้างที่ถูกลบนี้จริง ๆ แล้วจะทำให้รหัส O / P ไม่ได้รวบรวมซึ่งอาจเป็นพฤติกรรมที่คุณกำลังมองหา?


3

ฉันเห็นด้วยกับคำตอบและความคิดเห็นอื่น ๆ ที่คุณควรพิจารณาอย่างรอบคอบหากคุณต้องการเก็บข้อมูลอ้างอิงไว้ในชั้นเรียน และถ้าคุณทำคุณอาจต้องการให้ตัวชี้ที่ไม่ใช่ const เป็นเวกเตอร์ const แทน (เช่นstd::vector<int> const * numbers_)

อย่างไรก็ตามหากเป็นกรณีนี้ฉันพบว่าคำตอบอื่น ๆ ที่โพสต์ในขณะนี้อยู่ข้างๆ พวกเขาทั้งหมดแสดงให้คุณเห็นถึงวิธีการสร้างWoopคุณค่าเหล่านั้น

หากคุณมั่นใจได้ว่าเวกเตอร์ที่คุณส่งผ่านนั้นจะอยู่ได้นานกว่าWoopอินสแตนซ์ของคุณคุณสามารถปิดใช้งานการสร้าง a Woopจากค่า rvalue ได้อย่างชัดเจน ที่เป็นไปได้โดยใช้ไวยากรณ์ C ++ 11 นี้:

Woop (std::vector<int> const &&) = delete;

ตอนนี้โค้ดตัวอย่างของคุณจะไม่รวบรวมอีกต่อไป คอมไพเลอร์ที่มีข้อผิดพลาดคล้ายกับ:

prog.cc: In function 'int main()':
prog.cc:29:25: error: use of deleted function 'Woop::Woop(const std::vector<int>&&)'
   29 |     Woop woop(someNums());
      |                         ^
prog.cc:15:5: note: declared here
   15 |     Woop(std::vector<int> const &&) = delete;
      |     ^~~~

PS: คุณอาจต้องการตัวสร้างที่ชัดเจนดูเช่นคำหลักที่ชัดเจนหมายถึงอะไร .


ฉันดูเหมือนจะขโมยคำตอบของคุณที่นั่น ขออภัย!
Gem Taylor

1

เพื่อป้องกันกรณีดังกล่าวคุณสามารถเลือกที่จะใช้พอยน์เตอร์ (เนื่องจากWeep(&std::vector<int>{1,2,3})ไม่ได้รับอนุญาต) หรือคุณอาจใช้การอ้างอิงที่ไม่ใช่แบบคอนซึ่งจะเกิดข้อผิดพลาดชั่วคราว

Woop(const std::vector<int> *nums);
Woop(std::vector<int> *nums);
Woop(std::vector<int>& nums);

สิ่งเหล่านี้ยังไม่รับประกันว่าค่าจะยังคงใช้ได้ แต่จะหยุดความผิดพลาดที่น้อยที่สุดไม่สร้างสำเนาและไม่จำเป็นต้องnumsสร้างในรูปแบบพิเศษ (เช่นเป็นstd::shared_ptrหรือstd::weak_ptrไม่)

std::scoped_lockการอ้างอิงถึง mutex จะเป็นตัวอย่างและอีกอันที่ไม่ต้องการ ptr ที่ไม่ซ้ำกัน / ใช้ร่วมกัน / อ่อน มักstd::mutexจะเป็นเพียงสมาชิกพื้นฐานหรือตัวแปรท้องถิ่น คุณยังคงต้องระวังให้มาก แต่ในกรณีเหล่านี้โดยทั่วไปจะง่ายต่อการกำหนดอายุ

std::weak_ptrเป็นอีกตัวเลือกสำหรับผู้ที่ไม่ได้เป็นเจ้าของ แต่คุณบังคับให้ผู้โทรใช้shared_ptr(และยังจัดสรรฮีปด้วย) และบางครั้งก็ไม่ต้องการ

หากการคัดลอกเป็นไปได้นั่นจะเป็นการหลีกเลี่ยงปัญหา

หากWoopควรเป็นเจ้าของการส่งผ่านเป็นค่า r และย้าย (และหลีกเลี่ยงปัญหาตัวชี้ / การอ้างอิงทั้งหมด) หรือใช้unique_ptrหากคุณไม่สามารถย้ายค่าตัวเองหรือต้องการให้ตัวชี้ยังคงใช้ได้

// the caller can't continue to use nums, they could however get `numbers` from Woop or such like
// or just let Woop only manipulate numbers directly.
Woop(std::vector<int> &&nums) 
   : numbers(std::move(nums)) {}
std::vector<int> numbers;

// while the caller looses the unique_ptr, they might still use a raw pointer, but be careful.
// Or again access numbers only via Woop as with the move construct above.
Woop(std::unique_ptr<std::vector<int>> &&nums) 
    : numbers(std::move(nums)) {}
std::unique_ptr<std::vector<int>> numbers;

หรือถ้ามีการแชร์ความเป็นเจ้าของคุณสามารถใช้shared_ptrสำหรับทุกสิ่งและมันจะถูกลบไปพร้อมกับการอ้างอิงขั้นสุดท้าย แต่สิ่งนี้สามารถติดตามวัฏจักรชีวิตของวัตถุทำให้เกิดความสับสนได้หากใช้มากเกินไป


1

คุณสามารถใช้template programmingและarraysถ้าคุณต้องการมีวัตถุที่เก็บconstภาชนะ เนื่องจากconstexprคอนสตรัคและconstexpr arraysคุณประสบความสำเร็จและconst correctnesscompile time execution

นี่คือโพสต์ที่อาจน่าสนใจ: std :: move const vector

#include <array>
#include <iostream>
#include <vector>


std::array<int,4>  someNums()
{
    return {3, 5, 7, 11};
}


template<typename U, std::size_t size>
class Woop
{
public:

template<typename ...T>
    constexpr Woop(T&&... nums) : numbers{nums...} {};

    template<typename T, std::size_t arr_size>
    constexpr Woop(std::array<T, arr_size>&& arr_nums) : numbers(arr_nums) {};

    void report()
    const {
        for (auto&& i : numbers)
            std::cout << i << ' ';
         std::cout << '\n';
    }



private: 
    const std::array<U, size> numbers;
    //constexpr vector with C++20
};

int main()
{
    Woop<int, 4> wooping1(someNums());
    Woop<int, 7> wooping2{1, 2, 3, 5, 12 ,3 ,51};

    wooping1.report();
    wooping2.report();
    return 0;
}

เรียกใช้รหัส

เอาท์พุท:

3 5 7 11                                                                                                                        
1 2 3 5 12 3 51

1
ด้วยตัวเลขที่เป็นเช่นstd::arrayนี้รับประกันว่าจะคัดลอกแม้ว่าการย้ายจะเป็นอย่างอื่นก็ตาม ด้านบนของที่wooping1และwooping2ไม่ได้เป็นประเภทเดียวกันซึ่งน้อยกว่าอุดมคติ
sp2danny

@ sp2danny ขอบคุณสำหรับความคิดเห็นของคุณและฉันต้องเห็นด้วยกับคุณทั้งสองประเด็น user7860670 ให้ทางออกที่ดีกว่า :)
M.Mac
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.