Почему ссылки не восстанавливаются в C++
Ссылки на C++ имеют два свойства:
- Они всегда указывают на один и тот же объект.
- Они не могут быть 0.
Указатели противоположны:
- Они могут указывать на разные объекты.
- Они могут быть 0.
Почему в C++ нет "ненулевой, перезагружаемой ссылки или указателя"? Я не могу придумать вескую причину, почему ссылки не должны быть перезаписываемыми.
Изменить: вопрос возникает часто, потому что я обычно использую ссылки, когда я хочу убедиться, что "ассоциация" (я избегаю слова "ссылка" или "указатель" здесь) никогда не является недействительным.
Я не думаю, что когда-либо думал, что "здорово, что эта ссылка всегда относится к одному и тому же объекту". Если бы ссылки были перезаписываемыми, можно было бы получить текущее поведение, подобное этому:
int i = 3;
int& const j = i;
Это уже легальный C++, но бессмысленный.
Я повторяю свой вопрос следующим образом: "Каково было обоснование дизайна" ссылка - это объект "? Почему считалось полезным, чтобы ссылки всегда были одним и тем же объектом, а не только когда объявлялись как const?"
Ура, Феликс
17 ответов
Причина, по которой C++ не позволяет вам перепривязывать ссылки, приведена в "Построении и разработке C++" Страуструпа:
Невозможно изменить то, на что ссылается ссылка после инициализации. Таким образом, после инициализации ссылки на C++ невозможно сделать ссылку на другой объект позже; это не может быть повторно связано. Я был в прошлом укушен ссылками Algol68, где
r1=r2
можно либо назначить черезr1
объекту, на который ссылаются, или назначьте новое значение ссылкиr1
(Повторное связываниеr1
) в зависимости от типаr2
, Я хотел избежать таких проблем в C++.
В C++ часто говорят, что "ссылка - это объект". В некотором смысле это так: хотя ссылки и обрабатываются как указатели при компиляции исходного кода, ссылка предназначена для обозначения объекта, который не копируется при вызове функции. Поскольку ссылки не имеют прямой адресации (например, ссылки не имеют адреса и возвращают адрес объекта), семантически не имеет смысла переназначать их. Более того, в C++ уже есть указатели, которые обрабатывают семантику переустановки.
Потому что тогда у вас не будет повторяемого типа, который не может быть 0. Если вы не включили 3 типа ссылок / указателей. Что только усложнит язык из-за очень небольшого усиления (и тогда почему бы не добавить 4-й тип тоже? Не повторяемая ссылка, которая может быть 0?)
Лучший вопрос может быть: почему вы хотите, чтобы ссылки были перезаписываемыми? Если бы они были, это сделало бы их менее полезными во многих ситуациях. Компилятору будет сложнее выполнять анализ псевдонимов.
Кажется, что основная причина, по которой ссылки в Java или C# являются перезаписываемыми, заключается в том, что они выполняют работу указателей. Они указывают на объекты. Они не являются псевдонимами для объекта.
Каким должен быть эффект следующего?
int i = 42;
int& j = i;
j = 43;
В сегодняшнем C++ с невосстановимыми ссылками это просто. j является псевдонимом для i, и i заканчивается значением 43.
Если бы ссылки были перезаписываемыми, тогда третья строка связала бы ссылку j с другим значением. Это больше не псевдоним i, а целочисленный литерал 43 (что, конечно, неверно). Или, возможно, более простой (или, по крайней мере, синтаксически правильный) пример:
int i = 42;
int k = 43;
int& j = i;
j = k;
С многоразовыми ссылками. j будет указывать на k после оценки этого кода. С неосетируемыми ссылками C++, j по-прежнему указывает на i, и i присваивается значение 43.
Повторное использование ссылок изменяет семантику языка. Ссылка больше не может быть псевдонимом для другой переменной. Вместо этого он становится отдельным типом значения с собственным оператором присваивания. И тогда один из самых распространенных способов использования ссылок был бы невозможен. И ничего не получится взамен. Вновь приобретенный функционал для ссылок уже существовал в виде указателей. Так что теперь у нас есть два способа сделать то же самое, и нет способа сделать то, что делают ссылки в текущем языке C++.
Интересно, что многие ответы здесь немного размыты или даже не относятся к делу (например, это не потому, что ссылки не могут быть нулевыми или похожими, на самом деле, вы можете легко построить пример, где ссылка равна нулю).
Реальная причина, по которой перенастройка ссылки невозможна, довольно проста.
Указатели позволяют сделать две вещи: изменить значение за указателем (либо через
->
или*
оператор), и изменить сам указатель (прямое назначение=
). Пример:int a; int * p = &a;
- Изменение значения требует разыменования:
*p = 42;
- Изменение указателя:
p = 0;
- Изменение значения требует разыменования:
Ссылки позволяют вам только изменить значение. Зачем? Поскольку нет другого синтаксиса для выражения переустановки. Пример:
int a = 10; int b = 20; int & r = a; r = b; // re-set r to b, or set a to 20?
Другими словами, было бы неоднозначно, если бы вам было разрешено повторно установить ссылку. Это имеет еще больший смысл при передаче по ссылке:
void foo(int & r)
{
int b = 20;
r = b; // re-set r to a? or set a to 20?
}
void main()
{
int a = 10;
foo(a);
}
Надеюсь, это поможет:-)
Ссылка не является указателем, она может быть реализована в виде указателя в фоновом режиме, но ее базовая концепция не эквивалентна указателю. Ссылка должна выглядеть так *is*
объект, на который он ссылается. Поэтому вы не можете изменить его, и оно не может быть NULL.
Указатель - это просто переменная, которая содержит адрес памяти. У самого указателя есть собственный адрес памяти, и внутри этого адреса памяти он содержит другой адрес памяти, на который, как говорят, он указывает. Ссылка - это не то же самое, у нее нет собственного адреса, и, следовательно, она не может быть изменена для "хранения" другого адреса.
Я думаю, что часто задаваемые вопросы о ссылках на C++ в Parashift лучше всего говорят:
Важное примечание: Несмотря на то, что ссылка часто реализуется с использованием адреса на базовом ассемблере, не думайте о ссылке как о забавно выглядящем указателе на объект. Ссылка - это объект. Это не указатель на объект и не копия объекта. Это объект.
и снова в FAQ 8.5:
В отличие от указателя, когда ссылка привязана к объекту, она не может быть "переустановлена" для другого объекта. Сама ссылка не является объектом (она не имеет идентификатора; взятие адреса ссылки дает вам адрес референта; помните: ссылка является его референтом).
Повторно устанавливаемая ссылка будет функционально идентична указателю.
Относительно обнуляемости: вы не можете гарантировать, что такая "перезаписываемая ссылка" не является NULL во время компиляции, поэтому любой такой тест должен быть выполнен во время выполнения. Вы можете достичь этого сами, написав шаблон класса в стиле интеллектуального указателя, который выдает исключение при инициализации или назначении NULL:
struct null_pointer_exception { ... };
template<typename T>
struct non_null_pointer {
// No default ctor as it could only sensibly produce a NULL pointer
non_null_pointer(T* p) : _p(p) { die_if_null(); }
non_null_pointer(non_null_pointer const& nnp) : _p(nnp._p) {}
non_null_pointer& operator=(T* p) { _p = p; die_if_null(); }
non_null_pointer& operator=(non_null_pointer const& nnp) { _p = nnp._p; }
T& operator*() { return *_p; }
T const& operator*() const { return *_p; }
T* operator->() { return _p; }
// Allow implicit conversion to T* for convenience
operator T*() const { return _p; }
// You also need to implement operators for +, -, +=, -=, ++, --
private:
T* _p;
void die_if_null() const {
if (!_p) { throw null_pointer_exception(); }
}
};
Это может быть полезно в некоторых случаях - функция, принимающая non_null_pointer<int>
параметр, безусловно, передает больше информации вызывающей стороне, чем функция, принимающая int*
,
Вероятно, было бы менее запутанным назвать C++ ссылки "псевдонимами"? Как уже упоминали другие, ссылки в C++ должны быть как переменная, на которую они ссылаются, а не как указатель /ссылка на переменную. Таким образом, я не могу придумать вескую причину, по которой они должны быть сбрасываемыми.
при работе с указателями часто имеет смысл принимать значение NULL в качестве значения (в противном случае вы, вероятно, захотите использовать ссылку). Если вы специально хотите запретить удерживать значение null, вы всегда можете написать свой собственный умный тип указателя;)
Ссылки на C++ иногда могут быть принудительно установлены на 0 с некоторыми компиляторами (это плохая идея *, и это нарушает стандарт *).
int &x = *((int*)0); // Illegal but some compilers accept it
РЕДАКТИРОВАТЬ: согласно различным людям, которые знают стандарт намного лучше, чем я, приведенный выше код производит "неопределенное поведение". По крайней мере, в некоторых версиях GCC и Visual Studio, я видел, что это делает ожидаемую вещь: эквивалент установки указателя на NULL (и вызывает исключение NULL-указателя при обращении к нему).
На самом деле это не ответ, а обходной путь для этого ограничения.
По сути, когда вы пытаетесь "перепривязать" ссылку, вы фактически пытаетесь использовать одно и то же имя для ссылки на новое значение в следующем контексте. В C++ этого можно достичь, введя область видимости блока.
В примере Джальфа
int i = 42;
int k = 43;
int& j = i;
//change i, or change j?
j = k;
если вы хотите изменить i, напишите, как указано выше. Тем не менее, если вы хотите изменить значение j
означать k
, вы можете сделать это:
int i = 42;
int k = 43;
int& j = i;
//change i, or change j?
//change j!
{
int& j = k;
//do what ever with j's new meaning
}
Вы не можете сделать это:
int theInt = 0;
int& refToTheInt = theInt;
int otherInt = 42;
refToTheInt = otherInt;
... по той же причине, почему secondInt и firstInt не имеют здесь одно и то же значение:
int firstInt = 1;
int secondInt = 2;
secondInt = firstInt;
firstInt = 3;
assert( firstInt != secondInt );
Я согласен с принятым ответом. Но для удобства они ведут себя как указатели.
struct A{
int y;
int& x;
A():y(0),x(y){}
};
int main(){
A a;
const A& ar=a;
ar.x++;
}
работает. Увидеть
Причины проектирования поведения эталонных членов классов, передаваемых по константной ссылке
Существует обходной путь, если вы хотите, чтобы переменная-член являлась ссылкой, и вы хотите иметь возможность ее перепривязать. Хотя я нахожу это полезным и надежным, заметьте, что он использует некоторые (очень слабые) предположения относительно структуры памяти. Вам решать, находится ли это в пределах ваших стандартов кодирования.
#include <iostream>
struct Field_a_t
{
int& a_;
Field_a_t(int& a)
: a_(a) {}
Field_a_t& operator=(int& a)
{
// a_.~int(); // do this if you have a non-trivial destructor
new(this)Field_a_t(a);
}
};
struct MyType : Field_a_t
{
char c_;
MyType(int& a, char c)
: Field_a_t(a)
, c_(c) {}
};
int main()
{
int i = 1;
int j = 2;
MyType x(i, 'x');
std::cout << x.a_;
x.a_ = 3;
std::cout << i;
((Field_a_t&)x) = j;
std::cout << x.a_;
x.a_ = 4;
std::cout << j;
}
Это не очень эффективно, так как вам нужен отдельный тип для каждого переназначаемого ссылочного поля и сделать их базовыми классами; кроме того, здесь есть слабое предположение, что класс, имеющий единственный ссылочный тип, не будет иметь __vfptr
или любой другой type_id
связанное поле, которое потенциально может разрушить привязки во время выполнения MyType. Все компиляторы, которых я знаю, удовлетворяют этому условию (и было бы бессмысленно этого не делать).
Потому что иногда вещи не должны быть переориентированы. (Например, ссылка на синглтон.)
Потому что в функции замечательно знать, что ваш аргумент не может быть нулевым.
Но в основном потому, что он позволяет использовать что-то, что действительно является указателем, но действует как объект локального значения. C++ старается изо всех сил, цитируя Страуструпа, заставлять экземпляры классов "делать как целые числа d". Передача int через vaue - это дешево, потому что int вписывается в машинный регистр. Классы часто больше чем целые, и передача их по значению имеет значительные накладные расходы.
Возможность передавать указатель (который часто имеет размер типа int или, может быть, два целых числа), который "выглядит как" объект значения, позволяет нам писать более чистый код без "подробностей реализации" разыменований. И, наряду с перегрузкой операторов, это позволяет нам писать классы, использующие синтаксис, подобный синтаксису, используемому с int. В частности, это позволяет нам писать шаблонные классы с синтаксисом, который может в равной степени применяться к примитивам, таким как int, и классам (например, классу Complex number).
И, особенно с перегрузкой операторов, есть места, где мы должны возвращать объект, но опять же, намного дешевле вернуть указатель. Снова, возвращение ссылки - это наш выход.
И указатели сложны. Может быть, не для вас, и не для тех, кто понимает указатель - это просто значение адреса памяти. Но, вспомнив мой класс CS 101, они запутали нескольких учеников.
char* p = s; *p = *s; *p++ = *s++; i = ++*p;
может быть запутанным
Черт возьми, после 40 лет C люди все еще не могут даже согласиться с тем, что объявление указателя должно быть:
char* p;
или же
char *p;
Тот факт, что ссылки в C++ не обнуляются, является побочным эффектом того, что они являются просто псевдонимами.
Мне всегда было интересно, почему они не сделали для этого оператор ссылки (скажем:=).
Просто чтобы действовать кому-то на нервы, я написал некоторый код, чтобы изменить цель ссылки в структуре.
Нет, я не рекомендую повторять мой трюк. Он сломается, если будет перенесен на достаточно другую архитектуру.
Я полагаю, что это связано с оптимизацией.
Статическая оптимизация намного проще, когда вы можете однозначно знать, какой бит памяти означает переменная. Указатели нарушают это условие и повторная ссылка тоже будет.
Быть наполовину серьезным: ИМХО, чтобы они немного отличались от указателей;) Вы знаете, что можете написать:
MyClass & c = *new MyClass();
Если бы вы могли позже написать:
c = *new MyClass("other")
будет ли иметь смысл иметь какие-либо ссылки наряду с указателями?
MyClass * a = new MyClass();
MyClass & b = *new MyClass();
a = new MyClass("other");
b = *new MyClass("another");