Как разрешить висячий const ref

18

Следующая короткая программа

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

имеет проблему со свисающими ссылками, о которой ни один компилятор не предупреждает. Проблема в том, что временные пользователи могут быть привязаны к const-refs, которые затем можно хранить. Тогда возникает вопрос: Есть ли способ избежать попадания в эту проблему? Предпочтительно тот, который не включает в себя жертву правильности или постоянное копирование больших объектов.

sp2danny
источник
4
Это сложно. Я могу заверить вас, что дважды подумал, прежде чем сделать ссылку на переменную-член const. Если вы сомневаетесь, я хотел бы как-то смоделировать эти данные, чтобы можно было использовать интеллектуальный указатель (либо std::unique_ptrдля исключительного владения, std::shared_ptrлибо для совместного владения, либо std::weak_ptr, по крайней мере, для распознавания потерянных данных).
Шефф
В C ++ вы не платите за то, что вам не нужно / не используется. Программист должен позаботиться о том, чтобы время жизни упомянутого объекта не заканчивалось, пока ссылка еще используется / существует. То же самое для необработанных указателей, ... Существуют умные указатели, чтобы предоставить вам функции, которые вы просили :)
Fareanor
2
Референтные члены всегда ошибаются: herbutter.com/2020/02/23/references-simply
Максим Егорушкин
Хотя компилятор не предупреждает, эта ошибка может быть исправлена ​​Valgrind и -fsanitize=address. Я не думаю, что есть лучшая практика, чтобы избежать этого без ущерба для производительности.
ks1322
Читайте

Ответы:

8

В ситуации, когда какой-либо метод сохраняет ссылку после возврата, рекомендуется использовать std::reference_wrapperвместо обычной ссылки:

#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. он уже поставляется с набором перегрузок, предотвращающих связывание значений r и непреднамеренную передачу временных значений, поэтому нет необходимости беспокоиться о дополнительной запрещенной перегрузке, принимающей значение r Woop (std::vector<int> const &&) = delete;для вашего метода:
Woop woop{someNums()}; // error
woop.report();
  1. он допускает неявное связывание lvalues, чтобы не нарушать существующие допустимые вызовы:
auto nums{someNums()};
Woop woop{nums}; // ok
woop.report();
  1. это позволяет явное связывание lvalues, что является хорошей практикой, чтобы указать, что вызывающая сторона сохранит ссылку после возврата:
auto nums{someNums()};
Woop woop{::std::ref(nums)}; // even better because explicit
woop.report();
user7860670
источник
10

Одним из способов сделать ваш класс менее уязвимым может быть добавление удаленного конструктора, который принимает правильную ссылку. Это остановит ваш экземпляр класса от привязок к временным.

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

Этот удаленный конструктор фактически сделает код O / P некомпилированным, что может быть за поведение, которое вы ищете?

Джем Тейлор
источник
3

Я согласен с другими ответами и комментариями, которые вы должны тщательно продумать, если вам действительно нужно хранить ссылку в классе. И что, если вы это сделаете, вы, вероятно, захотите вместо этого неконстантный указатель на константный вектор (т.е. std::vector<int> const * numbers_).

Однако, если это так, я считаю, что другие в настоящее время опубликованные ответы не имеют смысла. Они все показывают вам, как создать Woopэти ценности.

Если вы можете быть уверены, что вектор, который вы передадите, переживет ваш Woopэкземпляр, тогда вы можете явно отключить создание a Woopиз значения r. Это возможно с использованием синтаксиса C ++ 11:

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

Теперь ваш пример кода больше не будет компилироваться. Компилятор с ошибкой, аналогичной:

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: Вам, вероятно, нужен явный конструктор, см., Например, что означает явное ключевое слово? ,

Darhuuk
источник
Кажется, я украл твой ответ там. Сожалею!
Джем Тейлор
1

Чтобы предотвратить этот конкретный случай, вы можете либо взять указатель (так как Weep(&std::vector<int>{1,2,3})это не разрешено), либо вы можете использовать неконстантную ссылку, которая также будет иметь временную ошибку.

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

Они по-прежнему не гарантируют, что значение остается действительным, но, по крайней мере, останавливает простейшую ошибку, не создает копию и не требует numsспециального создания (например, как std::shared_ptrили std::weak_ptrделает).

std::scoped_lockв качестве примера можно привести ссылку на мьютекс, а в случае, когда уникальное / общее / слабое ptr действительно не требуется. Часто это std::mutexпросто базовый член или локальная переменная. Вы все еще должны быть очень осторожны, но в этих случаях, как правило, легко определить продолжительность жизни.

std::weak_ptrэто еще один вариант для не владеющих, но затем вы заставляете вызывающую сторону использовать shared_ptr(и, следовательно, также выделяете кучу), а иногда это не требуется.

Если копия в порядке, это просто устраняет проблему.

Если Woopнужно взять в собственность, либо передать как r-значение и переместить (и полностью избежать проблем с указателями / ссылками), либо использовать, unique_ptrесли вы не можете переместить само значение или хотите, чтобы указатель оставался действительным.

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

Или, если владение является общим, вы можете использовать его shared_ptrдля всего, и оно будет удалено вместе с окончательной ссылкой, но это может сделать отслеживание жизненных циклов объекта очень запутанным при чрезмерном использовании.

Огненный Лансер
источник
1

Вы можете использовать template programmingи, arraysесли вы хотите, чтобы объект, который содержит constконтейнер. Благодаря constexprконструктору и constexpr arraysвы добьетесь const correctnessи compile time execution.

Вот сообщение, которое может быть интересным: std :: move 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;
}

запустить код

Вывод:

3 5 7 11                                                                                                                        
1 2 3 5 12 3 51
M.Mac
источник
1
С цифрами как std::arrayэто гарантированно скопировать, даже если в противном случае будет доступен ход. Вдобавок ко всему wooping1и wooping2не одного типа, что не идеально.
sp2danny
@ sp2danny спасибо за ваш отзыв, и я должен согласиться с вами по обоим пунктам. user7860670 предоставил лучшее решение :)
M.Mac