Como resolver dangling const ref

18

O seguinte programa curto

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

tem um problema de referência pendente, que nenhum compilador parece avisar. A questão é que os temporários podem ser vinculados a const-refs, que você pode manter por aí. A questão então é; Existe um método para evitar entrar nesse problema? De preferência um que não envolva o sacrifício da correção constante, ou sempre fazer cópias de objetos grandes.

sp2danny
fonte
4
Isso é complicado. Posso garantir que penso duas vezes antes de fazer uma variável membro referência const. Em caso de dúvida, eu consideraria modelar esses dados de alguma forma que o ponteiro inteligente possa estar envolvido (seja std::unique_ptrpara propriedade exclusiva std::shared_ptrou propriedade compartilhada ou std::weak_ptrpara, pelo menos, reconhecer dados perdidos).
Scheff
Em C ++, você não paga pelo que não precisa / usa. Cabe ao programador cuidar para que a vida útil do objeto referido não termine enquanto a referência ainda estiver em uso / existente. A mesma coisa para ponteiros brutos, ... Existem ponteiros inteligentes para oferecer os recursos que você pediu :)
Fareanor
2
Os membros de referência são sempre um erro: herbsutter.com/2020/02/23/references-simply
Maxim Egorushkin
Embora o compilador não avise, esse bug pode ser capturado por Valgrind e -fsanitize=address. Não acho que exista nenhuma prática recomendada para evitá-lo sem sacrificar o desempenho.
ks1322 10/03

Respostas:

8

Na situação em que algum método mantém uma referência após o retorno, é uma boa idéia utilizar em std::reference_wrappervez da referência 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. ele já vem com um conjunto de sobrecargas que impedem a vinculação de rvalores e a passagem não intencional de temporários, portanto, não há necessidade de se preocupar com uma sobrecarga proibida extra, que leva um rvalue Woop (std::vector<int> const &&) = delete;para o seu método:
Woop woop{someNums()}; // error
woop.report();
  1. Ele permite a ligação implícita de lvalues, para não interromper as invocações válidas existentes:
auto nums{someNums()};
Woop woop{nums}; // ok
woop.report();
  1. permite a ligação explícita de lvalues, o que é uma boa prática para indicar que o chamador manterá a referência após retornar:
auto nums{someNums()};
Woop woop{::std::ref(nums)}; // even better because explicit
woop.report();
user7860670
fonte
10

Uma maneira de tornar sua classe menos vulnerável pode ser adicionar um construtor excluído que aceite a referência correta. Isso impediria que sua instância de classe fizesse ligações com temporários.

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

Esse construtor excluído realmente tornaria o código O / P não compilado, qual pode ser o comportamento que você está procurando?

Gem Taylor
fonte
3

Eu concordo com as outras respostas e comentários que você deve pensar cuidadosamente se realmente precisar armazenar uma referência dentro da classe. E se você fizer isso, provavelmente desejará um ponteiro não-const para um vetor const (ie std::vector<int> const * numbers_).

No entanto, se for esse o caso, acho que as outras respostas atualmente postadas estão fora de questão. Eles estão todos mostrando como fazerWoop esses valores.

Se você puder garantir que o vetor que você transmite sobreviverá à sua Woopinstância, poderá desabilitar explicitamente a construção de a Wooppartir de um rvalor. Isso é possível usando esta sintaxe do C ++ 11:

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

Agora seu código de exemplo não será mais compilado. O compilador com um erro semelhante 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: Você provavelmente deseja um construtor explícito, consulte Por exemplo, o que a palavra-chave explícita significa? .

Darhuuk
fonte
Parece que eu roubei sua resposta lá. Desculpa!
Gem Taylor
1

Para evitar esse caso em particular, você pode optar por usar um ponteiro (uma vez que Weep(&std::vector<int>{1,2,3})não é permitido) ou por uma referência não-const, que também causará um erro temporário.

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

Isso ainda não garante que o valor permaneça válido, mas interrompe pelo menos o erro mais fácil, não cria uma cópia e não precisa numsser criado de uma maneira especial (por exemplo, como std::shared_ptrou std::weak_ptrfaz).

std::scoped_locktomar uma referência ao mutex seria um exemplo, e aquele em que o ptr exclusivo / compartilhado / fraco não é realmente desejado. Muitas vezes, ostd::mutex será apenas um membro básico ou variável local. Você ainda precisa ter muito cuidado, mas nesses casos geralmente é fácil determinar o tempo de vida.

std::weak_ptré outra opção para quem não possui, mas força o chamador a usar shared_ptr(e, portanto, também aloca a pilha), e às vezes isso não é desejado.

Se uma cópia estiver correta, isso evita o problema.

Se Woopdeve assumir a propriedade, passe como um valor r e mova (e evite problemas de ponteiro / referência inteiramente) ou use unique_ptrse você não puder mover o valor em si ou desejar que o ponteiro permaneça 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;

Ou, se a propriedade for compartilhada, você poderá usar shared_ptrpara tudo, e ela será excluída juntamente com a referência final, mas isso pode fazer com que o controle dos ciclos de vida do objeto fique muito confuso se for usado em excesso.

Fire Lancer
fonte
1

Você pode usar template programminge arraysse quiser ter um objeto que contenha um constcontêiner. Devido ao constexprconstrutor e constexpr arraysvocê consegue const correctnesse compile time execution.

Aqui está um post que pode ser interessante: 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;
}

código de execução

Resultado:

3 5 7 11                                                                                                                        
1 2 3 5 12 3 51
M.Mac
fonte
11
Com os números como std::arrayeste, é garantido que você copiará, mesmo que uma movimentação esteja disponível. Em cima disso wooping1e wooping2não são do mesmo tipo, que é menos do que o ideal.
sp2danny 10/03
@ sp2danny obrigado pelo seu feedback e eu tenho que concordar com você em ambos os pontos. user7860670 forneceu uma solução melhor :)
M.Mac 11/03