Cómo resolver colgar const ref

18

El siguiente programa corto

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

tiene un problema de referencia pendiente, que ningún compilador parece advertir. El problema es que los temporales pueden estar vinculados a const-refs, que luego puede mantener. La pregunta entonces es; ¿Hay algún método para evitar entrar en este problema? Preferiblemente uno que no implique sacrificar la corrección constante, o siempre hacer copias de objetos grandes.

sp2danny
fuente
44
Eso es complicado Puedo asegurarle que lo pienso dos veces antes de hacer una referencia constante de miembro variable. En caso de duda, consideraría modelar estos datos de alguna manera en los que pueda estar involucrado el puntero inteligente (ya sea std::unique_ptrpara propiedad exclusiva std::shared_ptro propiedad compartida o std::weak_ptr, al menos, para reconocer datos perdidos).
Scheff
En C ++, no paga por lo que no necesita / usa. Es responsabilidad del programador asegurarse de que la vida útil del objeto referido no termine mientras la referencia todavía está en uso / existe. Lo mismo para los punteros en bruto, ... Hay punteros inteligentes para brindarle las funciones que solicitó :)
Fareanor
2
Los miembros de referencia siempre son un error: hierbasutter.com/2020/02/23/references-simply
Maxim Egorushkin
Aunque el compilador no advierte, Valgrind y este error pueden atraparlo -fsanitize=address. No creo que haya una mejor práctica para evitarlo sin sacrificar el rendimiento.
ks1322

Respuestas:

8

En una situación en la que algún método mantiene una referencia después de devolverla, es una buena idea utilizarla en std::reference_wrapperlugar de la referencia normal:

#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. ya viene con un conjunto de sobrecargas que evitan la unión de valores y el paso involuntario de temporarios, por lo que no hay necesidad de molestarse con una sobrecarga prohibida adicional que toma un valor Woop (std::vector<int> const &&) = delete;para su método:
Woop woop{someNums()}; // error
woop.report();
  1. permite el enlace implícito de valores para que no rompa las invocaciones válidas existentes:
auto nums{someNums()};
Woop woop{nums}; // ok
woop.report();
  1. permite el enlace explícito de valores, lo cual es una buena práctica para indicar que la persona que llama mantendrá la referencia después de devolver:
auto nums{someNums()};
Woop woop{::std::ref(nums)}; // even better because explicit
woop.report();
usuario7860670
fuente
10

Una forma de hacer que su clase sea menos vulnerable podría ser agregar un constructor eliminado que tome una referencia correcta. Esto evitaría que su instancia de clase haga enlaces a temporales.

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

Este constructor eliminado en realidad haría que el código O / P no se compilara, ¿cuál puede ser el comportamiento que está buscando?

Gema Taylor
fuente
3

Estoy de acuerdo con las otras respuestas y comentarios en que debe pensar detenidamente si realmente necesita almacenar una referencia dentro de la clase. Y si lo hace, probablemente querrá un puntero no constante a un vector constante en su lugar (es decir std::vector<int> const * numbers_).

Sin embargo, si ese es el caso, encuentro que las otras respuestas publicadas actualmente están fuera del punto. Todos te muestran cómo hacer Wooppropios esos valores.

Si puede asegurarse de que el vector que pasa sobrevivirá a su Woopinstancia, entonces puede deshabilitar explícitamente la construcción Woopde un valor r. Eso es posible usando esta sintaxis de C ++ 11:

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

Ahora su código de ejemplo ya no se compilará. El compilador da un error similar 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;
      |     ^~~~

PD: Probablemente desee un constructor explícito, consulte, por ejemplo, ¿qué significa la palabra clave explícita? .

Darhuuk
fuente
Parece que robé tu respuesta allí. ¡Lo siento!
Gema Taylor
1

Para evitar ese caso en particular, puede optar por tomar un puntero (ya Weep(&std::vector<int>{1,2,3})que no está permitido) o puede tomar una referencia no constante que también generará un error temporal.

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

Estos todavía no garantizan que el valor siga siendo válido, pero al menos detiene el error más fácil, no crea una copia y no necesita numsser creado de una manera especial (por ejemplo, como std::shared_ptro lo std::weak_ptrhace).

std::scoped_locktomar una referencia al mutex sería un ejemplo, y uno donde ptr único / compartido / débil realmente no se desea. A menudo std::mutex, solo será un miembro básico o una variable local. Aún debe tener mucho cuidado, pero en estos casos generalmente es fácil determinar la duración de la vida.

std::weak_ptres otra opción para no ser propietario, pero luego obliga a la persona que llama a usar shared_ptr(y, por lo tanto, también a la asignación de montón), y a veces eso no es deseable.

Si una copia está bien, eso simplemente evita el problema.

Si Woopdebe tomar posesión, pase como un valor r y mueva (y evite los problemas de puntero / referencia por completo), o use unique_ptrsi no puede mover el valor en sí o si desea que el puntero siga siendo válido.

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

O si se comparte la propiedad, puede usarlo shared_ptrpara todo, y se eliminará junto con la referencia final, pero esto puede hacer que el seguimiento de los ciclos de vida de los objetos se vuelva muy confuso si se usa en exceso.

Lancer de fuego
fuente
1

Puede usar template programmingy arrayssi desea tener un objeto que contenga un constcontenedor. Debido al constexprconstructor y constexpr arrayslogras const correctnessy compile time execution.

Aquí hay una publicación que puede ser interesante: std :: mover un vector constante

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

Ejecutar código

Salida:

3 5 7 11                                                                                                                        
1 2 3 5 12 3 51
M.Mac
fuente
1
Con los números como std::arrayeste, se garantiza que esto se copiará, incluso si un movimiento estuviera disponible de otra manera. Además de eso wooping1y wooping2no son del mismo tipo, que es menos que ideal.
sp2danny
@ sp2danny gracias por sus comentarios y tengo que estar de acuerdo con usted en ambos puntos. user7860670 proporcionó una mejor solución :)
M.Mac