Опасности использования ссылки на указатель в качестве аргумента функции - PullRequest
0 голосов
/ 06 мая 2020

Кто-нибудь может сказать мне, безопасно ли это, потому что я думаю, что это не так:

class A
{
public:
    A(int*& i) : m_i(i)
    {}

    int*& m_i;
};

class B
{
public:
    B(int* const& i) : m_i(i)
    {}

    int* const & m_i;
};


int main()
{
    int i = 1;
    int *j = &i;

    A a1(j);   // this works (1)
    A a2(&i);  // compiler error (2)
    B b(&i);   // this works again (3)
}

Я понимаю, почему (1) работает. Мы передаем указатель, функция принимает его как ссылку.

Но почему не работает (2)? С моей точки зрения, мы передаем один и тот же указатель, просто не присваивая его сначала переменной-указателю. Я предполагаю, что &i является rvalue и не имеет собственной памяти, поэтому ссылка не может быть действительной. Я могу согласиться с этим объяснением (если это правда).

Но какого черта компилирует (3)? Разве это не означает, что мы разрешаем недопустимую ссылку, поэтому b.m_i по существу не определено?

Я совершенно не прав в том, как это работает? Я спрашиваю, потому что получаю странный сбой модульного теста, который я могу объяснить только тем, что указатели становятся недействительными. Они случаются только с некоторыми компиляторами, поэтому я предполагал, что это должно быть что-то за пределами стандарта. ничего не подозревающий вызывающий абонент всегда может вызвать его с помощью &i, как с обычным аргументом указателя?

Дополнение: Как отметил @ franji1, следующая интересная мысль поможет понять, что здесь происходит. Я модифицировал main (), чтобы изменить внутренний указатель, а затем распечатать члены m_i:

int main()
{
    int i = 1;
    int *j = &i;  // j points to 1

    A a1(j);
    B b(&i);

    int re = 2;
    j = &re;  // j now points to 2

  std::cout << *a1.m_i << "\n";  // output: 2
  std::cout << *b.m_i << "\n";  // output: 1
}

Итак, очевидно, что a1 работает так, как задумано. Однако, поскольку b не может знать, что j был изменен, кажется, что он содержит ссылку на «личный» указатель, но меня беспокоит то, что он не определен в стандарте должным образом, поэтому могут быть компиляторы, для которых этот «личный» указатель не определен. Кто-нибудь может это подтвердить?

Ответы [ 3 ]

2 голосов
/ 06 мая 2020
Конструктор

A принимает неконстантную ссылку на указатель int*. A a1(j); работает, потому что j - это переменная int*, поэтому ссылка выполняется. И j переживает a1, поэтому член A::m_i можно безопасно использовать в течение всего времени жизни a1.

A a2(&i); не компилируется, потому что, хотя &i является int* , operator& возвращает временное значение, которое не может быть связано с неконстантной ссылкой.

B b(&i); компилируется, потому что конструктор B принимает ссылку в константу int*, которая может быть связана с временным. Временное время жизни будет увеличено за счет привязки к параметру конструктора i, но затем истечет после выхода из конструктора, поэтому член B::m_i будет висящей ссылкой и будет небезопасен для использования в все после выхода из конструктора.

1 голос
/ 06 мая 2020

j - это lvalue, и поэтому он может быть привязан к неконстантной ссылке lvaue.

&i - это prvalue, и его нельзя привязать к неконстантной ссылке lvalue. Вот почему (2) не компилирует

&i является prvalue (временным), и его можно привязать к ссылке const lvalue. Привязка prvalue к ссылке продлевает время жизни временного до времени жизни ссылки. В этом случае это временное время жизни продлевается до времени жизни параметра конструктора i. Затем вы инициализируете ссылку m_i на i (параметр конструктора) (который является ссылкой на временное), но поскольку i является lvalue, время жизни временного не увеличивается. В итоге вы получаете ссылочный член m_i, привязанный к объекту, который не является живым. У вас есть висячая ссылка . Доступ к m_i с этого момента (после завершения конструктора) - это неопределенное поведение.


Простая таблица того, к чему могут привязываться ссылки: C ++ 11 ссылка rvalue vs ссылка на const

0 голосов
/ 06 мая 2020

Указатель - это адрес памяти. Для простоты представьте указатель как переменную uint64_t, содержащую число, представляющее адрес памяти чего-либо. Ссылка - это просто псевдоним для некоторой переменной.

В примере (1) вы передаете указатель конструктору, ожидая ссылки на указатель. Он работает по назначению, поскольку компилятор получает адрес памяти, в которой хранится значение указателя, и передает его конструктору. Конструктор получает это число и создает указатель псевдонима. В результате вы получаете псевдоним j. Если вы измените j, чтобы указать на что-то еще, то m_i также будет изменен. Вы можете изменить m_i, чтобы он также указывал на что-нибудь еще.

В примере (2) вы передаете числовое значение конструктору, ожидая ссылки на указатель. Таким образом, вместо адреса адреса конструктор получает адрес, а компилятор не имеет возможности удовлетворить подпись конструктора.

В примере (3) вы передаете числовое значение конструктору, ожидая постоянной ссылки к указателю. Постоянная ссылка - это фиксированное число, просто адрес памяти. В этом случае компилятор понимает намерение и предоставляет адрес памяти для установки в конструкторе. В результате вы получаете фиксированный псевдоним i.

EDIT (для ясности): разница между (2) и (3) заключается в том, что &i не является действительной ссылкой на int*, но это действительная ссылка const на int*.

...