Jak rozwiązać zwisający const ref

18

Poniższy krótki program

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

ma wiszący problem z referencją, o którym żaden kompilator nie ostrzega. Problem polega na tym, że elementy tymczasowe można powiązać z ograniczeniami, które następnie można zachować. Pytanie zatem brzmi; Czy istnieje metoda uniknięcia tego problemu? Najlepiej taki, który nie wymaga poświęcenia stałej poprawności lub wykonania kopii dużych obiektów.

sp2danny
źródło
4
To trudne. Zapewniam cię, że dwa razy myślę, zanim utworzę odwołanie do zmiennej const const. W razie wątpliwości rozważę modelowanie tych danych w sposób, w jaki może być zaangażowany inteligentny wskaźnik ( std::unique_ptrdla wyłącznej własności std::shared_ptrlub współwłasności, lub std::weak_ptrprzynajmniej do rozpoznania utraconych danych).
Scheff
W C ++ nie płacisz za to, czego nie potrzebujesz / nie używasz. Programiści muszą zadbać o to, aby żywotność obiektu referencyjnego nie kończyła się, dopóki referencja jest nadal używana / istnieje. To samo dotyczy nieprzetworzonych wskaźników, ... Są inteligentne wskaźniki zapewniające funkcje, o które prosiłeś :)
Fareanor
2
Członkowie referencyjni są zawsze błędem: herbutter.com/2020/02/23/references-simply
Maxim Egorushkin
Chociaż kompilator nie ostrzega, ten błąd jest wykrywalny przez Valgrind i -fsanitize=address. Nie sądzę, aby istniała jakaś najlepsza praktyka, aby tego uniknąć bez poświęcania wydajności.
ks1322

Odpowiedzi:

8

W sytuacji, gdy jakaś metoda zachowuje referencję po zwrocie, dobrym pomysłem jest użycie std::reference_wrapperzamiast normalnej referencji:

#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. ma już zestaw przeciążeń zapobiegających wiązaniu wartości i niezamierzonemu przejściu tymczasowości, więc nie ma potrzeby zawracać sobie głowy dodatkowym niedozwolonym przeciążeniem, biorąc wartość Woop (std::vector<int> const &&) = delete;dla twojej metody:
Woop woop{someNums()}; // error
woop.report();
  1. umożliwia niejawne wiązanie wartości lv, więc nie zepsuje istniejących prawidłowych wywołań:
auto nums{someNums()};
Woop woop{nums}; // ok
woop.report();
  1. umożliwia wyraźne powiązanie wartości lv, co jest dobrą praktyką do wskazania, że ​​osoba dzwoniąca zachowa referencję po powrocie:
auto nums{someNums()};
Woop woop{::std::ref(nums)}; // even better because explicit
woop.report();
użytkownik7860670
źródło
10

Jednym ze sposobów, aby uczynić swoją klasę mniej podatną na zagrożenia, może być dodanie usuniętego konstruktora, który wymaga poprawnego odwołania. To powstrzyma instancję klasy przed tworzeniem powiązań z tymczasami.

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

Ten usunięty konstruktor sprawiłby, że kod O / P nie zostałby skompilowany, co może być zachowaniem, którego szukasz?

Gem Taylor
źródło
3

Zgadzam się z innymi odpowiedziami i komentarzami, które powinieneś dokładnie przemyśleć, jeśli naprawdę potrzebujesz przechowywać referencje w klasie. A jeśli to zrobisz, prawdopodobnie będziesz chciał zamiast wskaźnika const do wektora stałego (tj std::vector<int> const * numbers_.).

Jeśli jednak tak jest, stwierdzam, że inne obecnie opublikowane odpowiedzi nie mają znaczenia. Wszystkie pokazują, jak tworzyć Woopwłasne wartości.

Jeśli możesz upewnić się, że wektor, który przekazujesz, przeżyje Woopinstancję, możesz jawnie wyłączyć konstruowanie a Woopna podstawie wartości. Jest to możliwe przy użyciu tej składni C ++ 11:

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

Twój przykładowy kod nie będzie się już kompilował. Kompilator podający błąd podobny do:

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: Prawdopodobnie chcesz jawnego konstruktora, patrz np. Co oznacza to słowo kluczowe? .

Darhuuk
źródło
Wygląda na to, że ukradłem tam twoją odpowiedź. Przepraszam!
Gem Taylor
1

Aby uniknąć tego konkretnego przypadku, możesz wybrać wskaźnik (ponieważ Weep(&std::vector<int>{1,2,3})jest to niedozwolone) lub możesz wybrać odniesienie inne niż stałe, które również będzie tymczasowo powodować błąd.

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

Nadal nie gwarantują one, że wartość pozostaje ważna, ale powstrzymuje przynajmniej najłatwiejszy błąd, nie tworzy kopii i nie musi numsbyć tworzona w specjalny sposób (np. Jako std::shared_ptrlub std::weak_ptrrobi).

std::scoped_lockodwołanie się do muteksu byłoby przykładem, a taki, w którym unikalne / wspólne / słabe ptr nie jest tak naprawdę potrzebne. Często std::mutexbędzie to tylko podstawowy element członkowski lub zmienna lokalna. Nadal musisz być bardzo ostrożny, ale w takich przypadkach zazwyczaj łatwo jest określić długość życia.

std::weak_ptrjest kolejną opcją dla nie posiadających, ale wtedy zmuszasz osobę wywołującą do używania shared_ptr(a tym samym do przydzielania sterty), a czasem nie jest to pożądane.

Jeśli kopia jest w porządku, to po prostu pozwala uniknąć problemu.

Jeśli Woopnależy przejąć na własność albo przekazać jako wartość r i przenieść (i całkowicie unikać problemów ze wskaźnikiem / referencją), albo użyć, unique_ptrjeśli nie można przenieść samej wartości lub chcesz, aby wskaźnik pozostał ważny.

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

Lub jeśli własność jest współdzielona, ​​możesz użyć shared_ptrdo wszystkiego, a zostanie ona usunięta wraz z ostatecznym odniesieniem, ale może to spowodować, że śledzenie cykli życia obiektu stanie się bardzo mylące, jeśli zostanie nadmiernie wykorzystane.

Lansjer ognia
źródło
1

Możesz użyć template programmingi arraysjeśli chcesz mieć obiekt, który zawiera constkontener. Dzięki constexprkonstruktorowi constexpr arraysosiągasz const correctnessi compile time execution.

Oto post, który może być interesujący: 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;
}

uruchom kod

Wynik:

3 5 7 11                                                                                                                        
1 2 3 5 12 3 51
M.Mac
źródło
1
Liczby jako takie std::arraygwarantują kopiowanie, nawet jeśli ruch byłby w przeciwnym razie dostępny. Na dodatek wooping1i wooping2nie są tego samego typu, co jest dalekie od ideału.
sp2danny
@ sp2danny dziękuję za opinie i muszę się z tobą zgodzić w obu kwestiach. user7860670 zapewnił lepsze rozwiązanie :)
M.Mac