So lösen Sie baumelnde Konstante ref

18

Das folgende kurze Programm

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

hat ein baumelndes Referenzproblem, vor dem kein Compiler zu warnen scheint. Das Problem ist, dass Provisorien an const-refs gebunden werden können, die Sie dann behalten können. Die Frage ist dann; Gibt es eine Methode, um dieses Problem zu vermeiden? Am besten eine, bei der es nicht darum geht, die Korrektheit der Konstanten zu opfern oder immer Kopien großer Objekte zu erstellen.

sp2danny
quelle
4
Das ist schwierig. Ich kann Ihnen versichern, dass ich zweimal darüber nachdenke, bevor ich eine Mitgliedsvariable const-Referenz mache. Im Zweifelsfall würde ich in Betracht ziehen, diese Daten so zu modellieren, dass ein intelligenter Zeiger beteiligt sein kann (entweder std::unique_ptrfür exklusives Eigentum std::shared_ptroder für gemeinsames Eigentum oder std::weak_ptrzumindest, um verlorene Daten zu erkennen).
Scheff
In C ++ zahlen Sie nicht für das, was Sie nicht benötigen / verwenden. Es ist Sache des Programmierers, darauf zu achten, dass die Lebensdauer des referenzierten Objekts nicht endet, solange die Referenz noch verwendet wird / vorhanden ist. Das Gleiche gilt für Rohzeiger, ... Es gibt intelligente Zeiger, die Ihnen die Funktionen bieten, nach denen Sie gefragt haben :)
Fareanor
2
Referenzmitglieder sind immer ein Fehler: herbsutter.com/2020/02/23/references-simply
Maxim Egorushkin
Obwohl der Compiler nicht warnt, kann dieser Fehler von Valgrind und -fsanitize=address. Ich glaube nicht, dass es eine bewährte Methode gibt, um dies zu vermeiden, ohne die Leistung zu beeinträchtigen.
ks1322

Antworten:

8

In Situationen, in denen eine Methode nach der Rückgabe eine Referenz behält, empfiehlt es sich, std::reference_wrapperanstelle einer normalen Referenz Folgendes zu verwenden :

#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. Es enthält bereits eine Reihe von Überladungen, die das Binden von R-Werten und das unbeabsichtigte Übergeben von Provisorien verhindern. Sie müssen sich also nicht mit einer zusätzlichen verbotenen Überladung befassen, die einen R-Wert Woop (std::vector<int> const &&) = delete;für Ihre Methode verwendet:
Woop woop{someNums()}; // error
woop.report();
  1. Es ermöglicht die implizite Bindung von l-Werten, damit vorhandene gültige Aufrufe nicht beschädigt werden:
auto nums{someNums()};
Woop woop{nums}; // ok
woop.report();
  1. Es ermöglicht die explizite Bindung von lWerten. Dies ist eine gute Vorgehensweise, um anzuzeigen, dass der Anrufer die Referenz nach der Rückgabe behält:
auto nums{someNums()};
Woop woop{::std::ref(nums)}; // even better because explicit
woop.report();
user7860670
quelle
10

Eine Möglichkeit, Ihre Klasse weniger anfällig zu machen, könnte darin bestehen, einen gelöschten Konstruktor hinzuzufügen, der eine Rechtsreferenz benötigt. Dies würde Ihre Klasseninstanz daran hindern, Bindungen an temporäre Elemente herzustellen.

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

Dieser gelöschte Konstruktor würde tatsächlich dazu führen, dass der O / P-Code nicht kompiliert wird. Welches Verhalten könnten Sie suchen?

Gem Taylor
quelle
3

Ich stimme den anderen Antworten und Kommentaren zu, die Sie sorgfältig überlegen sollten, wenn Sie wirklich eine Referenz in der Klasse speichern müssen. Und wenn Sie dies tun, möchten Sie wahrscheinlich stattdessen einen nicht konstanten Zeiger auf einen konstanten Vektor (dh std::vector<int> const * numbers_).

Wenn dies jedoch der Fall ist, finde ich, dass die anderen aktuell veröffentlichten Antworten nebensächlich sind. Sie alle zeigen Ihnen, wie Sie Woopdiese Werte besitzen können.

Wenn Sie sicherstellen können, dass der von Ihnen übergebene Vektor Ihre WoopInstanz überlebt , können Sie die Erstellung eines Woopaus einem r-Wert explizit deaktivieren . Dies ist mit dieser C ++ 11-Syntax möglich:

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

Jetzt wird Ihr Beispielcode nicht mehr kompiliert. Der Compiler mit gibt einen ähnlichen Fehler aus:

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: Sie möchten wahrscheinlich einen expliziten Konstruktor, siehe z. B. Was bedeutet das explizite Schlüsselwort? .

Darhuuk
quelle
Ich scheine dort Ihre Antwort gestohlen zu haben. Es tut uns leid!
Gem Taylor
1

Um diesen speziellen Fall zu verhindern, können Sie entweder einen Zeiger verwenden (da dies Weep(&std::vector<int>{1,2,3})nicht zulässig ist) oder eine nicht konstante Referenz verwenden, die auch bei einem temporären Fehler auftritt.

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

Diese garantieren immer noch nicht, dass der Wert gültig bleibt, stoppen jedoch zumindest den einfachsten Fehler, erstellen keine Kopie und müssen nicht numsauf spezielle Weise erstellt werden (z. B. wie std::shared_ptroder std::weak_ptr).

std::scoped_lockEin Verweis auf den Mutex wäre ein Beispiel, und eines, bei dem ein eindeutiger / geteilter / schwacher ptr nicht wirklich erwünscht ist. Oft ist das std::mutexnur ein Basismitglied oder eine lokale Variable. Sie müssen immer noch sehr vorsichtig sein, aber in diesen Fällen ist es im Allgemeinen einfach, die Lebensdauer zu bestimmen.

std::weak_ptrist eine weitere Option für Nicht-Besitzer, aber dann zwingen Sie den Anrufer zur Verwendung shared_ptr(und damit auch zur Heap-Zuweisung), und manchmal ist das nicht erwünscht.

Wenn eine Kopie in Ordnung ist, wird das Problem nur vermieden.

If Woopsollte den Besitz übernehmen, entweder als r-Wert übergeben und verschieben (und Zeiger- / Referenzprobleme vollständig vermeiden) oder verwenden, unique_ptrwenn Sie den Wert selbst nicht verschieben können oder möchten, dass der Zeiger gültig bleibt.

// 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;

Wenn das Eigentum geteilt wird, können Sie es shared_ptrfür alles verwenden und es wird zusammen mit der endgültigen Referenz gelöscht. Dies kann jedoch dazu führen, dass das Verfolgen von Objektlebenszyklen bei Überbeanspruchung sehr verwirrend wird.

Fire Lancer
quelle
1

Sie können verwenden template programmingund arrayswenn Sie ein Objekt haben möchten, das einen constContainer enthält. Aufgrund des constexprKonstruktors constexpr arrayserreichen Sie const correctnessund compile time execution.

Hier ist ein Beitrag, der interessant sein könnte: std :: move a 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;
}

Code ausführen

Ausgabe:

3 5 7 11                                                                                                                        
1 2 3 5 12 3 51
M.Mac
quelle
1
Mit den Zahlen als wird std::arraydiese garantiert kopiert, auch wenn sonst ein Zug möglich wäre. Darüber hinaus wooping1und wooping2sind nicht der gleiche Typ, was weniger als ideal ist.
sp2danny
@ sp2danny danke für dein Feedback und ich muss dir in beiden Punkten zustimmen. user7860670 lieferte eine bessere Lösung :)
M.Mac