Comment résoudre le problème de ref

18

Le programme court suivant

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

a un problème de référence pendant, qu'aucun compilateur ne semble avertir. Le problème est que les temporaires peuvent être liés à des const-refs, que vous pouvez ensuite conserver. La question est alors: Existe-t-il une méthode pour éviter d'entrer dans ce problème? De préférence, qui n'implique pas de sacrifier l'exactitude de const, ou de toujours faire des copies de gros objets.

sp2danny
la source
4
C'est délicat. Je peux vous assurer que je réfléchis à deux fois avant de faire une référence const de variable membre. En cas de doute, j'envisagerais de modéliser ces données d'une manière ou d'une autre pour que le pointeur intelligent puisse être impliqué (soit std::unique_ptrpour la propriété exclusive, soit pour la propriété std::shared_ptrpartagée, soit std::weak_ptr, au moins, pour reconnaître les données perdues).
Scheff
En C ++, vous ne payez pas ce dont vous n'avez pas besoin / utilisez. Il appartient au programmeur de veiller à ce que la durée de vie de l'objet référencé ne se termine pas tant que la référence est toujours utilisée / existante. Même chose pour les pointeurs bruts, ... Il y a des pointeurs intelligents pour vous apporter les fonctionnalités que vous avez demandées :)
Fareanor
2
Les membres de référence sont toujours une erreur: herbsutter.com/2020/02/23/references-simply
Maxim Egorushkin
Bien que le compilateur ne prévienne pas, ce bogue est captable par Valgrind et -fsanitize=address. Je ne pense pas qu'il existe de meilleure pratique pour l'éviter sans sacrifier les performances.
ks1322

Réponses:

8

Dans le cas où une méthode conserve une référence après son retour, c'est une bonne idée d'utiliser à la std::reference_wrapperplace de la référence normale:

#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. il est déjà livré avec un ensemble de surcharges empêchant la liaison de rvalues ​​et le passage involontaire de temporaires, il n'est donc pas nécessaire de se soucier d'une surcharge supplémentaire interdite prenant une rvalue Woop (std::vector<int> const &&) = delete;pour votre méthode:
Woop woop{someNums()}; // error
woop.report();
  1. il permet la liaison implicite de lvalues ​​afin de ne pas casser les appels valides existants:
auto nums{someNums()};
Woop woop{nums}; // ok
woop.report();
  1. il permet une liaison explicite de lvalues, ce qui est une bonne pratique pour indiquer que l'appelant conservera la référence après avoir renvoyé:
auto nums{someNums()};
Woop woop{::std::ref(nums)}; // even better because explicit
woop.report();
user7860670
la source
10

Une façon de rendre votre classe moins vulnérable pourrait être d'ajouter un constructeur supprimé qui prend une référence à droite. Cela empêcherait votre instance de classe de créer des liaisons avec des temporaires.

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

Ce constructeur supprimé ferait en fait le code O / P ne pas compiler, ce qui peut être le comportement que vous recherchez?

Gem Taylor
la source
3

Je suis d'accord avec les autres réponses et commentaires que vous devriez réfléchir attentivement si vous avez vraiment besoin de stocker une référence dans la classe. Et que si vous le faites, vous voudrez probablement un pointeur non-const vers un vecteur const à la place (ie std::vector<int> const * numbers_).

Cependant, si c'est le cas, je trouve que les autres réponses actuellement affichées sont à côté du sujet. Ils vous montrent tous comment vous approprier Woopces valeurs.

Si vous pouvez vous assurer que le vecteur que vous transmettez survivra à votre Woopinstance, vous pouvez explicitement désactiver la construction d'un à Wooppartir d'une valeur r. C'est possible en utilisant cette syntaxe C ++ 11:

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

Maintenant, votre exemple de code ne se compilera plus. Le compilateur avec donne une erreur similaire à:

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: Vous voulez probablement un constructeur explicite, voir par exemple Que signifie le mot-clé explicite? .

Darhuuk
la source
Il me semble avoir volé votre réponse là-bas. Désolé!
Gem Taylor
1

Pour éviter ce cas particulier, vous pouvez choisir de prendre un pointeur (car ce Weep(&std::vector<int>{1,2,3})n'est pas autorisé) ou vous pouvez prendre une référence non const qui provoquera également une erreur sur un temporaire.

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

Celles-ci ne garantissent toujours pas que la valeur reste valide, mais empêche au moins l'erreur la plus simple, ne crée pas de copie et n'a pas besoin d' numsêtre créée d'une manière spéciale (par exemple, comme std::shared_ptrou le std::weak_ptrfait).

std::scoped_lockprendre une référence au mutex serait un exemple, et celui où un ptr unique / partagé / faible n'est pas vraiment souhaité. Souvent, le std::mutexsera simplement un membre de base ou une variable locale. Vous devez toujours être très prudent, mais dans ces cas, il est généralement facile de déterminer la durée de vie.

std::weak_ptrest une autre option pour les non-propriétaires, mais vous forcez ensuite l'appelant à utiliser shared_ptr(et donc également à allouer des tas), et parfois ce n'est pas souhaité.

Si une copie est OK, cela évite simplement le problème.

Si Woopdoit s'approprier, passez comme valeur r et déplacez (et évitez complètement les problèmes de pointeur / référence), ou utilisez unique_ptrsi vous ne pouvez pas déplacer la valeur elle-même ou si le pointeur reste valide.

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

Ou si la propriété est partagée, vous pouvez l'utiliser shared_ptrpour tout, et elle sera supprimée avec la référence finale, mais cela peut rendre la surveillance des cycles de vie des objets très déroutante si elle est sur-utilisée.

Lancer de feu
la source
1

Vous pouvez utiliser template programminget arrayssi vous voulez avoir un objet qui contient un constconteneur. En raison du constexprconstructeur et constexpr arraysvous réalisez const correctnesset compile time execution.

Voici un article qui peut être intéressant: 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;
}

exécuter du code

Production:

3 5 7 11                                                                                                                        
1 2 3 5 12 3 51
M.Mac
la source
1
Avec les chiffres en tant que std::arrayceci est garanti de copier, même si un mouvement serait autrement disponible. En plus de cela wooping1et wooping2ne sont pas du même type, ce qui est loin d'être idéal.
sp2danny
@ sp2danny merci pour vos commentaires et je dois être d'accord avec vous sur les deux points. user7860670 a fourni une meilleure solution :)
M.Mac