Come risolvere il costante const ref


18

Il seguente breve programma

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

presenta un problema di riferimento penzolante, di cui nessun compilatore sembra avvertire. Il problema è che i provvisori possono essere legati a const-refs, che puoi quindi tenere in giro. La domanda allora è; Esiste un metodo per evitare di affrontare questo problema? Preferibilmente uno che non implica sacrificare la costanza della costanza o fare sempre copie di oggetti di grandi dimensioni.


4
È difficile. Posso assicurarti che ci penso due volte prima di fare un riferimento const const variabile. In caso di dubbio, prenderei in considerazione di modellare questi dati in qualche modo in modo che possa essere coinvolto il puntatore intelligente (o std::unique_ptrper proprietà esclusiva std::shared_ptro proprietà condivisa, o std::weak_ptralmeno per riconoscere i dati persi).
Scheff,

In C ++, non paghi per ciò che non ti serve / usi. Spetta al programmatore assicurarsi che la durata dell'oggetto indicato non termini mentre il riferimento è ancora in uso / esistente. Stessa cosa per i puntatori non elaborati, ... Ci sono puntatori intelligenti per offrirti le funzionalità che hai chiesto :)
Fareanor

2
I membri di riferimento sono sempre un errore: herbsutter.com/2020/02/23/references-simply
Maxim

Sebbene il compilatore non avverta, questo bug è rilevabile da Valgrind e -fsanitize=address. Non credo che ci siano buone pratiche per evitarlo senza sacrificare le prestazioni.
ks1322

Risposte:


8

Nel caso in cui un metodo mantenga un riferimento dopo la restituzione, è una buona idea utilizzare std::reference_wrapperinvece del normale riferimento:

#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. viene già fornito con una serie di sovraccarichi che impediscono l'associazione di valori e il passaggio involontario di provvisori, quindi non è necessario preoccuparsi di un sovraccarico vietato aggiuntivo che prende un valore Woop (std::vector<int> const &&) = delete;per il metodo:
Woop woop{someNums()}; // error
woop.report();
  1. consente l'associazione implicita di valori in modo da non interrompere le invocazioni valide esistenti:
auto nums{someNums()};
Woop woop{nums}; // ok
woop.report();
  1. consente l'associazione esplicita di valori, che è una buona pratica per indicare che il chiamante manterrà il riferimento dopo la restituzione:
auto nums{someNums()};
Woop woop{::std::ref(nums)}; // even better because explicit
woop.report();

10

Un modo per rendere la tua classe meno vulnerabile potrebbe essere quello di aggiungere un costruttore eliminato che prende un ref di destra. Ciò impedirebbe alla tua istanza di classe di creare collegamenti ai provvisori.

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

Questo costruttore eliminato eliminerebbe effettivamente la compilazione del codice O / P, quale potrebbe essere il comportamento che stai cercando?


3

Sono d'accordo con le altre risposte e commenti che dovresti pensare attentamente se hai davvero bisogno di memorizzare un riferimento all'interno della classe. E che se lo fai, probabilmente vorrai invece un puntatore non const a un vettore const (cioè std::vector<int> const * numbers_).

Tuttavia, in tal caso, trovo che le altre risposte attualmente pubblicate siano accanto al punto. Ti stanno tutti mostrando come fare Wooppropri quei valori.

Se puoi assicurarti che il vettore che passi sopravvivrà alla tua Woopistanza, allora puoi disabilitare esplicitamente la costruzione di un Woopda un valore. Questo è possibile usando questa sintassi C ++ 11:

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

Ora il tuo codice di esempio non verrà più compilato. Il compilatore con fornisce un errore simile a:

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: Probabilmente vuoi un costruttore esplicito, vedi ad es. Cosa significa la parola chiave esplicita? .


Mi sembra di aver rubato la tua risposta lì. Scusate!
Gem Taylor

1

Per evitare quel caso particolare, puoi scegliere di prendere un puntatore (poiché Weep(&std::vector<int>{1,2,3})non è consentito) o potresti prendere un riferimento non const che si guasterà anche in un temporaneo.

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

Questi ancora non garantiscono che il valore rimanga valido, ma almeno interrompe l'errore più semplice, non crea una copia e non deve numsessere creato in un modo speciale (ad esempio come std::shared_ptro std::weak_ptrfa).

std::scoped_lockprendere un riferimento al mutex sarebbe un esempio, e uno in cui un ptr unico / condiviso / debole non è realmente desiderato. Spesso std::mutexsarà solo un membro di base o una variabile locale. Devi ancora stare molto attento, ma in questi casi è generalmente facile determinare la durata della vita.

std::weak_ptrè un'altra opzione per non possedere, ma poi costringi il chiamante a usare shared_ptr(e quindi anche a allocare l'heap), e talvolta ciò non è voluto.

Se una copia è OK, questo evita il problema.

Se Woopdeve diventare proprietario, passa come valore r e sposta (ed evita del tutto i problemi relativi a puntatore / riferimento), oppure usa unique_ptrse non puoi spostare il valore stesso o vuoi che il puntatore rimanga valido.

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

Oppure, se la proprietà è condivisa, puoi utilizzarla shared_ptrper tutto e verrà eliminata insieme al riferimento finale, ma ciò può rendere molto confuso il monitoraggio dei cicli di vita degli oggetti se utilizzato in modo eccessivo.


1

È possibile utilizzare template programminge arraysse si desidera avere un oggetto che contiene un constcontenitore. Grazie al constexprcostruttore e constexpr arraystu raggiungi const correctnesse compile time execution.

Ecco un post che può essere interessante: std :: sposta un vettore const

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

eseguire il codice

Produzione:

3 5 7 11                                                                                                                        
1 2 3 5 12 3 51

1
Con i numeri come std::arrayquesto è garantito da copiare, anche se una mossa sarebbe altrimenti disponibile. In cima che wooping1e wooping2non sono dello stesso tipo, che è meno che ideale.
sp2danny,

@ sp2danny grazie per il tuo feedback e devo essere d'accordo con te su entrambi i punti. user7860670 ha fornito una soluzione migliore :)
M.Mac
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.